import test from 'node:test'; import assert from 'node:assert/strict'; import Fastify from 'fastify'; import cookie from '@fastify/cookie'; import rateLimit from '@fastify/rate-limit'; import api from '../src/api.js'; function makeApp(user, targetUser) { const calls = []; const pool = { async query(sql, params = []) { calls.push({ sql, params }); if (sql.includes('FROM sessions s JOIN users u')) return { rows: [user] }; if (sql.includes('FROM user_roles')) return { rows: [] }; if (sql.startsWith('UPDATE schools SET name')) return { rows: [{ id: params[1], name: params[0] }] }; if (sql.startsWith('SELECT * FROM classes WHERE id')) return { rows: [{ id: 100, school_id: 2, name: 'Groep 4' }] }; if (sql.startsWith('SELECT * FROM users WHERE id') && targetUser) return { rows: [targetUser] }; return { rows: [] }; }, }; return (async () => { const app = Fastify({ trustProxy: 2 }); await app.register(cookie); await app.register(rateLimit, { global: false }); app.decorate('pg', pool); await app.register(api, { prefix: '/api' }); await app.ready(); return { app, calls }; })(); } const cookies = { teach_session: 'x'.repeat(64) }; const superUser = { id: 1, username: 'sm', role: 'super', school_id: null, class_id: null, data: {}, data_rev: 0 }; const admin = { id: 3, username: 'beheer', role: 'admin', school_id: 2, class_id: null, data: {}, data_rev: 0 }; test('systeemmanager kan een school hernoemen', async () => { const { app, calls } = await makeApp(superUser); const res = await app.inject({ method: 'PATCH', url: '/api/admin/schools/5', cookies, payload: { name: 'De Nieuwe Naam' } }); assert.equal(res.statusCode, 200, res.body); assert.equal(res.json().school.name, 'De Nieuwe Naam'); const upd = calls.find((c) => c.sql.startsWith('UPDATE schools SET name')); assert.deepEqual(upd.params, ['De Nieuwe Naam', '5']); await app.close(); }); test('schoolbeheerder mag geen scholen hernoemen', async () => { const { app } = await makeApp(admin); const res = await app.inject({ method: 'PATCH', url: '/api/admin/schools/5', cookies, payload: { name: 'Hack' } }); assert.equal(res.statusCode, 403); await app.close(); }); test('schoolbeheerder en systeemmanager kunnen aan een klas gekoppeld worden', async () => { // schoolbeheerder van dezelfde school const { app, calls } = await makeApp(superUser, { id: 3, role: 'admin', school_id: 2 }); const res = await app.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies, payload: { userId: 3 } }); assert.equal(res.statusCode, 200, res.body); assert.ok(calls.some((c) => c.sql.includes('INSERT INTO class_teachers'))); await app.close(); // systeemmanager (geen school) mag ook const { app: app2 } = await makeApp(superUser, { id: 1, role: 'super', school_id: null }); const res2 = await app2.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies, payload: { userId: 1 } }); assert.equal(res2.statusCode, 200, res2.body); await app2.close(); // leerlingen niet, en staf van een andere school niet const { app: app3 } = await makeApp(superUser, { id: 9, role: 'pupil', school_id: 2 }); const res3 = await app3.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies, payload: { userId: 9 } }); assert.equal(res3.statusCode, 400); await app3.close(); const { app: app4 } = await makeApp(superUser, { id: 4, role: 'admin', school_id: 3 }); const res4 = await app4.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies, payload: { userId: 4 } }); assert.equal(res4.statusCode, 400); await app4.close(); }); test('ouder-achtige rol krijgt geen promotie en geen klaskoppeling', async () => { // rolwijziging: alleen staf → staf; een parent-account mag nooit promoveren const { app, calls } = await makeApp(superUser, { id: 7, role: 'parent', school_id: null }); const res = await app.inject({ method: 'PATCH', url: '/api/admin/users/7', cookies, payload: { role: 'admin' } }); assert.equal(res.statusCode, 200, res.body); assert.ok(!calls.some((c) => c.sql.startsWith('UPDATE users SET role')), 'rolwijziging mag voor een ouder-account niet uitgevoerd worden'); await app.close(); // klaskoppeling: parent weigeren, net als pupil const { app: app2, calls: calls2 } = await makeApp(superUser, { id: 7, role: 'parent', school_id: null }); const res2 = await app2.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies, payload: { userId: 7 } }); assert.equal(res2.statusCode, 400); assert.ok(!calls2.some((c) => c.sql.includes('INSERT INTO class_teachers'))); await app2.close(); }); test('hernoemen weigert een lege of te lange naam', async () => { const { app } = await makeApp(superUser); const res = await app.inject({ method: 'PATCH', url: '/api/admin/schools/5', cookies, payload: { name: ' ' } }); assert.equal(res.statusCode, 400); const res2 = await app.inject({ method: 'PATCH', url: '/api/admin/schools/5', cookies, payload: { name: 'x'.repeat(90) } }); assert.equal(res2.statusCode, 400); await app.close(); });