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)); }); /* "1 opdracht i.p.v. een groep" - de fix zelf zit in math.js (per som loggen i.p.v. per blok van 10), maar het datamodel/de server moeten het optionele detail (welke som/welk antwoord) wel kunnen bewaren zonder de rest van de voortgang-logging te breken bij rommelige/te grote input. */ test('voortgang: detail (welke opdracht) wordt meegegeven aan de 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 detail = { question: '7 + 8 =', answer: 14, expected: 15, correct: false }; const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: { ...validBody, detail } }); assert.equal(res.statusCode, 200, res.body); assert.deepEqual(insertCall(calls).params[8], detail); }); test('voortgang: een te grote detail wordt genegeerd zonder de hele log-poging te laten falen', 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 detail = { question: 'x'.repeat(2000) }; const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: { ...validBody, detail } }); assert.equal(res.statusCode, 200, res.body); assert.equal(insertCall(calls).params[8], null); }); test('voortgang: een detail dat geen plain object is (array/string) wordt genegeerd', 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: { ...validBody, detail: ['niet', 'plain'] } }); assert.equal(res.statusCode, 200, res.body); assert.equal(insertCall(calls).params[8], null); }); test('voortgang: zonder detail-veld blijft alles zoals voorheen (null, geen fout)', 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: validBody }); assert.equal(res.statusCode, 200, res.body); assert.equal(insertCall(calls).params[8], null); });