294 lines
10 KiB
TypeScript
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 = '';
|
|
});
|
|
});
|