All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m23s
238 lines
11 KiB
JavaScript
238 lines
11 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';
|
|
|
|
// Ingelogde leerkracht via een gemockte sessie: elke query die de sessie-join
|
|
// doet levert de gebruiker, de data-update legt zijn SQL+params vast zodat de
|
|
// test de rev-voorwaarde en het 409-pad kan controleren.
|
|
function makeApp({ dataRev = 3, updateSucceeds = true, ownedRewards = [] } = {}) {
|
|
const calls = [];
|
|
const pool = {
|
|
async query(sql, params = []) {
|
|
calls.push({ sql, params });
|
|
if (sql.includes('FROM sessions s JOIN users u')) return { rows: [{
|
|
id: 7, username: 'docent', display_name: 'Docent', role: 'teacher',
|
|
school_id: 2, class_id: null, data: {}, data_rev: dataRev,
|
|
}] };
|
|
if (sql.includes('FROM user_roles')) return { rows: [] };
|
|
if (sql.startsWith('SELECT reward_key FROM user_avatar_rewards')) {
|
|
return { rows: ownedRewards.map((reward_key) => ({ reward_key })) };
|
|
}
|
|
if (sql.startsWith('SELECT data, data_rev')) return { rows: [{ data: { boards: {} }, data_rev: dataRev }] };
|
|
if (sql.startsWith('UPDATE users SET data =')) {
|
|
return updateSucceeds ? { rows: [{ data_rev: dataRev + 1 }] } : { rows: [] };
|
|
}
|
|
return { rows: [] };
|
|
},
|
|
};
|
|
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 sessionCookie = { teach_session: 'x'.repeat(64) };
|
|
|
|
const avatarPayload = {
|
|
species: "animal", speciesVariant: "rabbit", bodyColor: "#aabbcc",
|
|
hairStyle: "wavy", hairColor: "#553311", accessoryHead: "glasses", accessoryBody: "scarf",
|
|
};
|
|
|
|
test("avatar-API bewaart een geldige variant binnen de gekozen categorie", async () => {
|
|
const { app, calls } = await makeApp();
|
|
const res = await app.inject({ method: "PUT", url: "/api/me/avatar", cookies: sessionCookie, payload: avatarPayload });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
assert.equal(res.json().avatar.speciesVariant, "rabbit");
|
|
const update = calls.find((c) => c.sql.startsWith("UPDATE users SET avatar_data"));
|
|
assert.equal(update.params[0].species, "animal");
|
|
assert.equal(update.params[0].speciesVariant, "rabbit");
|
|
await app.close();
|
|
});
|
|
|
|
test("avatar-API geeft oudere clients een standaardvariant", async () => {
|
|
const { app } = await makeApp();
|
|
const { speciesVariant, ...legacyPayload } = avatarPayload;
|
|
const res = await app.inject({ method: "PUT", url: "/api/me/avatar", cookies: sessionCookie, payload: legacyPayload });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
assert.equal(res.json().avatar.speciesVariant, "cat");
|
|
await app.close();
|
|
});
|
|
|
|
test("avatar-API weigert een variant uit een andere categorie", async () => {
|
|
const { app, calls } = await makeApp();
|
|
const res = await app.inject({ method: "PUT", url: "/api/me/avatar", cookies: sessionCookie, payload: { ...avatarPayload, speciesVariant: "rex" } });
|
|
assert.equal(res.statusCode, 400, res.body);
|
|
assert.equal(res.json().error, "ongeldige soortvariant");
|
|
assert.equal(calls.some((c) => c.sql.startsWith("UPDATE users SET avatar_data")),false);
|
|
await app.close();
|
|
});
|
|
test("avatar-API weigert een beloningsaccessoire totdat het is vrijgespeeld", async () => {
|
|
const locked = await makeApp();
|
|
const denied = await locked.app.inject({
|
|
method: "PUT", url: "/api/me/avatar", cookies: sessionCookie,
|
|
payload: { ...avatarPayload, accessoryHead: "star" },
|
|
});
|
|
assert.equal(denied.statusCode, 403, denied.body);
|
|
assert.equal(locked.calls.some((call) => call.sql.startsWith("UPDATE users SET avatar_data")), false);
|
|
await locked.app.close();
|
|
|
|
const unlocked = await makeApp({ ownedRewards: ["head:star"] });
|
|
const allowed = await unlocked.app.inject({
|
|
method: "PUT", url: "/api/me/avatar", cookies: sessionCookie,
|
|
payload: { ...avatarPayload, accessoryHead: "star" },
|
|
});
|
|
assert.equal(allowed.statusCode, 200, allowed.body);
|
|
await unlocked.app.close();
|
|
});
|
|
test('GET /me/data geeft data én rev terug', async () => {
|
|
const { app } = await makeApp({ dataRev: 3 });
|
|
const res = await app.inject({ method: 'GET', url: '/api/me/data', cookies: sessionCookie });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
assert.deepEqual(res.json(), { data: { boards: {} }, rev: 3 });
|
|
await app.close();
|
|
});
|
|
|
|
test('PUT /me/data met actuele rev slaagt en geeft de nieuwe rev', async () => {
|
|
const { app, calls } = await makeApp({ dataRev: 3 });
|
|
const res = await app.inject({ method: 'PUT', url: '/api/me/data', cookies: sessionCookie,
|
|
headers: { 'x-data-rev': '3' }, payload: { boards: {} } });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
assert.equal(res.json().rev, 4);
|
|
const upd = calls.find((c) => c.sql.startsWith('UPDATE users SET data ='));
|
|
assert.ok(upd.sql.includes('AND data_rev = $3'));
|
|
assert.equal(upd.params[2], 3);
|
|
await app.close();
|
|
});
|
|
|
|
test('PUT /me/data met verouderde rev geeft 409', async () => {
|
|
const { app } = await makeApp({ dataRev: 5, updateSucceeds: false });
|
|
const res = await app.inject({ method: 'PUT', url: '/api/me/data', cookies: sessionCookie,
|
|
headers: { 'x-data-rev': '3' }, payload: { boards: {} } });
|
|
assert.equal(res.statusCode, 409, res.body);
|
|
assert.match(res.json().error, /ander tabblad/);
|
|
await app.close();
|
|
});
|
|
|
|
test('PUT /me/data zonder rev-header slaat zonder voorwaarde op (legacy)', async () => {
|
|
const { app, calls } = await makeApp({ dataRev: 3 });
|
|
const res = await app.inject({ method: 'PUT', url: '/api/me/data', cookies: sessionCookie,
|
|
payload: { boards: {} } });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
const upd = calls.find((c) => c.sql.startsWith('UPDATE users SET data ='));
|
|
assert.ok(!upd.sql.includes('AND data_rev'));
|
|
await app.close();
|
|
});
|
|
|
|
test('PUT /me/data/:section vervangt alleen die sub-key via jsonb_set', async () => {
|
|
const { app, calls } = await makeApp({ dataRev: 3 });
|
|
const res = await app.inject({ method: 'PUT', url: '/api/me/data/themes', cookies: sessionCookie,
|
|
headers: { 'x-data-rev': '3' }, payload: { nl: [], en: [] } });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
assert.equal(res.json().rev, 4);
|
|
const upd = calls.find((c) => c.sql.startsWith('UPDATE users SET data ='));
|
|
assert.ok(upd.sql.includes('jsonb_set'));
|
|
assert.equal(upd.params[0], '{themes}');
|
|
assert.ok(upd.sql.includes('AND data_rev = $4'));
|
|
assert.equal(upd.params[3], 3);
|
|
await app.close();
|
|
});
|
|
|
|
test('PUT /me/data/:section weigert onbekende secties', async () => {
|
|
const { app } = await makeApp({ dataRev: 3 });
|
|
const res = await app.inject({ method: 'PUT', url: '/api/me/data/hackme', cookies: sessionCookie,
|
|
headers: { 'x-data-rev': '3' }, payload: {} });
|
|
assert.equal(res.statusCode, 404);
|
|
await app.close();
|
|
});
|
|
|
|
test('PUT /me/data/anchors accepteert een lijst (en de andere secties juist niet)', async () => {
|
|
const { app, calls } = await makeApp({ dataRev: 3 });
|
|
const ok = await app.inject({ method: 'PUT', url: '/api/me/data/anchors', cookies: sessionCookie,
|
|
headers: { 'x-data-rev': '3' }, payload: [{ id: 'al-1', name: 'water', cells: {}, pics: {} }] });
|
|
assert.equal(ok.statusCode, 200, ok.body);
|
|
const upd = calls.find((c) => c.sql.includes('jsonb_set'));
|
|
assert.equal(upd.params[0], '{anchors}');
|
|
const bad = await app.inject({ method: 'PUT', url: '/api/me/data/themes', cookies: sessionCookie,
|
|
headers: { 'x-data-rev': '4' }, payload: [1, 2] });
|
|
assert.equal(bad.statusCode, 400);
|
|
const badAnchors = await app.inject({ method: 'PUT', url: '/api/me/data/anchors', cookies: sessionCookie,
|
|
headers: { 'x-data-rev': '4' }, payload: { niet: 'een lijst' } });
|
|
assert.equal(badAnchors.statusCode, 400);
|
|
await app.close();
|
|
});
|
|
|
|
test('PUT /me/data maakt eerst een snapshot en ruimt oude versies op', async () => {
|
|
const { app, calls } = await makeApp({ dataRev: 3 });
|
|
const res = await app.inject({ method: 'PUT', url: '/api/me/data', cookies: sessionCookie,
|
|
headers: { 'x-data-rev': '3' }, payload: { boards: {} } });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
const snapIdx = calls.findIndex((c) => c.sql.includes('INSERT INTO user_data_versions'));
|
|
const cleanIdx = calls.findIndex((c) => c.sql.includes('DELETE FROM user_data_versions'));
|
|
const updIdx = calls.findIndex((c) => c.sql.startsWith('UPDATE users SET data ='));
|
|
assert.ok(snapIdx >= 0 && cleanIdx >= 0);
|
|
assert.ok(snapIdx < updIdx, 'snapshot vóór de overschrijving');
|
|
assert.equal(calls[cleanIdx].params[1], 10);
|
|
await app.close();
|
|
});
|
|
|
|
test('GET /me/data/versions weigert leerlingen', async () => {
|
|
const { app } = await makeApp({ dataRev: 3 });
|
|
// aparte app met pupil-rol
|
|
const pupilApp = await (async () => {
|
|
const calls = [];
|
|
const pool = { async query(sql, params = []) {
|
|
calls.push({ sql, params });
|
|
if (sql.includes('FROM sessions s JOIN users u')) return { rows: [{
|
|
id: 8, username: 'kind', role: 'pupil', school_id: 2, class_id: 5, data: {}, data_rev: 0,
|
|
}] };
|
|
return { rows: [] };
|
|
} };
|
|
const a = Fastify({ trustProxy: 2 });
|
|
await a.register(cookie);
|
|
await a.register(rateLimit, { global: false });
|
|
a.decorate('pg', pool);
|
|
await a.register(api, { prefix: '/api' });
|
|
await a.ready();
|
|
return a;
|
|
})();
|
|
const res = await pupilApp.inject({ method: 'GET', url: '/api/me/data/versions', cookies: sessionCookie });
|
|
assert.equal(res.statusCode, 403);
|
|
await pupilApp.close();
|
|
await app.close();
|
|
});
|
|
|
|
test('POST /me/data/restore zet een versie terug en bewaart de huidige eerst', async () => {
|
|
const calls = [];
|
|
const pool = { async query(sql, params = []) {
|
|
calls.push({ sql, params });
|
|
if (sql.includes('FROM sessions s JOIN users u')) return { rows: [{
|
|
id: 7, username: 'docent', role: 'teacher', school_id: 2, class_id: null, data: { boards: { f: 1 } }, data_rev: 6,
|
|
}] };
|
|
if (sql.includes('FROM user_roles')) return { rows: [] };
|
|
if (sql.startsWith('SELECT data FROM user_data_versions')) return { rows: [{ data: { boards: { f: 0 } } }] };
|
|
if (sql.startsWith('UPDATE users SET data =')) return { rows: [{ data_rev: 7 }] };
|
|
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: 'POST', url: '/api/me/data/restore', cookies: sessionCookie,
|
|
payload: { versionId: 12 } });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
assert.deepEqual(res.json(), { data: { boards: { f: 0 } }, rev: 7 });
|
|
const snapIdx = calls.findIndex((c) => c.sql.includes('INSERT INTO user_data_versions'));
|
|
const updIdx = calls.findIndex((c) => c.sql.startsWith('UPDATE users SET data ='));
|
|
assert.ok(snapIdx >= 0 && snapIdx < updIdx, 'huidige data eerst bewaard');
|
|
await app.close();
|
|
});
|