Files
umami/tests/api/websites.spec.ts

294 lines
10 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import { expect, test } from './fixtures';
import { UNKNOWN_UUID } from './helpers/constants';
import { dateRange } from './helpers/dates';
import {
createTeam,
deleteTeam,
deleteWebsite,
uniqueDomain,
uniqueName,
} from './helpers/entities';
import { HOSTNAME } from './seed/dataset';
test.describe('Websites', () => {
test.describe.configure({ mode: 'serial' });
let teamId = '';
let websiteId = '';
let teamWebsiteId = '';
test.beforeAll(async ({ admin }) => {
teamId = (await createTeam(admin)).id;
});
test.afterAll(async ({ admin }) => {
for (const id of [websiteId, teamWebsiteId]) {
if (id) {
await deleteWebsite(admin, id);
}
}
if (teamId) {
await deleteTeam(admin, teamId);
}
});
test('POST /api/websites creates a website for the current user', async ({ admin, seed }) => {
const name = uniqueName('website');
const domain = uniqueDomain();
const response = await admin.post('/api/websites', { name, domain });
expect(response.status).toBe(200);
expect(response.body).toMatchObject({ name, domain, userId: seed.admin.id, shareId: null });
websiteId = response.body.id;
});
test('POST /api/websites creates a website for a team', async ({ admin }) => {
const response = await admin.post('/api/websites', {
name: uniqueName('team-website'),
domain: uniqueDomain(),
teamId,
});
expect(response.status).toBe(200);
expect(response.body).toMatchObject({ teamId, userId: null });
teamWebsiteId = response.body.id;
});
test('POST /api/websites accepts a fixed id', async ({ admin }) => {
const id = randomUUID();
const response = await admin.post('/api/websites', {
id,
name: uniqueName('website'),
domain: uniqueDomain(),
});
expect(response.status).toBe(200);
expect(response.body.id).toBe(id);
await deleteWebsite(admin, id);
});
test('POST /api/websites validates the body', async ({ admin }) => {
const domain = await admin.post('/api/websites', { name: 'x', domain: 'not a domain' });
const name = await admin.post('/api/websites', { domain: uniqueDomain() });
expect(domain.status).toBe(400);
expect(domain.body.error.code).toBe('bad-request');
expect(name.status).toBe(400);
});
test('POST /api/websites requires a user that can create websites', async ({ api, viewer }) => {
const anonymous = await api.post('/api/websites', { name: 'x', domain: uniqueDomain() });
const viewOnly = await viewer.post('/api/websites', { name: 'x', domain: uniqueDomain() });
expect(anonymous.status).toBe(401);
expect(viewOnly.status).toBe(401);
});
test('GET /api/websites lists the current user websites', async ({ admin, user }) => {
const response = await admin.get('/api/websites');
const search = await admin.get('/api/websites', { params: { search: 'zzz-no-such-website' } });
const other = await user.get('/api/websites');
expect(response.status).toBe(200);
expect(response.body.data.map((w: any) => w.id)).toContain(websiteId);
expect(response.body.data.map((w: any) => w.id)).not.toContain(teamWebsiteId);
expect(search.body.data).toHaveLength(0);
expect(other.body.data.map((w: any) => w.id)).not.toContain(websiteId);
});
test('GET /api/websites?includeTeams includes team websites', async ({ admin }) => {
const response = await admin.get('/api/websites', { params: { includeTeams: 'true' } });
expect(response.status).toBe(200);
expect(response.body.data.map((w: any) => w.id)).toContain(teamWebsiteId);
});
test('GET /api/websites/{websiteId} returns a website', async ({
admin,
api,
viewer,
share,
seed,
}) => {
const response = await admin.get(`/api/websites/${websiteId}`);
const shared = await (await share()).get(`/api/websites/${seed.website.id}`);
const anonymous = await api.get(`/api/websites/${websiteId}`);
const denied = await viewer.get(`/api/websites/${websiteId}`);
expect(response.status).toBe(200);
expect(response.body.id).toBe(websiteId);
expect(shared.status).toBe(200);
expect(shared.body.id).toBe(seed.website.id);
expect(anonymous.status).toBe(401);
expect(denied.status).toBe(401);
});
test('POST /api/websites/{websiteId} updates name and domain', async ({ admin, user }) => {
const name = uniqueName('renamed');
const domain = uniqueDomain();
const response = await admin.post(`/api/websites/${websiteId}`, { name, domain });
const denied = await user.post(`/api/websites/${websiteId}`, { name });
expect(response.status).toBe(200);
expect(response.body).toMatchObject({ id: websiteId, name, domain });
expect(denied.status).toBe(401);
});
test('POST /api/websites/{websiteId} manages the legacy share id', async ({ admin }) => {
const created = await admin.post(`/api/websites/${websiteId}`, { shareId: 'SPECSHARE1' });
const removed = await admin.post(`/api/websites/${websiteId}`, { shareId: null });
expect(created.status).toBe(200);
expect(created.body.shareId).toBe('SPECSHARE1');
expect(removed.status).toBe(200);
expect(removed.body.shareId).toBeNull();
});
test('POST /api/websites/{websiteId} updates the recorder config', async ({ admin, api }) => {
const enabled = await admin.post(`/api/websites/${websiteId}`, {
replayConfig: { replayEnabled: true, sampleRate: 0.5, maskLevel: 'strict' },
});
const recorder = await api.get(`/api/websites/${websiteId}/recorder`);
const disabled = await admin.post(`/api/websites/${websiteId}`, { replayConfig: null });
const recorderOff = await api.get(`/api/websites/${websiteId}/recorder`);
expect(enabled.status).toBe(200);
expect(recorder.status).toBe(200);
expect(recorder.body).toMatchObject({
enabled: true,
replayEnabled: true,
heatmapEnabled: false,
sampleRate: 0.5,
maskLevel: 'strict',
});
expect(disabled.status).toBe(200);
expect(recorderOff.body).toEqual({ enabled: false });
});
test('GET /api/websites/{websiteId}/recorder is public and CORS enabled', async ({ api }) => {
const response = await api.get(`/api/websites/${UNKNOWN_UUID}/recorder`);
expect(response.status).toBe(200);
expect(response.body).toEqual({ enabled: false });
expect(response.headers['access-control-allow-origin']).toBe('*');
});
test('GET /api/websites/charts returns chart data for accessible websites', async ({
admin,
viewer,
seed,
}) => {
const response = await admin.get('/api/websites/charts', {
params: { ids: `${seed.website.id},${seed.website2.id}` },
});
const denied = await viewer.get('/api/websites/charts', { params: { ids: seed.website.id } });
const invalid = await admin.get('/api/websites/charts', { params: { ids: 'nope' } });
expect(response.status).toBe(200);
// `data` is keyed by website id; ids the caller cannot view are silently dropped.
expect(Object.keys(response.body.data).sort()).toEqual(
[seed.website.id, seed.website2.id].sort(),
);
expect(denied.status).toBe(200);
expect(denied.body.data).toEqual({});
expect(invalid.status).toBe(400);
});
test('GET /api/websites/{websiteId}/daterange returns the data window', async ({
admin,
share,
viewer,
seed,
}) => {
const response = await admin.get(`/api/websites/${seed.website.id}/daterange`);
const shared = await (await share()).get(`/api/websites/${seed.website.id}/daterange`);
const denied = await viewer.get(`/api/websites/${seed.website.id}/daterange`);
expect(response.status).toBe(200);
expect(new Date(response.body.startDate).getTime()).toBeGreaterThanOrEqual(seed.range.startAt);
expect(new Date(response.body.endDate).getTime()).toBeGreaterThanOrEqual(
new Date(response.body.startDate).getTime(),
);
expect(shared.status).toBe(200);
expect(denied.status).toBe(401);
});
test('GET /api/websites/{websiteId}/export returns a zip archive', async ({
admin,
share,
seed,
}) => {
const response = await admin.get(`/api/websites/${seed.website.id}/export`, {
params: dateRange(seed),
});
const shared = await (await share()).get(`/api/websites/${seed.website.id}/export`, {
params: dateRange(seed),
});
const missingRange = await admin.get(`/api/websites/${seed.website.id}/export`);
expect(response.status).toBe(200);
// base64 of the "PK\x03\x04" zip signature
expect(response.body.zip).toMatch(/^UEsDB/);
expect(shared.status).toBe(401);
expect(missingRange.status).toBe(400);
});
test('POST /api/websites/{websiteId}/reset clears the website data', async ({
admin,
api,
user,
}) => {
await api.post('/api/send', {
type: 'event',
payload: { website: websiteId, hostname: HOSTNAME, url: '/reset', ip: '10.0.9.3' },
});
const denied = await user.post(`/api/websites/${websiteId}/reset`);
const response = await admin.post(`/api/websites/${websiteId}/reset`);
const website = await admin.get(`/api/websites/${websiteId}`);
expect(denied.status).toBe(401);
expect(response.status).toBe(200);
expect(response.body).toEqual({ ok: true });
expect(website.body.resetAt).toBeTruthy();
});
test('POST /api/websites/{websiteId}/transfer moves a website between owners', async ({
admin,
user,
seed,
}) => {
const toTeam = await admin.post(`/api/websites/${websiteId}/transfer`, { teamId });
const toUser = await admin.post(`/api/websites/${websiteId}/transfer`, {
userId: seed.admin.id,
});
const empty = await admin.post(`/api/websites/${websiteId}/transfer`, {});
const denied = await user.post(`/api/websites/${websiteId}/transfer`, { userId: seed.user.id });
expect(toTeam.status).toBe(200);
expect(toTeam.body).toMatchObject({ teamId, userId: null });
expect(toUser.status).toBe(200);
expect(toUser.body).toMatchObject({ teamId: null, userId: seed.admin.id });
expect(empty.status).toBe(400);
expect(denied.status).toBe(401);
});
test('DELETE /api/websites/{websiteId} deletes a website', async ({ admin, user }) => {
const denied = await user.del(`/api/websites/${teamWebsiteId}`);
const response = await admin.del(`/api/websites/${websiteId}`);
const gone = await admin.get(`/api/websites/${websiteId}`);
expect(denied.status).toBe(401);
expect(response.status).toBe(200);
expect(response.body).toEqual({ ok: true });
expect(gone.body).toBeNull();
websiteId = '';
});
});