- Nieuwe migratie db/010: shared_library-tabel (school_id NULL = systeembreed, anders per school; kind board/anchor; folder als pad) - Routes: GET /shared/:kind (lijst, gefilterd op systeembreed + eigen school), GET /shared/:kind/:id (met data, voor kopiëren), POST (publiceren; scope global vereist systeemmanager, scope school staf met school), PATCH/DELETE per item en PATCH /shared/:kind/folder (prefix-rewrite, alleen als álle items onder het pad beheerd mogen worden) - Beheerregel: systeemmanager altijd, eigenaar altijd, schoolbeheerder voor items van de eigen school; leerlingen hebben geen toegang - Nieuwe permissies shared.global.manage / shared.school.manage (beide permissions.js); zes nieuwe servertests
111 lines
5.1 KiB
JavaScript
111 lines
5.1 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';
|
|
|
|
// App met een instelbare ingelogde gebruiker en vastgelegde queries; de
|
|
// shared_library-select levert een vaste mix van systeembrede en school-items.
|
|
function makeApp(user, sharedRows = []) {
|
|
const calls = [];
|
|
const pool = {
|
|
async query(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.includes('FROM shared_library')) return { rows: sharedRows };
|
|
if (sql.includes('INSERT INTO shared_library') && sql.includes('RETURNING id')) return { rows: [{ id: 42 }] };
|
|
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 cookies = { teach_session: 'x'.repeat(64) };
|
|
const superUser = { id: 1, username: 'sm', role: 'super', school_id: null, class_id: null, data: {}, data_rev: 0 };
|
|
const teacher = { id: 7, username: 'juf', role: 'teacher', school_id: 2, class_id: null, data: {}, data_rev: 0 };
|
|
const pupil = { id: 9, username: 'kind', role: 'pupil', school_id: 2, class_id: 5, data: {}, data_rev: 0 };
|
|
|
|
test('GET /shared/:kind filtert op systeembreed + eigen school voor staf', async () => {
|
|
const { app, calls } = await makeApp(teacher, [
|
|
{ id: 1, school_id: null, owner_id: 1, folder: 'Rekenen', name: 'Breuken', owner_name: 'SM' },
|
|
]);
|
|
const res = await app.inject({ method: 'GET', url: '/api/shared/board', cookies });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
const items = res.json().items;
|
|
assert.equal(items[0].scope, 'global');
|
|
assert.equal(items[0].canManage, false);
|
|
const q = calls.find((c) => c.sql.includes('FROM shared_library'));
|
|
assert.ok(q.sql.includes('school_id IS NULL OR s.school_id = $2'));
|
|
assert.equal(q.params[1], 2);
|
|
await app.close();
|
|
});
|
|
|
|
test('leerlingen hebben geen toegang tot de gedeelde bibliotheek', async () => {
|
|
const { app } = await makeApp(pupil);
|
|
const res = await app.inject({ method: 'GET', url: '/api/shared/board', cookies });
|
|
assert.equal(res.statusCode, 403);
|
|
await app.close();
|
|
});
|
|
|
|
test('publiceren met scope global vereist systeemmanager', async () => {
|
|
const { app } = await makeApp(teacher);
|
|
const res = await app.inject({ method: 'POST', url: '/api/shared/board', cookies,
|
|
payload: { name: 'Breuken', folder: 'Rekenen', data: { widgets: [] }, scope: 'global' } });
|
|
assert.equal(res.statusCode, 403);
|
|
const { app: superApp, calls } = await makeApp(superUser);
|
|
const ok = await superApp.inject({ method: 'POST', url: '/api/shared/board', cookies,
|
|
payload: { name: 'Breuken', folder: 'Rekenen', data: { widgets: [] }, scope: 'global' } });
|
|
assert.equal(ok.statusCode, 200, ok.body);
|
|
const ins = calls.find((c) => c.sql.includes('INSERT INTO shared_library'));
|
|
assert.equal(ins.params[0], null); // school_id NULL = systeembreed
|
|
await superApp.close();
|
|
await app.close();
|
|
});
|
|
|
|
test('publiceren met scope school gebruikt de eigen school', async () => {
|
|
const { app, calls } = await makeApp(teacher);
|
|
const res = await app.inject({ method: 'POST', url: '/api/shared/anchor', cookies,
|
|
payload: { name: 'water', data: { cells: {} }, scope: 'school' } });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
const ins = calls.find((c) => c.sql.includes('INSERT INTO shared_library'));
|
|
assert.equal(ins.params[0], 2);
|
|
assert.equal(ins.params[2], 'anchor');
|
|
await app.close();
|
|
});
|
|
|
|
test('beheren mag alleen door eigenaar, schoolbeheerder of systeemmanager', async () => {
|
|
// teacher (id 7) probeert andermans school-item te verwijderen
|
|
const foreignItem = { id: 5, school_id: 2, owner_id: 8, kind: 'board', folder: '', name: 'X', data: {} };
|
|
const { app } = await makeApp(teacher, [foreignItem]);
|
|
const res = await app.inject({ method: 'DELETE', url: '/api/shared/board/5', cookies });
|
|
assert.equal(res.statusCode, 403);
|
|
// eigenaar mag wel
|
|
const ownItem = { ...foreignItem, owner_id: 7 };
|
|
const { app: app2 } = await makeApp(teacher, [ownItem]);
|
|
const res2 = await app2.inject({ method: 'DELETE', url: '/api/shared/board/5', cookies });
|
|
assert.equal(res2.statusCode, 200, res2.body);
|
|
await app2.close();
|
|
await app.close();
|
|
});
|
|
|
|
test('map-rewrite weigert als niet alle items beheerd mogen worden', async () => {
|
|
const rows = [
|
|
{ id: 5, school_id: 2, owner_id: 7, kind: 'board', folder: 'Taal', name: 'A', data: {} },
|
|
{ id: 6, school_id: 2, owner_id: 8, kind: 'board', folder: 'Taal/Sub', name: 'B', data: {} },
|
|
];
|
|
const { app } = await makeApp(teacher, rows);
|
|
const res = await app.inject({ method: 'PATCH', url: '/api/shared/board/folder', cookies,
|
|
payload: { scope: 'school', from: 'Taal', to: 'Lezen' } });
|
|
assert.equal(res.statusCode, 403);
|
|
await app.close();
|
|
});
|