218 lines
8.1 KiB
TypeScript
218 lines
8.1 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
import { expect, test } from './fixtures';
|
|
import { ENTITY_TYPE, UNKNOWN_UUID } from './helpers/constants';
|
|
import { dateRange } from './helpers/dates';
|
|
import { deletePixel, uniqueName, uniqueSlug } from './helpers/entities';
|
|
|
|
test.describe('Pixels', () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
let pixelId = '';
|
|
let slug = '';
|
|
let shareId = '';
|
|
|
|
test.afterAll(async ({ admin }) => {
|
|
if (shareId) {
|
|
await admin.del(`/api/share/id/${shareId}`);
|
|
}
|
|
|
|
if (pixelId) {
|
|
await deletePixel(admin, pixelId);
|
|
}
|
|
});
|
|
|
|
test('POST /api/pixels creates a pixel for the current user', async ({ admin, seed }) => {
|
|
const name = uniqueName('pixel');
|
|
slug = uniqueSlug('pixel');
|
|
const response = await admin.post('/api/pixels', { name, slug });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toMatchObject({
|
|
id: expect.any(String),
|
|
name,
|
|
slug,
|
|
userId: seed.admin.id,
|
|
teamId: null,
|
|
createdAt: expect.any(String),
|
|
deletedAt: null,
|
|
});
|
|
|
|
pixelId = response.body.id;
|
|
});
|
|
|
|
test('POST /api/pixels accepts a fixed id and a team', async ({ admin, seed }) => {
|
|
const id = randomUUID();
|
|
const response = await admin.post('/api/pixels', {
|
|
id,
|
|
name: uniqueName('team-pixel'),
|
|
slug: uniqueSlug('team'),
|
|
teamId: seed.team.id,
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toMatchObject({ id, teamId: seed.team.id, userId: null });
|
|
|
|
await deletePixel(admin, id);
|
|
});
|
|
|
|
test('POST /api/pixels validates the body and requires permission', async ({
|
|
admin,
|
|
viewer,
|
|
api,
|
|
}) => {
|
|
const shortSlug = await admin.post('/api/pixels', { name: 'x', slug: 'short' });
|
|
const missingName = await admin.post('/api/pixels', { slug: uniqueSlug('pixel') });
|
|
const denied = await viewer.post('/api/pixels', { name: 'x', slug: uniqueSlug('pixel') });
|
|
const anonymous = await api.post('/api/pixels', { name: 'x', slug: uniqueSlug('pixel') });
|
|
|
|
expect(shortSlug.status).toBe(400);
|
|
expect(shortSlug.body.error.code).toBe('bad-request');
|
|
expect(missingName.status).toBe(400);
|
|
expect(denied.status).toBe(401);
|
|
expect(anonymous.status).toBe(401);
|
|
});
|
|
|
|
test('GET /api/pixels lists the current user pixels', async ({ admin, user, viewer }) => {
|
|
const response = await admin.get('/api/pixels');
|
|
const search = await admin.get('/api/pixels', { params: { search: slug } });
|
|
const sorted = await admin.get('/api/pixels', {
|
|
params: { orderBy: 'createdAt', sortDescending: 'true', pageSize: 1 },
|
|
});
|
|
const invalidSort = await admin.get('/api/pixels', { params: { sortDescending: 'yes' } });
|
|
const other = await user.get('/api/pixels');
|
|
const none = await viewer.get('/api/pixels');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toMatchObject({
|
|
data: expect.any(Array),
|
|
count: expect.any(Number),
|
|
page: 1,
|
|
pageSize: expect.any(Number),
|
|
});
|
|
expect(response.body.data.map((p: any) => p.id)).toContain(pixelId);
|
|
expect(search.body.data.map((p: any) => p.id)).toEqual([pixelId]);
|
|
expect(sorted.status).toBe(200);
|
|
expect(sorted.body.data).toHaveLength(1);
|
|
expect(invalidSort.status).toBe(400);
|
|
expect(other.body.data.map((p: any) => p.id)).not.toContain(pixelId);
|
|
expect(none.status).toBe(200);
|
|
expect(none.body.data).toHaveLength(0);
|
|
});
|
|
|
|
test('GET /api/pixels/{pixelId} returns a pixel', async ({ admin, user, viewer, api }) => {
|
|
const response = await admin.get(`/api/pixels/${pixelId}`);
|
|
const denied = await viewer.get(`/api/pixels/${pixelId}`);
|
|
const anonymous = await api.get(`/api/pixels/${pixelId}`);
|
|
const unknown = await user.get(`/api/pixels/${UNKNOWN_UUID}`);
|
|
// Admins bypass the ownership check, so an unknown id yields an empty 200.
|
|
const adminUnknown = await admin.get(`/api/pixels/${UNKNOWN_UUID}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toMatchObject({ id: pixelId, slug });
|
|
expect(denied.status).toBe(401);
|
|
expect(anonymous.status).toBe(401);
|
|
expect(unknown.status).toBe(401);
|
|
expect(adminUnknown.status).toBe(200);
|
|
expect(adminUnknown.body).toBeNull();
|
|
});
|
|
|
|
test('POST /api/pixels/{pixelId} updates a pixel', async ({ admin, user, seed }) => {
|
|
const name = uniqueName('renamed');
|
|
slug = uniqueSlug('renamed');
|
|
const response = await admin.post(`/api/pixels/${pixelId}`, { name, slug });
|
|
const duplicate = await admin.post(`/api/pixels/${pixelId}`, { slug: seed.pixel.slug });
|
|
const denied = await user.post(`/api/pixels/${pixelId}`, { name });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toMatchObject({ id: pixelId, name, slug });
|
|
expect(duplicate.status).toBe(400);
|
|
expect(duplicate.body.error.message).toBe('That slug is already taken.');
|
|
expect(denied.status).toBe(401);
|
|
});
|
|
|
|
test('POST /api/pixels/{pixelId}/shares creates a pixel share', async ({ admin, user }) => {
|
|
const name = uniqueName('pixel-share');
|
|
const response = await admin.post(`/api/pixels/${pixelId}/shares`, {
|
|
name,
|
|
parameters: { overview: true },
|
|
});
|
|
const invalid = await admin.post(`/api/pixels/${pixelId}/shares`, {});
|
|
const denied = await user.post(`/api/pixels/${pixelId}/shares`, { name });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toMatchObject({
|
|
entityId: pixelId,
|
|
shareType: ENTITY_TYPE.pixel,
|
|
name,
|
|
slug: expect.any(String),
|
|
parameters: { overview: true },
|
|
});
|
|
expect(invalid.status).toBe(400);
|
|
expect(denied.status).toBe(401);
|
|
|
|
shareId = response.body.id;
|
|
});
|
|
|
|
test('GET /api/pixels/{pixelId}/shares lists pixel shares', async ({ admin, viewer, api }) => {
|
|
const response = await admin.get(`/api/pixels/${pixelId}/shares`);
|
|
const paged = await admin.get(`/api/pixels/${pixelId}/shares`, {
|
|
params: { page: 1, pageSize: 1 },
|
|
});
|
|
const denied = await viewer.get(`/api/pixels/${pixelId}/shares`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toMatchObject({ data: expect.any(Array), count: 1, page: 1 });
|
|
expect(response.body.data[0]).toMatchObject({ id: shareId, entityId: pixelId });
|
|
expect(paged.body.pageSize).toBe(1);
|
|
expect(denied.status).toBe(401);
|
|
|
|
// The share resolves publicly to a pixel-typed token.
|
|
const resolved = await api.get(`/api/share/${response.body.data[0].slug}`);
|
|
|
|
expect(resolved.status).toBe(200);
|
|
expect(resolved.body).toMatchObject({
|
|
shareType: ENTITY_TYPE.pixel,
|
|
pixelId,
|
|
websiteId: pixelId,
|
|
});
|
|
});
|
|
|
|
test('GET /api/pixels/charts returns chart data for accessible pixels', async ({
|
|
admin,
|
|
viewer,
|
|
seed,
|
|
}) => {
|
|
const response = await admin.get('/api/pixels/charts', {
|
|
params: { ids: `${seed.pixel.id},${pixelId}`, ...dateRange(seed), timezone: 'UTC' },
|
|
});
|
|
const defaultRange = await admin.get('/api/pixels/charts', { params: { ids: seed.pixel.id } });
|
|
const denied = await viewer.get('/api/pixels/charts', { params: { ids: seed.pixel.id } });
|
|
const invalid = await admin.get('/api/pixels/charts', { params: { ids: 'nope' } });
|
|
const missing = await admin.get('/api/pixels/charts');
|
|
|
|
expect(response.status).toBe(200);
|
|
// `data` is keyed by pixel id; ids the caller cannot view are silently dropped.
|
|
expect(Object.keys(response.body.data)).toContain(seed.pixel.id);
|
|
expect(response.body.data[seed.pixel.id]).toBeTruthy();
|
|
expect(defaultRange.status).toBe(200);
|
|
expect(denied.status).toBe(200);
|
|
expect(denied.body.data).toEqual({});
|
|
expect(invalid.status).toBe(400);
|
|
expect(missing.status).toBe(400);
|
|
});
|
|
|
|
test('DELETE /api/pixels/{pixelId} deletes a pixel', async ({ admin, user }) => {
|
|
const denied = await user.del(`/api/pixels/${pixelId}`);
|
|
const response = await admin.del(`/api/pixels/${pixelId}`);
|
|
const gone = await admin.get(`/api/pixels/${pixelId}`);
|
|
|
|
expect(denied.status).toBe(401);
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({ ok: true });
|
|
expect(gone.status).toBe(200);
|
|
expect(gone.body).toBeNull();
|
|
|
|
pixelId = '';
|
|
});
|
|
});
|