teach/test/schools.test.js
Ramon e34bda5251
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 42s
test: expliciete regressietest voor de beperkte rechten van groepsleiding (v0.4.08-beta)
Geen gedragswijziging - dit legt vast (en bevestigt) wat al klopte: een
groepsleiding kan geen rol wijzigen, geen gebruiker aan een school
koppelen/losmaken, geen klas aanmaken en geen school beheren. Alleen
systeemmanager (en voor klassen: ook schoolbeheerder) mag dit.
2026-07-18 04:49:10 +02:00

435 lines
23 KiB
JavaScript

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';
import { hashPassword } from '../src/auth.js';
import { readFile } from 'node:fs/promises';
function makeApp(user, targetUser) {
const calls = [];
const query = async (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: [] };
};
const pool = {
query,
// pool.connect() voor transacties (o.a. gebruiker verwijderen)
async connect() { return { query, release() {} }; },
};
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 };
})();
}
/* variant waarin de scholen-existentiecheck slaagt */
function makeAppWithSchool(user, targetUser) {
return (async () => {
const { app, calls } = await makeApp(user, targetUser);
const pool = app.pg;
const orig = pool.query.bind(pool);
pool.query = async (sql, params = []) => {
if (sql.startsWith('SELECT 1 FROM schools')) { calls.push({ sql, params }); return { rows: [{ 1: 1 }] }; }
return orig(sql, params);
};
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('een bestaande groepsleiding of schoolbeheerder kan via rolwijziging nooit systeemmanager worden', async () => {
// zelfs een systeemmanager mag dit niet via de gewone rolwijziging - alleen
// via POST /admin/users (met de eigen-wachtwoord-bevestiging) mag een
// nieuwe systeemmanager ontstaan
const { app, calls } = await makeApp(superUser, { id: 7, role: 'teacher', school_id: 2 });
const res = await app.inject({ method: 'PATCH', url: '/api/admin/users/7', cookies,
payload: { role: 'super' } });
assert.equal(res.statusCode, 200, res.body);
assert.ok(!calls.some((c) => c.sql.startsWith('UPDATE users SET role')),
'promotie naar systeemmanager via rolwijziging moet genegeerd worden');
await app.close();
const { app: app2, calls: calls2 } = await makeApp(superUser, { id: 8, role: 'admin', school_id: 2 });
const res2 = await app2.inject({ method: 'PATCH', url: '/api/admin/users/8', cookies,
payload: { role: 'super' } });
assert.equal(res2.statusCode, 200, res2.body);
assert.ok(!calls2.some((c) => c.sql.startsWith('UPDATE users SET role')));
await app2.close();
// ook niet via het extra-rollen-mechanisme
const { app: app3, calls: calls3 } = await makeApp(superUser, { id: 8, role: 'admin', school_id: 2 });
const res3 = await app3.inject({ method: 'PATCH', url: '/api/admin/users/8', cookies,
payload: { extraRoles: ['super', 'teacher'] } });
assert.equal(res3.statusCode, 200, res3.body);
assert.deepEqual(res3.json().extraRoles, ['teacher']);
assert.ok(!calls3.some((c) => c.sql.startsWith('INSERT INTO user_roles') && c.params.includes('super')));
await app3.close();
});
test('groepsleiding mag zelf geen rol wijzigen en geen school koppelen/losmaken', async () => {
// rolwijziging: users.role.change is alleen 'super'. Een groepsleiding die
// dit probeert op een niet-leerling-account wordt zelfs al eerder
// geweigerd (teacherOnly-check: groepsleiding beheert uitsluitend
// leerlingen), dus 403 - niet stilzwijgend genegeerd.
const teacherActor = { id: 5, username: 'juf', role: 'teacher', school_id: 2, class_id: null, data: {}, data_rev: 0 };
const { app, calls } = await makeApp(teacherActor, { id: 7, role: 'teacher', school_id: 2 });
const res = await app.inject({ method: 'PATCH', url: '/api/admin/users/7', cookies,
payload: { role: 'admin' } });
assert.equal(res.statusCode, 403, res.body);
assert.ok(!calls.some((c) => c.sql.startsWith('UPDATE users SET role')),
'groepsleiding mag geen rol van een ander account wijzigen');
await app.close();
// schoolId koppelen/losmaken: alleen systeemmanager (req.user.allRoles.includes('super')).
// Op een eigen leerling (waar een groepsleiding verder wél bij mag) blijft
// dit alsnog verboden, dus hier specifiek getest met een leerling-doel.
const { app: app2, calls: calls2 } = await makeApp(teacherActor, { id: 7, role: 'pupil', school_id: 2, class_id: 40 });
const res2 = await app2.inject({ method: 'PATCH', url: '/api/admin/users/7', cookies,
payload: { schoolId: null } });
assert.ok(!calls2.some((c) => c.sql.startsWith('UPDATE users SET school_id')),
'groepsleiding mag een account niet van school koppelen/losmaken');
await app2.close();
// klassen aanmaken/koppelen: classes.manage is alleen super/admin
const { app: app3, calls: calls3 } = await makeApp(teacherActor, null);
const res3 = await app3.inject({ method: 'POST', url: '/api/admin/classes', cookies,
payload: { name: 'Nieuwe klas' } });
assert.equal(res3.statusCode, 403, res3.body);
assert.ok(!calls3.some((c) => c.sql.startsWith('INSERT INTO classes')));
await app3.close();
// scholen beheren: schools.manage is alleen super
const { app: app4 } = await makeApp(teacherActor);
const res4 = await app4.inject({ method: 'PATCH', url: '/api/admin/schools/2', cookies,
payload: { name: 'Andere naam' } });
assert.equal(res4.statusCode, 403, res4.body);
await app4.close();
});
test('systeemmanager koppelt een gebruiker aan een school en maakt hem weer los', async () => {
// koppelen: school bestaat -> update + klas-/toewijzing-opruiming
const { app, calls } = await makeApp(superUser, { id: 7, role: 'teacher', school_id: null, username: 'juf' });
const res = await app.inject({ method: 'PATCH', url: '/api/admin/users/7', cookies,
payload: { schoolId: 2 } });
assert.equal(res.statusCode, 404, res.body); // mock geeft geen schools-rij terug
const { app: app2, calls: calls2 } = await makeAppWithSchool(superUser, { id: 7, role: 'teacher', school_id: null, username: 'juf' });
const res2 = await app2.inject({ method: 'PATCH', url: '/api/admin/users/7', cookies,
payload: { schoolId: 2 } });
assert.equal(res2.statusCode, 200, res2.body);
const upd = calls2.find((c) => c.sql.startsWith('UPDATE users SET school_id'));
assert.deepEqual(upd.params, [2, null, 7]);
assert.ok(calls2.some((c) => c.sql.includes('DELETE FROM class_teachers')));
await app.close(); await app2.close();
// losmaken: schoolId null, geen schools-check nodig; leerling verliest toewijzing
const { app: app3, calls: calls3 } = await makeApp(superUser, { id: 9, role: 'pupil', school_id: 2, class_id: 5, username: 'lena' });
const res3 = await app3.inject({ method: 'PATCH', url: '/api/admin/users/9', cookies,
payload: { schoolId: null } });
assert.equal(res3.statusCode, 200, res3.body);
const upd3 = calls3.find((c) => c.sql.startsWith('UPDATE users SET school_id'));
assert.deepEqual(upd3.params, [null, null, 9]);
assert.ok(calls3.some((c) => c.sql.includes('DELETE FROM assignments WHERE pupil_id')));
await app3.close();
});
test('leerling verplaatsen naar een andere klas (uitwisselen) werkt', async () => {
const { app, calls } = await makeApp(superUser, { id: 9, role: 'pupil', school_id: 2, class_id: 50, username: 'lena' });
const res = await app.inject({ method: 'PATCH', url: '/api/admin/users/9', cookies, payload: { classId: 100 } });
assert.equal(res.statusCode, 200, res.body);
const upd = calls.find((c) => c.sql === 'UPDATE users SET class_id = $1 WHERE id = $2');
assert.deepEqual(upd.params, [100, 9]);
await app.close();
});
test('school koppelen kan niet door een schoolbeheerder en niet op super/parent', async () => {
const { app, calls } = await makeAppWithSchool(admin, { id: 7, role: 'teacher', school_id: 2, username: 'juf' });
await app.inject({ method: 'PATCH', url: '/api/admin/users/7', cookies, payload: { schoolId: 3 } });
assert.ok(!calls.some((c) => c.sql.startsWith('UPDATE users SET school_id')), 'admin mag geen school wijzigen');
await app.close();
const { app: app2, calls: calls2 } = await makeAppWithSchool(superUser, { id: 20, role: 'parent', school_id: null, username: 'ouder' });
await app2.inject({ method: 'PATCH', url: '/api/admin/users/20', cookies, payload: { schoolId: 2 } });
assert.ok(!calls2.some((c) => c.sql.startsWith('UPDATE users SET school_id')), 'parent blijft schoolloos');
await app2.close();
});
test('school koppelen meldt een naamconflict netjes als 409', async () => {
const calls = [];
const pool = {
async query(sql, params = []) {
calls.push({ sql, params });
if (sql.includes('FROM sessions s JOIN users u')) return { rows: [superUser] };
if (sql.includes('FROM user_roles')) return { rows: [] };
if (sql.startsWith('SELECT * FROM users WHERE id')) return { rows: [{ id: 7, role: 'teacher', school_id: null, username: 'juf' }] };
if (sql.startsWith('SELECT 1 FROM schools')) return { rows: [{ 1: 1 }] };
if (sql.startsWith('UPDATE users SET school_id')) { const e = new Error('dup'); e.code = '23505'; throw e; }
return { rows: [] };
},
};
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();
const res = await app.inject({ method: 'PATCH', url: '/api/admin/users/7', cookies, payload: { schoolId: 2 } });
assert.equal(res.statusCode, 409);
assert.match(res.json().error, /bestaat al/);
await app.close();
});
test('systeemmanager verplaatst een klas naar een andere school; losmaken kan niet', async () => {
const { app, calls } = await makeAppWithSchool(superUser, null);
const res = await app.inject({ method: 'PATCH', url: '/api/admin/classes/100', cookies,
payload: { schoolId: 3 } });
assert.equal(res.statusCode, 200, res.body);
const upd = calls.find((c) => c.sql.startsWith('UPDATE classes SET school_id'));
assert.deepEqual(upd.params, [3, 100]);
// een klas hoort altijd bij een school: geen/null wordt geweigerd
const { app: app2 } = await makeAppWithSchool(superUser, null);
const res2 = await app2.inject({ method: 'PATCH', url: '/api/admin/classes/100', cookies,
payload: { schoolId: null } });
assert.equal(res2.statusCode, 400);
// schoolbeheerder mag dit niet (schools.manage = super)
const { app: app3, calls: calls3 } = await makeAppWithSchool(admin, null);
const res3 = await app3.inject({ method: 'PATCH', url: '/api/admin/classes/100', cookies,
payload: { schoolId: 3 } });
assert.equal(res3.statusCode, 403);
assert.ok(!calls3.some((c) => c.sql.startsWith('UPDATE classes SET school_id')));
await app.close(); await app2.close(); await app3.close();
});
test('klas verwijderen werkt en meldt bij een fout de echte reden i.p.v. kale serverfout', async () => {
const { app, calls } = await makeApp(superUser, null);
const res = await app.inject({ method: 'DELETE', url: '/api/admin/classes/100', cookies });
assert.equal(res.statusCode, 200, res.body);
assert.ok(calls.some((c) => c.sql === 'DELETE FROM classes WHERE id = $1'));
const { app: app2 } = await makeApp(superUser, null);
const pool2 = app2.pg;
const orig = pool2.query.bind(pool2);
pool2.query = async (sql, params) => {
if (sql === 'DELETE FROM classes WHERE id = $1') {
const e = new Error('boom'); e.detail = 'Key is still referenced from table "iets".'; throw e;
}
return orig(sql, params);
};
const res2 = await app2.inject({ method: 'DELETE', url: '/api/admin/classes/100', cookies });
assert.equal(res2.statusCode, 409, res2.body);
assert.match(res2.json().error, /still referenced/);
await app.close(); await app2.close();
});
test('gebruiker verwijderen ruimt eerst alle afhankelijke rijen op (geen serverfout)', async () => {
const { app, calls } = await makeApp(superUser, { id: 9, role: 'teacher', school_id: 2, username: 'juf' });
const res = await app.inject({ method: 'DELETE', url: '/api/admin/users/9', cookies });
assert.equal(res.statusCode, 200, res.body);
// in een transactie, afhankelijke tabellen vóór users
assert.ok(calls.some((c) => c.sql === 'BEGIN'));
assert.ok(calls.some((c) => c.sql.includes('DELETE FROM class_teachers WHERE user_id')));
assert.ok(calls.some((c) => c.sql.includes('DELETE FROM assignments WHERE pupil_id')));
assert.ok(calls.some((c) => c.sql.includes('DELETE FROM boards WHERE owner_id')));
const usersIdx = calls.findIndex((c) => c.sql === 'DELETE FROM users WHERE id = $1');
const ctIdx = calls.findIndex((c) => c.sql.includes('DELETE FROM class_teachers'));
assert.ok(usersIdx > ctIdx, 'gebruiker wordt pas ná de afhankelijke rijen verwijderd');
assert.ok(calls.some((c) => c.sql === 'COMMIT'));
await app.close();
});
test('als verwijderen tóch faalt, komt de echte reden terug (geen kale serverfout)', async () => {
const { app } = await makeApp(superUser, { id: 9, role: 'teacher', school_id: 2, username: 'juf' });
const pool = app.pg;
const orig = pool.connect.bind(pool);
pool.connect = async () => {
const client = await orig();
const q = client.query;
client.query = async (sql, params) => {
if (sql === 'DELETE FROM users WHERE id = $1') { const e = new Error('boom'); e.detail = 'Key is still referenced from table "iets".'; throw e; }
return q(sql, params);
};
return client;
};
const res = await app.inject({ method: 'DELETE', url: '/api/admin/users/9', cookies });
assert.equal(res.statusCode, 409, res.body);
assert.match(res.json().error, /still referenced/);
await app.close();
});
test('faalt de opzoekquery vóór deleteUserFully, dan ook de echte reden i.p.v. kale serverfout', async () => {
const { app } = await makeApp(superUser, { id: 9, role: 'teacher', school_id: 2, username: 'juf' });
const pool = app.pg;
const orig = pool.query.bind(pool);
pool.query = async (sql, params) => {
if (sql.startsWith('SELECT * FROM users WHERE id')) { const e = new Error('connection terminated'); throw e; }
return orig(sql, params);
};
const res = await app.inject({ method: 'DELETE', url: '/api/admin/users/9', cookies });
assert.equal(res.statusCode, 409, res.body);
assert.match(res.json().error, /connection terminated/);
await app.close();
});
test('migratie 015 herstelt ON DELETE CASCADE voor user-foreign-keys', async () => {
const sql = await readFile('db/015_user_fk_cascade.sql', 'utf8');
assert.match(sql, /confrelid = 'users'::regclass/);
assert.match(sql, /ON DELETE CASCADE/);
assert.match(sql, /confdeltype NOT IN \('c', 'n'\)/);
});
test('migratie 016 hernoemt bestaande dubbele klasnamen vóórdat de unieke index wordt gezet', async () => {
const sql = await readFile('db/016_class_name_unique.sql', 'utf8');
// eerst hernoemen (niets verwijderen), dan pas de constraint - anders faalt
// de migratie zelf op een database die deze bug al heeft laten ontstaan
const renameIdx = sql.indexOf('ROW_NUMBER()');
const indexIdx = sql.indexOf('CREATE UNIQUE INDEX');
assert.ok(renameIdx > -1 && indexIdx > -1 && renameIdx < indexIdx,
'hernoemen van dubbele namen moet vóór het aanmaken van de unieke index staan');
assert.match(sql, /PARTITION BY school_id, lower\(name\)/);
assert.doesNotMatch(sql, /DELETE FROM classes/);
});
test('migratie 017 laat schoolbeheerder en systeemmanager ook echt via de database toe aan een klas', async () => {
// regressie: de trigger uit 006 stond alleen 'teacher' toe en blokkeerde
// daarmee (met een verwarrende foutmelding) wat de applicatielaag
// (POST /admin/classes/:id/teachers) allang toestond voor admin/super -
// een mismatch die de gemockte-pool-tests principieel nooit konden vangen.
const sql = await readFile('db/017_class_teacher_roles.sql', 'utf8');
assert.match(sql, /u\.role = 'super'/);
assert.match(sql, /u\.role IN \('teacher', 'admin'\)/);
assert.match(sql, /CREATE OR REPLACE FUNCTION teach_validate_class_teacher/);
});
test('klas aanmaken weigert een dubbele naam binnen dezelfde school (409)', async () => {
const { app } = await makeAppWithSchool(superUser, null);
const pool = app.pg;
const orig = pool.query.bind(pool);
pool.query = async (sql, params) => {
if (sql.startsWith('INSERT INTO classes')) { const e = new Error('dup'); e.code = '23505'; throw e; }
return orig(sql, params);
};
const res = await app.inject({ method: 'POST', url: '/api/admin/classes', cookies,
payload: { name: 'B1', school: 2 } });
assert.equal(res.statusCode, 409, res.body);
assert.match(res.json().error, /bestaat al/);
await app.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();
});
test('systeemmanager aanmaken vereist het eigen wachtwoord van de aanmaker ter bevestiging', async () => {
const ownHash = await hashPassword('correcthorse1');
const superWithPw = { ...superUser, password_hash: ownHash };
// fout eigen wachtwoord: geweigerd, geen insert
const { app, calls } = await makeApp(superWithPw);
const res = await app.inject({ method: 'POST', url: '/api/admin/users', cookies,
payload: { role: 'super', username: 'nieuwesm', confirmPassword: 'helemaal-fout' } });
assert.equal(res.statusCode, 403, res.body);
assert.match(res.json().error, /wachtwoord onjuist/);
assert.ok(!calls.some((c) => c.sql.startsWith('INSERT INTO users')));
await app.close();
// geen eigen wachtwoord meegestuurd: ook geweigerd
const { app: app2, calls: calls2 } = await makeApp(superWithPw);
const res2 = await app2.inject({ method: 'POST', url: '/api/admin/users', cookies,
payload: { role: 'super', username: 'nieuwesm' } });
assert.equal(res2.statusCode, 403, res2.body);
assert.ok(!calls2.some((c) => c.sql.startsWith('INSERT INTO users')));
await app2.close();
// correct eigen wachtwoord: mag door naar de insert
const { app: app3, calls: calls3 } = await makeApp(superWithPw);
const res3 = await app3.inject({ method: 'POST', url: '/api/admin/users', cookies,
payload: { role: 'super', username: 'nieuwesm', confirmPassword: 'correcthorse1' } });
assert.equal(res3.statusCode, 200, res3.body);
assert.ok(calls3.some((c) => c.sql.startsWith('INSERT INTO users')));
await app3.close();
// een schoolbeheerder mag sowieso geen systeemmanager aanmaken (creatableRoles),
// ongeacht wat er als confirmPassword meegestuurd wordt
const adminHash = await hashPassword('adminwachtwoord1');
const { app: app4, calls: calls4 } = await makeApp({ ...admin, password_hash: adminHash });
const res4 = await app4.inject({ method: 'POST', url: '/api/admin/users', cookies,
payload: { role: 'super', username: 'nieuwesm', confirmPassword: 'adminwachtwoord1' } });
assert.equal(res4.statusCode, 403, res4.body);
assert.match(res4.json().error, /geen rechten voor deze rol/);
assert.ok(!calls4.some((c) => c.sql.startsWith('INSERT INTO users')));
await app4.close();
});