Files
solelog/apps/api/test/activities.test.ts
Bas van Rossem 70ac27ec8e
All checks were successful
Build and Push Docker Image / build (push) Successful in 28s
style: align oxfmt to trailing-comma 'all' and normalize code
The repo was authored prettier-style (trailing-comma 'all') but .oxfmtrc.json
was set to 'es5', so every formatted file diverged. Switch the config to 'all'
to match the existing code, ignore docs/** and **/drizzle/** (prose + generated
snapshots the formatter should not own), and reformat the source tree once for
consistency. No behavioural change; all suites green (api 60, worker 28, admin 21).
2026-06-17 21:36:18 +02:00

309 lines
11 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { createApp } from '../src/app';
import { db } from '../src/db/client';
import { workSessions, user } from '../src/db/schema';
import { eq } from 'drizzle-orm';
import { authToken, bearer } from './helpers';
const json = { 'content-type': 'application/json' };
describe('activities routes', () => {
it('401s GET /api/activities without a token', async () => {
const app = createApp();
const res = await app.request('/api/activities');
expect(res.status).toBe(401);
});
it('401s POST /api/activities without a token', async () => {
const app = createApp();
const res = await app.request('/api/activities', {
method: 'POST',
headers: json,
body: JSON.stringify({ name: 'Frezen', insole_types: ['Kurk'] }),
});
expect(res.status).toBe(401);
});
it('forbids a worker from creating an activity (403)', async () => {
const app = createApp();
const token = await authToken(app, 'act-worker-create@example.com'); // default worker
const res = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Frezen', insole_types: ['Kurk'] }),
});
expect(res.status).toBe(403);
});
it('lets a worker read activities (200)', async () => {
const app = createApp();
const token = await authToken(app, 'act-worker-read@example.com');
const res = await app.request('/api/activities', { headers: bearer(token) });
expect(res.status).toBe(200);
});
it('creates an activity and lists it', async () => {
const app = createApp();
const token = await authToken(app, 'act-create@example.com', 'admin');
const createRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Frezen', insole_types: ['Kurk', 'Berk'] }),
});
expect(createRes.status).toBe(200);
const created = await createRes.json();
expect(created.name).toBe('Frezen');
expect(created.insole_types).toEqual(['Kurk', 'Berk']);
expect(typeof created.id).toBe('number');
expect(typeof created.created_at).toBe('string');
const listRes = await app.request('/api/activities', { headers: bearer(token) });
expect(listRes.status).toBe(200);
const list = await listRes.json();
expect(Array.isArray(list)).toBe(true);
expect(list.some((a: { id: number }) => a.id === created.id)).toBe(true);
});
it('defaults insole_types to all three when omitted', async () => {
const app = createApp();
const token = await authToken(app, 'act-default@example.com', 'admin');
const res = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Slijpen' }),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.insole_types).toEqual(['Kurk', 'Berk', '3D']);
});
it('filters by ?insole_type', async () => {
const app = createApp();
const token = await authToken(app, 'act-filter@example.com', 'admin');
const printRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Printen-only-3D', insole_types: ['3D'] }),
});
const print = await printRes.json();
const corkRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Kurk-only', insole_types: ['Kurk'] }),
});
const cork = await corkRes.json();
const res = await app.request('/api/activities?insole_type=3D', { headers: bearer(token) });
expect(res.status).toBe(200);
const list: Array<{ id: number }> = await res.json();
expect(list.some((a) => a.id === print.id)).toBe(true);
expect(list.some((a) => a.id === cork.id)).toBe(false);
});
it('400s POST with an empty name', async () => {
const app = createApp();
const token = await authToken(app, 'act-emptyname@example.com', 'admin');
const res = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: ' ', insole_types: ['Kurk'] }),
});
expect(res.status).toBe(400);
});
it('updates an activity', async () => {
const app = createApp();
const token = await authToken(app, 'act-update@example.com', 'admin');
const createRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Oud', insole_types: ['Kurk'] }),
});
const created = await createRes.json();
const res = await app.request(`/api/activities/${created.id}`, {
method: 'PUT',
headers: bearer(token),
body: JSON.stringify({ name: 'Nieuw', insole_types: ['Berk', '3D'] }),
});
expect(res.status).toBe(200);
const updated = await res.json();
expect(updated.id).toBe(created.id);
expect(updated.name).toBe('Nieuw');
expect(updated.insole_types).toEqual(['Berk', '3D']);
});
it('404s PUT for a missing id', async () => {
const app = createApp();
const token = await authToken(app, 'act-put404@example.com', 'admin');
const res = await app.request('/api/activities/999999', {
method: 'PUT',
headers: bearer(token),
body: JSON.stringify({ name: 'X', insole_types: ['Kurk'] }),
});
expect(res.status).toBe(404);
});
it('orders GET by sort_order then name', async () => {
const app = createApp();
const token = await authToken(app, 'act-order@example.com', 'admin');
// Create three activities; they append with increasing sort_order.
const names = ['Order-C', 'Order-A', 'Order-B'];
const created: Array<{ id: number; name: string }> = [];
for (const name of names) {
const res = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name, insole_types: ['Kurk'] }),
});
created.push(await res.json());
}
const listRes = await app.request('/api/activities', { headers: bearer(token) });
const list: Array<{ id: number; sort_order: number }> = await listRes.json();
// Filtered to just our three, in returned order.
const ours = list.filter((a) => created.some((c) => c.id === a.id));
expect(ours.map((a) => a.id)).toEqual(created.map((c) => c.id));
// sort_order is non-decreasing across the whole list.
for (let i = 1; i < list.length; i++) {
expect(list[i].sort_order).toBeGreaterThanOrEqual(list[i - 1].sort_order);
}
});
it('appends a new activity with a higher sort_order than existing ones', async () => {
const app = createApp();
const token = await authToken(app, 'act-append@example.com', 'admin');
const firstRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Append-1', insole_types: ['Kurk'] }),
});
const first = await firstRes.json();
const secondRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Append-2', insole_types: ['Kurk'] }),
});
const second = await secondRes.json();
expect(second.sort_order).toBeGreaterThan(first.sort_order);
});
it('lets an admin reorder activities (PUT /api/activities/reorder)', async () => {
const app = createApp();
const token = await authToken(app, 'act-reorder@example.com', 'admin');
// Existing activities (possibly seeded by other tests) plus our two.
const aRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Reorder-A', insole_types: ['Kurk'] }),
});
const a = await aRes.json();
const bRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Reorder-B', insole_types: ['Kurk'] }),
});
const b = await bRes.json();
// Build the full ordered id list, then swap a and b so b comes first.
const beforeRes = await app.request('/api/activities', { headers: bearer(token) });
const before: Array<{ id: number }> = await beforeRes.json();
const ids = before.map((r) => r.id);
const ia = ids.indexOf(a.id);
const ib = ids.indexOf(b.id);
[ids[ia], ids[ib]] = [ids[ib], ids[ia]];
const reorderRes = await app.request('/api/activities/reorder', {
method: 'PUT',
headers: bearer(token),
body: JSON.stringify({ ids }),
});
expect(reorderRes.status).toBe(200);
const afterRes = await app.request('/api/activities', { headers: bearer(token) });
const after: Array<{ id: number }> = await afterRes.json();
expect(after.map((r) => r.id)).toEqual(ids);
// In particular, b now precedes a.
expect(after.findIndex((r) => r.id === b.id)).toBeLessThan(
after.findIndex((r) => r.id === a.id),
);
});
it('forbids a worker from reordering activities (403)', async () => {
const app = createApp();
const token = await authToken(app, 'act-reorder-worker@example.com');
const res = await app.request('/api/activities/reorder', {
method: 'PUT',
headers: bearer(token),
body: JSON.stringify({ ids: [1] }),
});
expect(res.status).toBe(403);
});
it('400s reorder when ids do not match the full set', async () => {
const app = createApp();
const token = await authToken(app, 'act-reorder-badids@example.com', 'admin');
const res = await app.request('/api/activities/reorder', {
method: 'PUT',
headers: bearer(token),
body: JSON.stringify({ ids: [999999] }),
});
expect(res.status).toBe(400);
});
it('deletes an activity and its sessions', async () => {
const app = createApp();
const token = await authToken(app, 'act-delete@example.com', 'admin');
const createRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'TeVerwijderen', insole_types: ['Kurk'] }),
});
const created = await createRes.json();
// Find the test user's id to attach a work_sessions row directly.
const [u] = await db.select().from(user).where(eq(user.email, 'act-delete@example.com'));
expect(u).toBeTruthy();
await db.insert(workSessions).values({
userId: u.id,
activityId: created.id,
startTime: new Date(),
});
const before = await db
.select()
.from(workSessions)
.where(eq(workSessions.activityId, created.id));
expect(before.length).toBe(1);
const res = await app.request(`/api/activities/${created.id}`, {
method: 'DELETE',
headers: bearer(token),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toEqual({ success: true });
const after = await db
.select()
.from(workSessions)
.where(eq(workSessions.activityId, created.id));
expect(after.length).toBe(0);
});
});