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