All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 46s
- Roadmap-punt "de voortgangen werken niet" onderzocht (niet blind
herbouwd - eerst gediagnosticeerd via de code en git-geschiedenis).
- Gevonden: pupil.js zette sinds v0.3.15-beta (toen kijken/werken-modus
werd toegevoegd) readonly:true in BEIDE takken van de modus-keuze -
een kopieerfout, want de hele reden voor die commit was net dat
"werken" de leerling wél moet laten werken. In "werken"-modus hoort
er geen readonly-vlag te staan; "kijken" (blijft ongewijzigd) is de
enige modus die dat nodig heeft.
- Concreet effect: de twee widgets die opts.readonly daadwerkelijk
lezen (letters.js/anchor.js) verborgen voor leerlingen in
werken-modus onterecht hun eigen bewerk-gereedschap (woordenlijst-
editor, opslaan/bibliotheek) - gereedschap dat een leerling in
werken-modus wél hoort te zien. De kernoefening zelf (spellen,
slepen) werd hierdoor niet geblokkeerd, dus dit verklaart niet de
volledige klacht - overige widgets negeren opts.readonly toch al.
- Tegelijk de stille `.catch(()=>{})` op het voortgang-loggen vervangen
door `console.warn` bij een mislukte POST /my/progress: een mislukte
log (verlopen sessie, weggehaalde toewijzing, servergolf) verdween
eerder spoorloos, wat een "werkt niet"-klacht onmogelijk te
onderzoeken maakte.
- Nieuwe test/progress-logging.test.js dekt het serverpad dat nog
helemaal geen tests had: leerling-eigen toewijzing wint van
klasbrede, thuiskind-zonder-school valt terug op de gekoppelde
ouder, geen-toewijzing geeft 400 zonder insert, niet-leerling geeft
403, ongeldige widgetId/widgetType geeft 400 - alle zes bevestigen
dat de SQL/toewijzingslogica zelf klopt, wat het probleem verder
vernauwt tot de nu twee gefixte punten hierboven of iets dat alleen
in de echte omgeving van de gebruiker reproduceerbaar is.
- Live geverifieerd: werken-modus toont nu het bewerk-gereedschap in
letters.js (voorheen verborgen), kijken-modus blijft exact
ongewijzigd (overlay + verborgen gereedschap). Volledige testsuite:
118/118.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
103 lines
5.3 KiB
JavaScript
103 lines
5.3 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';
|
|
|
|
/* "de voortgangen werken niet" (gebruikersklacht) - dit pad had nog GEEN enkele
|
|
test (mocked-pool of anders), ondanks dat het de kern van de voortgangs-
|
|
feature is: een leerling logt een afgerond moment, en het datamodel bepaalt
|
|
zelf teacher_id/board_id uit de toewijzing (nooit van de client). Dekt de
|
|
drie manieren waarop een toewijzing gevonden kan worden (leerling-eigen,
|
|
klasbreed, thuiskind-via-ouder) plus de foutpaden. */
|
|
function makeApp(user, { assignmentRows = {}, parentRows = [] } = {}) {
|
|
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 === 'SELECT * FROM assignments WHERE pupil_id = $1') return { rows: assignmentRows.byPupil ? [assignmentRows.byPupil] : [] };
|
|
if (sql === 'SELECT * FROM assignments WHERE class_id = $1') return { rows: assignmentRows.byClass ? [assignmentRows.byClass] : [] };
|
|
if (sql.startsWith('SELECT parent_id FROM parent_children')) return { rows: parentRows };
|
|
if (sql.startsWith('INSERT INTO progress_events')) 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 validBody = { widgetId: 'letters:0', widgetType: 'letters', attempts: 1, correct: 1, stars: 1 };
|
|
function insertCall(calls) {
|
|
return calls.find(c => c.sql.startsWith('INSERT INTO progress_events'));
|
|
}
|
|
|
|
test('voortgang: leerling-eigen toewijzing wint en levert de juiste teacher_id/board_id op', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, {
|
|
assignmentRows: {
|
|
byPupil: { teacher_id: 3, board_id: 'bord-a' },
|
|
byClass: { teacher_id: 4, board_id: 'bord-b' }, // zou genegeerd moeten worden
|
|
},
|
|
});
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
const ins = insertCall(calls);
|
|
assert.ok(ins, 'INSERT verwacht');
|
|
assert.equal(ins.params[1], 3);
|
|
assert.equal(ins.params[2], 'bord-a');
|
|
});
|
|
|
|
test('voortgang: valt terug op de klasbrede toewijzing als er geen eigen toewijzing is', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, {
|
|
assignmentRows: { byClass: { teacher_id: 4, board_id: 'bord-b' } },
|
|
});
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
const ins = insertCall(calls);
|
|
assert.equal(ins.params[1], 4);
|
|
assert.equal(ins.params[2], 'bord-b');
|
|
});
|
|
|
|
test('voortgang: thuiskind zonder school valt terug op de gekoppelde ouder als "leerkracht"', async () => {
|
|
const homeChild = { id: 11, username: 'timo', role: 'pupil', school_id: null, class_id: null, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(homeChild, { parentRows: [{ parent_id: 7 }] });
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
const ins = insertCall(calls);
|
|
assert.equal(ins.params[1], 7);
|
|
assert.equal(ins.params[2], 'thuis');
|
|
});
|
|
|
|
test('voortgang: leerling op school zonder enige toewijzing krijgt 400, geen stille silent-fail insert', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, {});
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
|
assert.equal(res.statusCode, 400, res.body);
|
|
assert.ok(!insertCall(calls), 'er mag geen INSERT gebeurd zijn');
|
|
});
|
|
|
|
test('voortgang: niet-leerling (bv. leerkracht) krijgt 403', async () => {
|
|
const teacher = { id: 3, username: 'juf', role: 'teacher', school_id: 2, class_id: null, data: {}, data_rev: 0 };
|
|
const { app } = await makeApp(teacher, {});
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
|
assert.equal(res.statusCode, 403, res.body);
|
|
});
|
|
|
|
test('voortgang: ontbrekende widgetId/widgetType levert 400 op, niet een kapotte insert', async () => {
|
|
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
|
const { app, calls } = await makeApp(pupil, { assignmentRows: { byPupil: { teacher_id: 3, board_id: 'bord-a' } } });
|
|
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: { attempts: 1, correct: 1 } });
|
|
assert.equal(res.statusCode, 400, res.body);
|
|
assert.ok(!insertCall(calls));
|
|
});
|