All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m28s
228 lines
9.7 KiB
JavaScript
228 lines
9.7 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import Fastify from 'fastify';
|
|
import cookie from '@fastify/cookie';
|
|
import rateLimit from '@fastify/rate-limit';
|
|
import api from '../src/api.js';
|
|
|
|
const cookies = { teach_session: 'x'.repeat(64) };
|
|
const boardData = {
|
|
boards: { folders: [{ boards: [{
|
|
id: 'board-live', name: 'Live bord',
|
|
data: { widgets: [{ id: 'letters', wid: 'letters-1', state: { words: ['boom'] } }] },
|
|
}] }] },
|
|
};
|
|
|
|
async function makeApp(user, query) {
|
|
const app = Fastify({ trustProxy: 2 });
|
|
await app.register(cookie);
|
|
await app.register(rateLimit, { global: false });
|
|
app.decorate('pg', { query });
|
|
await app.register(api, { prefix: '/api' });
|
|
await app.ready();
|
|
return app;
|
|
}
|
|
|
|
function auth(user, sql) {
|
|
if (sql.includes('FROM sessions s JOIN users u')) return { rows: [user] };
|
|
if (sql.includes('FROM user_roles')) return { rows: [] };
|
|
return null;
|
|
}
|
|
|
|
test('echte widgetinteractie start een beveiligde live leersessie', async () => {
|
|
const pupil = {
|
|
id: 9, username: 'lena', display_name: 'Lena', role: 'pupil',
|
|
school_id: 2, class_id: 50, data: {}, data_rev: 0,
|
|
};
|
|
const assignment = {
|
|
id: 80, class_id: 50, teacher_id: 3, board_id: 'board-live',
|
|
widget_id: 'letters-1', mode: 'werken', sequence: [],
|
|
};
|
|
const calls = [];
|
|
const query = async (sql, params = []) => {
|
|
calls.push({ sql, params });
|
|
const a = auth(pupil, sql);
|
|
if (a) return a;
|
|
if (sql === 'SELECT * FROM assignments WHERE pupil_id = $1') return { rows: [assignment] };
|
|
if (sql === 'SELECT data FROM users WHERE id = $1') return { rows: [{ data: boardData }] };
|
|
if (sql.includes('INSERT INTO live_learning_sessions')) return { rows: [{
|
|
id: 101, pupil_id: 9, teacher_id: 3, class_id: 50, assignment_id: 80,
|
|
board_id: 'board-live', widget_id: 'letters-1', widget_type: 'letters',
|
|
mode: 'werken', started_at: new Date(), last_seen_at: new Date(),
|
|
}] };
|
|
return { rows: [] };
|
|
};
|
|
const app = await makeApp(pupil, query);
|
|
const response = await app.inject({
|
|
method: 'POST', url: '/api/my/live-session/start', cookies,
|
|
payload: { widgetId: 'letters-1', widgetType: 'letters', mode: 'werken' },
|
|
});
|
|
assert.equal(response.statusCode, 200, response.body);
|
|
assert.equal(response.json().session.status, 'live');
|
|
const insert = calls.find((call) => call.sql.includes('INSERT INTO live_learning_sessions'));
|
|
assert.deepEqual(insert.params, [9, 3, 50, 80, 'board-live', 'letters-1', 'letters', 'werken']);
|
|
assert.ok(calls.some((call) => call.sql.includes('SET ended_at = now()')),
|
|
'een vorige open sessie wordt eerst recent gemaakt');
|
|
await app.close();
|
|
});
|
|
|
|
test('zelfstandige bibliotheekopdracht start live zonder bord van de leerkracht', async () => {
|
|
const pupil = {
|
|
id: 9, username: 'lena', display_name: 'Lena', role: 'pupil',
|
|
school_id: 2, class_id: 50, data: {}, data_rev: 0,
|
|
};
|
|
const assignment = {
|
|
id: 81, class_id: 50, teacher_id: 3, board_id: 'assignment-library',
|
|
widget_id: 'assignment-step-1', mode: 'werken',
|
|
sequence: [{
|
|
id: 'step-1', source: 'library', boardId: 'assignment-library',
|
|
widgetId: 'assignment-step-1', widgetType: 'letters', mode: 'werken',
|
|
requiresProgress: true, theme: null,
|
|
}],
|
|
};
|
|
const calls = [];
|
|
const query = async (sql, params = []) => {
|
|
calls.push({ sql, params });
|
|
const a = auth(pupil, sql);
|
|
if (a) return a;
|
|
if (sql === 'SELECT * FROM assignments WHERE pupil_id = $1') return { rows: [assignment] };
|
|
if (sql.startsWith('SELECT step_id') && sql.includes('FROM assignment_step_progress')) return { rows: [] };
|
|
if (sql.includes('INSERT INTO live_learning_sessions')) return { rows: [{
|
|
id: 102, pupil_id: 9, teacher_id: 3, class_id: 50, assignment_id: 81,
|
|
board_id: 'assignment-library', widget_id: 'assignment-step-1', widget_type: 'letters',
|
|
mode: 'werken', started_at: new Date(), last_seen_at: new Date(),
|
|
}] };
|
|
return { rows: [] };
|
|
};
|
|
const app = await makeApp(pupil, query);
|
|
const response = await app.inject({
|
|
method: 'POST', url: '/api/my/live-session/start', cookies,
|
|
payload: { widgetId: 'assignment-step-1', widgetType: 'letters', mode: 'werken' },
|
|
});
|
|
assert.equal(response.statusCode, 200, response.body);
|
|
const insert = calls.find((call) => call.sql.includes('INSERT INTO live_learning_sessions'));
|
|
assert.deepEqual(insert.params, [
|
|
9, 3, 50, 81, 'assignment-library', 'assignment-step-1', 'letters', 'werken',
|
|
]);
|
|
assert.ok(!calls.some((call) => call.sql.includes('SELECT data FROM users')));
|
|
await app.close();
|
|
});
|
|
|
|
test('voortgang houdt dezelfde live sessie direct actueel', async () => {
|
|
const pupil = {
|
|
id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50,
|
|
data: {}, data_rev: 0,
|
|
};
|
|
const calls = [];
|
|
const query = async (sql, params = []) => {
|
|
calls.push({ sql, params });
|
|
const a = auth(pupil, sql);
|
|
if (a) return a;
|
|
if (sql === 'SELECT * FROM assignments WHERE pupil_id = $1')
|
|
return { rows: [{ id: 80, teacher_id: 3, board_id: 'board-live', sequence: [] }] };
|
|
if (sql.includes('INSERT INTO progress_events')) return { rows: [] };
|
|
if (sql.includes('progress_count = progress_count + 1')) return { rows: [{ class_id: 50 }] };
|
|
return { rows: [] };
|
|
};
|
|
const app = await makeApp(pupil, query);
|
|
const response = await app.inject({
|
|
method: 'POST', url: '/api/my/progress', cookies,
|
|
payload: {
|
|
widgetId: 'letters-1', widgetType: 'letters', attempts: 4, correct: 3,
|
|
stars: 1, liveSessionId: 101,
|
|
},
|
|
});
|
|
assert.equal(response.statusCode, 200, response.body);
|
|
const update = calls.find((call) => call.sql.includes('progress_count = progress_count + 1'));
|
|
assert.deepEqual(update.params, [101, 9, 4, 3]);
|
|
await app.close();
|
|
});
|
|
|
|
test('leerkracht ziet nieuwe actieve en meest recente sessies van de eigen klas', async () => {
|
|
const teacher = {
|
|
id: 3, username: 'juf', role: 'teacher', school_id: 2,
|
|
class_id: null, data: boardData, data_rev: 0,
|
|
};
|
|
const now = new Date();
|
|
const query = async (sql) => {
|
|
const a = auth(teacher, sql);
|
|
if (a) return a;
|
|
if (sql.startsWith('SELECT * FROM classes')) return { rows: [{ id: 50, school_id: 2 }] };
|
|
if (sql.startsWith('SELECT 1 FROM class_teachers')) return { rows: [{ ok: 1 }] };
|
|
if (sql.includes('FROM live_learning_sessions ls')) return { rows: [
|
|
{
|
|
id: 101, pupil_id: 9, display_name: 'Lena', widget_id: 'letters-1',
|
|
widget_type: 'letters', mode: 'werken', started_at: now, last_seen_at: now,
|
|
progress_count: 1, attempts: 2, correct: 2, ended_at: null,
|
|
},
|
|
{
|
|
id: 100, pupil_id: 8, display_name: 'Sam', widget_id: 'math-1',
|
|
widget_type: 'madd', mode: 'werken', started_at: new Date(now - 120000),
|
|
last_seen_at: new Date(now - 60000), progress_count: 2, attempts: 4,
|
|
correct: 3, ended_at: new Date(now - 60000),
|
|
},
|
|
] };
|
|
return { rows: [] };
|
|
};
|
|
const app = await makeApp(teacher, query);
|
|
const response = await app.inject({
|
|
method: 'GET', url: '/api/progress/live/class/50', cookies,
|
|
});
|
|
assert.equal(response.statusCode, 200, response.body);
|
|
const body = response.json();
|
|
assert.equal(body.liveCount, 1);
|
|
assert.deepEqual(body.sessions.map((session) => session.status), ['live', 'recent']);
|
|
assert.equal(body.sessions[0].pupilName, 'Lena');
|
|
await app.close();
|
|
});
|
|
|
|
test('groepsleiding kan geen live sessies van een andere klas volgen', async () => {
|
|
const teacher = {
|
|
id: 3, username: 'juf', role: 'teacher', school_id: 2,
|
|
class_id: null, data: boardData, data_rev: 0,
|
|
};
|
|
const query = async (sql) => {
|
|
const a = auth(teacher, sql);
|
|
if (a) return a;
|
|
if (sql.startsWith('SELECT * FROM classes')) return { rows: [{ id: 51, school_id: 2 }] };
|
|
if (sql.startsWith('SELECT 1 FROM class_teachers')) return { rows: [] };
|
|
return { rows: [] };
|
|
};
|
|
const app = await makeApp(teacher, query);
|
|
const response = await app.inject({
|
|
method: 'GET', url: '/api/progress/live/class/51', cookies,
|
|
});
|
|
assert.equal(response.statusCode, 403);
|
|
await app.close();
|
|
});
|
|
|
|
test('live dashboard gebruikt SSE, heartbeats, retentie en ongebufferde proxying', async () => {
|
|
const [migration, server, pupil, dashboard, admin, html, nginx, core, version] = await Promise.all([
|
|
readFile('db/022_live_learning_sessions.sql', 'utf8'),
|
|
readFile('src/live-classroom.js', 'utf8'),
|
|
readFile('public/js/pupil.js', 'utf8'),
|
|
readFile('public/js/live-classroom-dashboard.js', 'utf8'),
|
|
readFile('public/js/admin.js', 'utf8'),
|
|
readFile('public/index.html', 'utf8'),
|
|
readFile('deploy/nginx.conf', 'utf8'),
|
|
readFile('public/js/core.js', 'utf8'),
|
|
readFile('VERSION', 'utf8'),
|
|
]);
|
|
for (const column of ['started_at', 'last_seen_at', 'ended_at', 'progress_count'])
|
|
assert.ok(migration.includes(column), column);
|
|
assert.match(server, /text\/event-stream/);
|
|
assert.match(server, /publish\(classStreams, classId, \x27sessions\x27\)/);
|
|
assert.match(server, /interval '24 hours'/);
|
|
assert.match(pupil, /pointerdown[\s\S]*ensurePupilLiveSession/);
|
|
assert.match(pupil, /PUPIL_LIVE_HEARTBEAT_MS/);
|
|
assert.match(pupil, /liveSessionId/);
|
|
assert.match(dashboard, /new EventSource/);
|
|
assert.match(dashboard, /setInterval[\s\S]*30000/);
|
|
assert.match(admin, /progress\/live\/class/);
|
|
assert.ok(html.indexOf('js/live-classroom-dashboard.js') < html.indexOf('js/admin.js'));
|
|
assert.match(nginx, /location \/api\/progress\/live\/[\s\S]*proxy_buffering off/);
|
|
for (const key of ['amLiveSessions', 'amLiveNew', 'amLiveRecent', 'amLiveConnected'])
|
|
assert.equal((core.match(new RegExp(key + ':', 'g')) || []).length, 2, key);
|
|
assert.ok(core.includes('const VERSION = "' + version.trim() + '"'));
|
|
});
|