teach/test/pupil-levels.test.js
Ramon 0841962824
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 44s
feat: voeg thematische kleurplaten toe (v0.4.65-beta)
2026-07-28 22:34:15 +02:00

172 lines
8.8 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { runInNewContext } from 'node:vm';
import Fastify from 'fastify';
import cookie from '@fastify/cookie';
import rateLimit from '@fastify/rate-limit';
import api from '../src/api.js';
/* Klassenmanagement: niveaugroep (groep 1-8) + niveau (1-3) per leerling per
vakgebied. Zelfde soort dekking als test/progress-logging.test.js voor het
server-pad, plus een echte (niet regex-)test van de SLO-getalgrenzen die
math.js gebruikt zodra een leerling een groep heeft. */
function makeApp(user, { classRow = { id: 100, school_id: 2, name: 'Groep 4' }, classDefault = null, pupilOverride = null, classPupilOverrides = [] } = {}) {
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('SELECT * FROM classes WHERE id')) return { rows: classRow ? [classRow] : [] };
if (sql.startsWith('SELECT * FROM users WHERE id')) return { rows: [user] };
if (sql === 'SELECT * FROM pupil_levels WHERE class_id = $1 AND subject = $2') return { rows: classDefault ? [classDefault] : [] };
if (sql === 'SELECT * FROM pupil_levels WHERE pupil_id = $1 AND subject = $2') return { rows: pupilOverride ? [pupilOverride] : [] };
if (sql.includes('FROM pupil_levels pl JOIN users u')) return { rows: classPupilOverrides };
if (sql.startsWith('INSERT INTO pupil_levels')) return { rows: [] };
if (sql.startsWith('DELETE FROM pupil_levels')) return { rows: [] };
return { rows: [] };
};
const pool = { query, 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 };
})();
}
const cookies = { teach_session: 'x'.repeat(64) };
const teacher = { id: 3, username: 'juf', role: 'teacher', school_id: 2, class_id: null, data: {}, data_rev: 0 };
const admin = { id: 4, username: 'beheer', role: 'admin', school_id: 2, class_id: null, data: {}, data_rev: 0 };
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 100, data: {}, data_rev: 0 };
const homePupil = { id: 10, username: 'thuis', role: 'pupil', school_id: null, class_id: null, data: {}, data_rev: 0 };
test('klassenmanagement: klas-standaard opslaan doet de juiste upsert-query', async () => {
const { app, calls } = await makeApp(admin);
const res = await app.inject({
method: 'PUT', url: '/api/levels/class/100', cookies,
payload: { subject: 'creatief', yearGroup: 4, level: 2 },
});
assert.equal(res.statusCode, 200, res.body);
const ins = calls.find(c => c.sql.startsWith('INSERT INTO pupil_levels'));
assert.ok(ins);
assert.deepEqual(ins.params, [100, 'creatief', 4, 2]);
});
test('klassenmanagement: ongeldig vakgebied geeft 400', async () => {
const { app } = await makeApp(teacher);
const res = await app.inject({
method: 'PUT', url: '/api/levels/class/100', cookies,
payload: { subject: 'gym', yearGroup: 4, level: 2 },
});
assert.equal(res.statusCode, 400, res.body);
});
test('klassenmanagement: groep buiten 1-8 geeft 400', async () => {
const { app } = await makeApp(teacher);
const res = await app.inject({
method: 'PUT', url: '/api/levels/class/100', cookies,
payload: { subject: 'rekenen', yearGroup: 12, level: 2 },
});
assert.equal(res.statusCode, 400, res.body);
});
test('klassenmanagement: alleen de drie beschreven uitwerkingen zijn geldig', async () => {
const { app } = await makeApp(teacher);
const res = await app.inject({
method: 'PUT', url: '/api/levels/class/100', cookies,
payload: { subject: 'rekenen', yearGroup: 4, level: 4 },
});
assert.equal(res.statusCode, 400, res.body);
});
test('klassenmanagement: GET class levert klas-standaard én leerling-overrides in één keer', async () => {
const { app } = await makeApp(admin, {
classDefault: { year_group: 4, level: 2 },
classPupilOverrides: [{ pupil_id: 9, year_group: 6, level: 3 }],
});
const res = await app.inject({ method: 'GET', url: '/api/levels/class/100?subject=rekenen', cookies });
assert.equal(res.statusCode, 200, res.body);
const body = res.json();
assert.deepEqual(body.classDefault, { yearGroup: 4, level: 2 });
assert.deepEqual(body.pupilOverrides, [{ pupilId: 9, yearGroup: 6, level: 3 }]);
});
test('klassenmanagement: /my/level - leerling-override wint van klas-standaard', async () => {
const { app } = await makeApp(pupil, { pupilOverride: { year_group: 6, level: 3 } });
const res = await app.inject({ method: 'GET', url: '/api/my/level?subject=rekenen', cookies });
assert.equal(res.statusCode, 200, res.body);
assert.deepEqual(res.json(), { yearGroup: 6, level: 3 });
});
test('klassenmanagement: /my/level - valt terug op de klas-standaard zonder eigen override', async () => {
const { app } = await makeApp(pupil, { classDefault: { year_group: 4, level: 2 } });
const res = await app.inject({ method: 'GET', url: '/api/my/level?subject=rekenen', cookies });
assert.equal(res.statusCode, 200, res.body);
assert.deepEqual(res.json(), { yearGroup: 4, level: 2 });
});
test('klassenmanagement: /my/level - niets ingesteld geeft de bestaande widget-standaardwaarde', async () => {
const { app } = await makeApp(pupil);
const res = await app.inject({ method: 'GET', url: '/api/my/level?subject=rekenen', cookies });
assert.equal(res.statusCode, 200, res.body);
assert.deepEqual(res.json(), { yearGroup: null, level: 1 });
});
test('klassenmanagement: leerkracht-rol wordt geweigerd op /my/level (alleen leerlingen)', async () => {
const { app } = await makeApp(teacher);
const res = await app.inject({ method: 'GET', url: '/api/my/level?subject=rekenen', cookies });
assert.equal(res.statusCode, 403, res.body);
});
test('thuiskind kan eigen creatief niveau opslaan, schoolleerling niet', async () => {
const { app, calls } = await makeApp(homePupil);
const res = await app.inject({ method: 'PUT', url: '/api/my/level', cookies,
payload: { subject: 'creatief', yearGroup: 5, level: 2 } });
assert.equal(res.statusCode, 200, res.body);
const insert = calls.find(call=>call.sql.startsWith('INSERT INTO pupil_levels'));
assert.deepEqual(insert.params,[10,'creatief',5,2]);
const { app: schoolApp } = await makeApp(pupil);
const denied = await schoolApp.inject({ method: 'PUT', url: '/api/my/level', cookies,
payload: { subject: 'creatief', yearGroup: 5, level: 2 } });
assert.equal(denied.statusCode,403,denied.body);
await app.close();await schoolApp.close();
});
/* Echte (niet regex-)test van de SLO-getalgrenzen: data.js's SLO_MATH_RANGES/
mathRangeFor zijn pure functies zonder DOM-afhankelijkheid, dus rechtstreeks
uitvoerbaar in een geïsoleerde VM-context - zelfde techniek als elders in
test/widgets.test.js voor MONEY_LEVELS/createMoneyTask. */
test('SLO-rekengrenzen: groep bepaalt de daadwerkelijke getalgrens, niet meer de vaste 10/20/100-klem', async () => {
const source = await readFile('public/js/data.js', 'utf8'), context = {};
runInNewContext(source + ';globalThis.__mathRange = mathRangeFor', context);
const mathRangeFor = context.__mathRange;
assert.equal(mathRangeFor(3, 3).addSub, 20, 'groep 3 t/m 20');
assert.equal(mathRangeFor(4, 3).addSub, 100, 'groep 4 t/m 100');
assert.equal(mathRangeFor(5, 3).addSub, 1000, 'groep 5 t/m 1000');
assert.equal(mathRangeFor(6, 3).addSub, 10000, 'groep 6 t/m 10.000');
// niveau is een verfijning BINNEN de groep: nooit hoger dan de groep-grens
for (let g = 1; g <= 8; g++) {
const r1 = mathRangeFor(g, 1), r2 = mathRangeFor(g, 2), r3 = mathRangeFor(g, 3);
assert.ok(r1.addSub <= r2.addSub && r2.addSub <= r3.addSub, `groep ${g}: niveau moet oplopen`);
assert.ok(r1.addSub >= 10, `groep ${g} niveau 1 nooit onder de kleinste bestaande standaardwaarde`);
}
// zonder groep (geen klassenmanagement ingesteld) geeft mathRangeFor niets terug -
// math.js valt dan terug op zijn eigen oude vaste logica
assert.equal(mathRangeFor(null, 2), null);
assert.equal(mathRangeFor(undefined, 2), null);
});
test('math.js: gen() gebruikt leerjaarregels en behoudt zonder groep de oude vaste klem', async () => {
const source = await readFile('public/js/widgets/math.js', 'utf8');
assert.match(source, /const slo = mathRangeFor\(S\.yearGroup, L\)/);
assert.match(source, /const rules = mathTaskRules\(S\.yearGroup, L\)/);
assert.match(source, /const max = rules \? rules\.addSub/);
assert.match(source, /const tmax = rules \? rules\.timesTable/);
assert.ok(source.includes('S.yearGroup = initState.yearGroup || null'));
});