- Nieuwe migratie db/011: shared_library.readonly (default false)
- Beheerregel aangescherpt: een vergrendeld school-item is ook voor de
eigenaar alleen-lezen; alleen schoolbeheerder en systeemmanager kunnen
het nog wijzigen (systeem-items waren al alleen-lezen voor iedereen
behalve de systeemmanager) - kopiëren blijft altijd werken
- De vlag zetten/weghalen kan per item (PATCH readonly) of per map
(PATCH /shared/:kind/folder met {from, readonly}), uitsluitend door de
schoolbeheerder van die school of de systeemmanager
- Lijst/detail geven readonly + canLock terug voor de UI; drie nieuwe tests
159 lines
7.6 KiB
JavaScript
159 lines
7.6 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('een vergrendeld school-item is ook voor de eigenaar alleen-lezen', async () => {
|
|
const lockedOwn = { id: 5, school_id: 2, owner_id: 7, kind: 'board', folder: '', name: 'X', data: {}, readonly: true };
|
|
const { app } = await makeApp(teacher, [lockedOwn]);
|
|
const res = await app.inject({ method: 'PATCH', url: '/api/shared/board/5', cookies,
|
|
payload: { name: 'Nieuwe naam' } });
|
|
assert.equal(res.statusCode, 403);
|
|
// kopiëren (GET met data) blijft gewoon werken
|
|
const get = await app.inject({ method: 'GET', url: '/api/shared/board/5', cookies });
|
|
assert.equal(get.statusCode, 200);
|
|
assert.equal(get.json().item.readonly, true);
|
|
assert.equal(get.json().item.canManage, false);
|
|
await app.close();
|
|
});
|
|
|
|
test('alleen schoolbeheerder/systeemmanager mag de alleen-lezen-vlag zetten', async () => {
|
|
const ownItem = { id: 5, school_id: 2, owner_id: 7, kind: 'board', folder: '', name: 'X', data: {}, readonly: false };
|
|
// eigenaar-leerkracht mag de vlag NIET zetten
|
|
const { app } = await makeApp(teacher, [ownItem]);
|
|
const res = await app.inject({ method: 'PATCH', url: '/api/shared/board/5', cookies,
|
|
payload: { readonly: true } });
|
|
assert.equal(res.statusCode, 403);
|
|
await app.close();
|
|
// schoolbeheerder van dezelfde school wel
|
|
const admin = { id: 3, username: 'beheer', role: 'admin', school_id: 2, class_id: null, data: {}, data_rev: 0 };
|
|
const { app: app2, calls } = await makeApp(admin, [ownItem]);
|
|
const res2 = await app2.inject({ method: 'PATCH', url: '/api/shared/board/5', cookies,
|
|
payload: { readonly: true } });
|
|
assert.equal(res2.statusCode, 200, res2.body);
|
|
const upd = calls.find((c) => c.sql.includes('SET readonly'));
|
|
assert.deepEqual(upd.params, [true, 5]);
|
|
await app2.close();
|
|
});
|
|
|
|
test('map vergrendelen zet de vlag op alle items onder het pad', async () => {
|
|
const admin = { id: 3, username: 'beheer', role: 'admin', school_id: 2, class_id: null, data: {}, data_rev: 0 };
|
|
const rows = [
|
|
{ id: 5, school_id: 2, owner_id: 7, kind: 'board', folder: 'Taal', name: 'A', data: {}, readonly: false },
|
|
{ id: 6, school_id: 2, owner_id: 8, kind: 'board', folder: 'Taal/Sub', name: 'B', data: {}, readonly: false },
|
|
];
|
|
const { app, calls } = await makeApp(admin, rows);
|
|
const res = await app.inject({ method: 'PATCH', url: '/api/shared/board/folder', cookies,
|
|
payload: { scope: 'school', from: 'Taal', readonly: true } });
|
|
assert.equal(res.statusCode, 200, res.body);
|
|
const upd = calls.find((c) => c.sql.includes('SET readonly'));
|
|
assert.deepEqual(upd.params, [true, [5, 6]]);
|
|
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();
|
|
});
|