test(api): Activities — fill gaps + lifecycle test

This commit is contained in:
Bas van Rossem
2026-07-22 12:01:03 +02:00
parent e6e4ca0a1e
commit af7251398c
4 changed files with 233 additions and 2 deletions

View File

@@ -1,13 +1,15 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect, beforeEach } 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';
import { authToken, bearer, resetDb } from './helpers';
const json = { 'content-type': 'application/json' };
describe('activities routes', () => {
beforeEach(resetDb);
it('401s GET /api/activities without a token', async () => {
const app = createApp();
const res = await app.request('/api/activities');
@@ -305,4 +307,144 @@ describe('activities routes', () => {
.where(eq(workSessions.activityId, created.id));
expect(after.length).toBe(0);
});
// --- write-route authz gating (401 no token / 403 worker) ---
it('401s PUT /api/activities/:id without a token', async () => {
const app = createApp();
const res = await app.request('/api/activities/1', {
method: 'PUT',
headers: json,
body: JSON.stringify({ name: 'X', insole_types: ['Kurk'] }),
});
expect(res.status).toBe(401);
});
it('forbids a worker from updating an activity (403)', async () => {
const app = createApp();
const token = await authToken(app, 'act-put-worker@example.com');
const res = await app.request('/api/activities/1', {
method: 'PUT',
headers: bearer(token),
body: JSON.stringify({ name: 'X', insole_types: ['Kurk'] }),
});
expect(res.status).toBe(403);
});
it('401s DELETE /api/activities/:id without a token', async () => {
const app = createApp();
const res = await app.request('/api/activities/1', { method: 'DELETE' });
expect(res.status).toBe(401);
});
it('forbids a worker from deleting an activity (403)', async () => {
const app = createApp();
const token = await authToken(app, 'act-delete-worker@example.com');
const res = await app.request('/api/activities/1', {
method: 'DELETE',
headers: bearer(token),
});
expect(res.status).toBe(403);
});
it('401s PUT /api/activities/reorder without a token', async () => {
const app = createApp();
const res = await app.request('/api/activities/reorder', {
method: 'PUT',
headers: json,
body: JSON.stringify({ ids: [1] }),
});
expect(res.status).toBe(401);
});
// --- id parsing + body validation on the write routes ---
it('404s PUT for a non-numeric id', async () => {
const app = createApp();
const token = await authToken(app, 'act-put-nan@example.com', 'admin');
const res = await app.request('/api/activities/not-a-number', {
method: 'PUT',
headers: bearer(token),
body: JSON.stringify({ name: 'X', insole_types: ['Kurk'] }),
});
expect(res.status).toBe(404);
});
it('400s PUT with an invalid body (empty name)', async () => {
const app = createApp();
const token = await authToken(app, 'act-put-invalid@example.com', 'admin');
const createRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Bewerkbaar', 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: ' ', insole_types: ['Kurk'] }),
});
expect(res.status).toBe(400);
});
it('404s DELETE for a non-numeric id', async () => {
const app = createApp();
const token = await authToken(app, 'act-delete-nan@example.com', 'admin');
const res = await app.request('/api/activities/not-a-number', {
method: 'DELETE',
headers: bearer(token),
});
expect(res.status).toBe(404);
});
it('400s reorder with an empty ids array', async () => {
const app = createApp();
const token = await authToken(app, 'act-reorder-empty@example.com', 'admin');
const res = await app.request('/api/activities/reorder', {
method: 'PUT',
headers: bearer(token),
body: JSON.stringify({ ids: [] }),
});
expect(res.status).toBe(400);
});
it('400s reorder when the ids count matches but contains an unknown id', async () => {
const app = createApp();
const token = await authToken(app, 'act-reorder-unknown@example.com', 'admin');
const createRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Enige', insole_types: ['Kurk'] }),
});
const created = await createRes.json();
// Same length as the full set (1), but the id is not a known activity.
const res = await app.request('/api/activities/reorder', {
method: 'PUT',
headers: bearer(token),
body: JSON.stringify({ ids: [created.id + 999] }),
});
expect(res.status).toBe(400);
});
it('round-trips insole_types as a stored subset', async () => {
const app = createApp();
const token = await authToken(app, 'act-subset@example.com', 'admin');
const createRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Subset', insole_types: ['Berk'] }),
});
const created = await createRes.json();
expect(created.insole_types).toEqual(['Berk']);
const listRes = await app.request('/api/activities', { headers: bearer(token) });
const list: Array<{ id: number; insole_types: string[] }> = await listRes.json();
const found = list.find((a) => a.id === created.id);
expect(found?.insole_types).toEqual(['Berk']);
});
});