import { describe, it, expect } from 'vitest'; import { createApp } from '../src/app'; import { authToken, bearer, createTestUser, seedActivity } from './helpers'; import { db } from '../src/db/client'; import { user } from '../src/db/schema'; import { eq } from 'drizzle-orm'; async function userIdByEmail(email: string): Promise { const [row] = await db.select({ id: user.id }).from(user).where(eq(user.email, email)); if (!row) throw new Error(`no user for ${email}`); return row.id; } describe('admin session views', () => { it('401s without a token', async () => { const app = createApp(); expect((await app.request('/api/admin/sessions')).status).toBe(401); expect((await app.request('/api/admin/sessions/active')).status).toBe(401); }); it('403s for a worker', async () => { const app = createApp(); const token = await authToken(app, 'admin-view-worker@example.com'); // worker expect((await app.request('/api/admin/sessions', { headers: bearer(token) })).status).toBe(403); }); it("returns ALL users' sessions for an admin, with user info", async () => { const app = createApp(); const adminTok = await authToken(app, 'admin-view-admin@example.com', 'admin'); const workerTok = await authToken(app, 'admin-view-w2@example.com'); // worker const activityId = await seedActivity('Frezen'); // Worker starts a session. const started = await ( await app.request('/api/sessions/start', { method: 'POST', headers: bearer(workerTok), body: JSON.stringify({ activity_id: activityId, insole_type: 'Kurk', pair_count: 2 }), }) ).json(); const res = await app.request('/api/admin/sessions', { headers: bearer(adminTok) }); expect(res.status).toBe(200); const body = await res.json(); const found = body.find((s: { id: number }) => s.id === started.id); expect(found).toBeTruthy(); expect(found.user_email).toBe('admin-view-w2@example.com'); expect(found.activity_name).toBe('Frezen'); const active = await app.request('/api/admin/sessions/active', { headers: bearer(adminTok) }); expect(active.status).toBe(200); const activeBody = await active.json(); expect(activeBody.some((s: { id: number }) => s.id === started.id)).toBe(true); }); }); describe('admin users roster', () => { it('returns id/name/email objects for an admin, ordered by name', async () => { const app = createApp(); const adminTok = await authToken(app, 'roster-admin@example.com', 'admin'); await createTestUser('roster-w1@example.com'); const res = await app.request('/api/admin/users', { headers: bearer(adminTok) }); expect(res.status).toBe(200); const body = await res.json(); expect(Array.isArray(body)).toBe(true); const found = body.find((u: { email: string }) => u.email === 'roster-w1@example.com'); expect(found).toBeTruthy(); expect(found).toHaveProperty('id'); expect(found).toHaveProperty('name'); expect(found).toHaveProperty('email'); // ordered by name ascending const names = body.map((u: { name: string }) => u.name); const sorted = [...names].sort(); expect(names).toEqual(sorted); }); it('403s for a worker', async () => { const app = createApp(); const workerTok = await authToken(app, 'roster-worker@example.com'); // worker expect((await app.request('/api/admin/users', { headers: bearer(workerTok) })).status).toBe( 403, ); }); }); describe('admin manual session create', () => { it('creates a completed manual session with duration excluding paused', async () => { const app = createApp(); const adminTok = await authToken(app, 'create-admin@example.com', 'admin'); await createTestUser('create-target@example.com'); const targetId = await userIdByEmail('create-target@example.com'); const activityId = await seedActivity('Snijden'); const start = new Date('2026-06-17T08:00:00.000Z'); const end = new Date('2026-06-17T09:00:00.000Z'); // 1h apart = 3600s const res = await app.request('/api/admin/sessions', { method: 'POST', headers: bearer(adminTok), body: JSON.stringify({ user_id: targetId, activity_id: activityId, insole_type: 'Kurk', pair_count: 3, start_time: start.toISOString(), end_time: end.toISOString(), paused_seconds: 600, notes: 'handmatig', }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.source).toBe('manual'); expect(body.status).toBe('completed'); expect(body.duration_seconds).toBe(3000); // 3600 - 600 expect(body.paused_seconds).toBe(600); expect(body.paused_at).toBeNull(); expect(body.user_id).toBe(targetId); expect(body.activity_name).toBe('Snijden'); }); it('400s when end < start', async () => { const app = createApp(); const adminTok = await authToken(app, 'create-badtime-admin@example.com', 'admin'); await createTestUser('create-badtime-target@example.com'); const targetId = await userIdByEmail('create-badtime-target@example.com'); const activityId = await seedActivity('Frezen'); const res = await app.request('/api/admin/sessions', { method: 'POST', headers: bearer(adminTok), body: JSON.stringify({ user_id: targetId, activity_id: activityId, insole_type: 'Kurk', pair_count: 1, start_time: '2026-06-17T09:00:00.000Z', end_time: '2026-06-17T08:00:00.000Z', }), }); expect(res.status).toBe(400); }); it('404s for an unknown user', async () => { const app = createApp(); const adminTok = await authToken(app, 'create-nouser-admin@example.com', 'admin'); const activityId = await seedActivity('Lijmen'); const res = await app.request('/api/admin/sessions', { method: 'POST', headers: bearer(adminTok), body: JSON.stringify({ user_id: 'does-not-exist', activity_id: activityId, insole_type: 'Kurk', pair_count: 1, start_time: '2026-06-17T08:00:00.000Z', end_time: '2026-06-17T09:00:00.000Z', }), }); expect(res.status).toBe(404); }); it('404s for an unknown activity', async () => { const app = createApp(); const adminTok = await authToken(app, 'create-noact-admin@example.com', 'admin'); await createTestUser('create-noact-target@example.com'); const targetId = await userIdByEmail('create-noact-target@example.com'); const res = await app.request('/api/admin/sessions', { method: 'POST', headers: bearer(adminTok), body: JSON.stringify({ user_id: targetId, activity_id: 999999, insole_type: 'Kurk', pair_count: 1, start_time: '2026-06-17T08:00:00.000Z', end_time: '2026-06-17T09:00:00.000Z', }), }); expect(res.status).toBe(404); }); it('403s for a worker', async () => { const app = createApp(); const workerTok = await authToken(app, 'create-worker@example.com'); // worker const res = await app.request('/api/admin/sessions', { method: 'POST', headers: bearer(workerTok), body: JSON.stringify({ user_id: 'x', activity_id: 1, insole_type: 'Kurk', pair_count: 1, start_time: '2026-06-17T08:00:00.000Z', end_time: '2026-06-17T09:00:00.000Z', }), }); expect(res.status).toBe(403); }); }); describe('admin session edit', () => { it('recomputes duration on edit', async () => { const app = createApp(); const adminTok = await authToken(app, 'edit-admin@example.com', 'admin'); await createTestUser('edit-target@example.com'); const targetId = await userIdByEmail('edit-target@example.com'); const activityId = await seedActivity('Polijsten'); const created = await ( await app.request('/api/admin/sessions', { method: 'POST', headers: bearer(adminTok), body: JSON.stringify({ user_id: targetId, activity_id: activityId, insole_type: 'Kurk', pair_count: 2, start_time: '2026-06-17T08:00:00.000Z', end_time: '2026-06-17T09:00:00.000Z', paused_seconds: 0, }), }) ).json(); expect(created.duration_seconds).toBe(3600); const res = await app.request(`/api/admin/sessions/${created.id}`, { method: 'PUT', headers: bearer(adminTok), body: JSON.stringify({ activity_id: activityId, insole_type: 'Berk', pair_count: 4, start_time: '2026-06-17T08:00:00.000Z', end_time: '2026-06-17T10:00:00.000Z', // now 2h paused_seconds: 300, notes: 'gecorrigeerd', status: 'completed', }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.duration_seconds).toBe(7200 - 300); expect(body.insole_type).toBe('Berk'); expect(body.pair_count).toBe(4); expect(body.notes).toBe('gecorrigeerd'); }); it('sets duration null when end_time is null (active)', async () => { const app = createApp(); const adminTok = await authToken(app, 'edit-active-admin@example.com', 'admin'); await createTestUser('edit-active-target@example.com'); const targetId = await userIdByEmail('edit-active-target@example.com'); const activityId = await seedActivity('Stikken'); const created = await ( await app.request('/api/admin/sessions', { method: 'POST', headers: bearer(adminTok), body: JSON.stringify({ user_id: targetId, activity_id: activityId, insole_type: 'Kurk', pair_count: 2, start_time: '2026-06-17T08:00:00.000Z', end_time: '2026-06-17T09:00:00.000Z', }), }) ).json(); const res = await app.request(`/api/admin/sessions/${created.id}`, { method: 'PUT', headers: bearer(adminTok), body: JSON.stringify({ activity_id: activityId, insole_type: 'Kurk', pair_count: 2, start_time: '2026-06-17T08:00:00.000Z', end_time: null, paused_seconds: 0, notes: null, status: 'active', }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.duration_seconds).toBeNull(); expect(body.end_time).toBeNull(); expect(body.status).toBe('active'); }); it('400s when end < start on edit', async () => { const app = createApp(); const adminTok = await authToken(app, 'edit-badtime-admin@example.com', 'admin'); await createTestUser('edit-badtime-target@example.com'); const targetId = await userIdByEmail('edit-badtime-target@example.com'); const activityId = await seedActivity('Wassen'); const created = await ( await app.request('/api/admin/sessions', { method: 'POST', headers: bearer(adminTok), body: JSON.stringify({ user_id: targetId, activity_id: activityId, insole_type: 'Kurk', pair_count: 2, start_time: '2026-06-17T08:00:00.000Z', end_time: '2026-06-17T09:00:00.000Z', }), }) ).json(); const res = await app.request(`/api/admin/sessions/${created.id}`, { method: 'PUT', headers: bearer(adminTok), body: JSON.stringify({ activity_id: activityId, insole_type: 'Kurk', pair_count: 2, start_time: '2026-06-17T09:00:00.000Z', end_time: '2026-06-17T08:00:00.000Z', paused_seconds: 0, notes: null, status: 'completed', }), }); expect(res.status).toBe(400); }); it('404s for an unknown session', async () => { const app = createApp(); const adminTok = await authToken(app, 'edit-404-admin@example.com', 'admin'); const activityId = await seedActivity('Drogen'); const res = await app.request('/api/admin/sessions/999999', { method: 'PUT', headers: bearer(adminTok), body: JSON.stringify({ activity_id: activityId, insole_type: 'Kurk', pair_count: 2, start_time: '2026-06-17T08:00:00.000Z', end_time: '2026-06-17T09:00:00.000Z', paused_seconds: 0, notes: null, status: 'completed', }), }); expect(res.status).toBe(404); }); }); describe('admin stop/discard another user session', () => { it("stops another worker's active session and computes duration", async () => { const app = createApp(); const adminTok = await authToken(app, 'stop-admin@example.com', 'admin'); const workerTok = await authToken(app, 'stop-worker@example.com'); // worker const activityId = await seedActivity('Frezen'); const started = await ( await app.request('/api/sessions/start', { method: 'POST', headers: bearer(workerTok), body: JSON.stringify({ activity_id: activityId, insole_type: 'Kurk', pair_count: 2 }), }) ).json(); const res = await app.request(`/api/admin/sessions/${started.id}/stop`, { method: 'POST', headers: bearer(adminTok), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.status).toBe('completed'); expect(body.end_time).not.toBeNull(); expect(body.duration_seconds).toBeGreaterThanOrEqual(0); expect(body.paused_at).toBeNull(); }); it("discards another worker's active session", async () => { const app = createApp(); const adminTok = await authToken(app, 'discard-admin@example.com', 'admin'); const workerTok = await authToken(app, 'discard-worker@example.com'); // worker const activityId = await seedActivity('Lijmen'); const started = await ( await app.request('/api/sessions/start', { method: 'POST', headers: bearer(workerTok), body: JSON.stringify({ activity_id: activityId, insole_type: 'Kurk', pair_count: 2 }), }) ).json(); const res = await app.request(`/api/admin/sessions/${started.id}/discard`, { method: 'POST', headers: bearer(adminTok), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.status).toBe('discarded'); expect(body.end_time).not.toBeNull(); }); it('409s when stopping an already-closed session', async () => { const app = createApp(); const adminTok = await authToken(app, 'stop-closed-admin@example.com', 'admin'); const workerTok = await authToken(app, 'stop-closed-worker@example.com'); const activityId = await seedActivity('Snijden'); const started = await ( await app.request('/api/sessions/start', { method: 'POST', headers: bearer(workerTok), body: JSON.stringify({ activity_id: activityId, insole_type: 'Kurk', pair_count: 2 }), }) ).json(); await app.request(`/api/admin/sessions/${started.id}/discard`, { method: 'POST', headers: bearer(adminTok), }); const res = await app.request(`/api/admin/sessions/${started.id}/stop`, { method: 'POST', headers: bearer(adminTok), }); expect(res.status).toBe(409); }); it('403s for a worker hitting stop/discard', async () => { const app = createApp(); const workerTok = await authToken(app, 'stop-worker-gate@example.com'); // worker expect( ( await app.request('/api/admin/sessions/1/stop', { method: 'POST', headers: bearer(workerTok), }) ).status, ).toBe(403); expect( ( await app.request('/api/admin/sessions/1/discard', { method: 'POST', headers: bearer(workerTok), }) ).status, ).toBe(403); }); });