Files
umami/tests/api/pixels.spec.ts
T

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 = '';
});
});