| 1 | // FEP-633c §4.2 — de reden mag niet verdampen.
|
|---|
| 2 | //
|
|---|
| 3 | // Guardianship-events waren vluchtig: onGuardianshipEvent wekte de long-poll,
|
|---|
| 4 | // keek in een tabel van zeven soorten of er een push bij hoorde, en liet de
|
|---|
| 5 | // rest vallen. Elf van de achttien soorten verdwenen daarmee spoorloos, met hun
|
|---|
| 6 | // inhoud erbij. Een weigering droeg `reason: 'not_a_teapot'` tot precies daar en
|
|---|
| 7 | // niet verder, terwijl §4.2 eist dat de ward en zijn guardians die reden TE
|
|---|
| 8 | // HOREN krijgen -- en niet dat ze het afleiden uit een aanbod dat opeens weg is.
|
|---|
| 9 | //
|
|---|
| 10 | // Vastleggen en melden zijn nu twee dingen. Deze test bewaakt het eerste; welke
|
|---|
| 11 | // gebeurtenis een mens wakker maakt blijft een aparte beslissing.
|
|---|
| 12 | import { test } from 'node:test';
|
|---|
| 13 | import assert from 'node:assert/strict';
|
|---|
| 14 |
|
|---|
| 15 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 16 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 17 |
|
|---|
| 18 | const dbMod = await import('../src/config/database.js');
|
|---|
| 19 | const db = dbMod.default;
|
|---|
| 20 | dbMod.initializeDatabase();
|
|---|
| 21 | const AP = (await import('../src/services/ActivityPubService.js')).default;
|
|---|
| 22 |
|
|---|
| 23 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 24 | .run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 25 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)')
|
|---|
| 26 | .run('s1', 'kid', 'kid', 'u1');
|
|---|
| 27 |
|
|---|
| 28 | test('een gebeurtenis die geen push oplevert wordt tóch onthouden', () => {
|
|---|
| 29 | // offer_rejected staat niet in de meldingstabel: niemand wordt er wakker van,
|
|---|
| 30 | // en dat was precies de reden dat hij ook nergens terechtkwam.
|
|---|
| 31 | const push = AP.onGuardianshipEvent('kid', {
|
|---|
| 32 | kind: 'offer_rejected', offer: 'o1',
|
|---|
| 33 | reason: 'not_a_teapot', candidate: 'https://elders.example/users/tess',
|
|---|
| 34 | });
|
|---|
| 35 | assert.equal(push, null, 'geen melding, zoals bedoeld');
|
|---|
| 36 |
|
|---|
| 37 | const log = AP.listGuardianEvents('kid');
|
|---|
| 38 | assert.equal(log.length, 1, 'maar wel vastgelegd');
|
|---|
| 39 | assert.equal(log[0].kind, 'offer_rejected');
|
|---|
| 40 | assert.equal(log[0].reason, 'not_a_teapot', 'MET de reden: daar staat dit logboek voor');
|
|---|
| 41 | assert.equal(log[0].candidate, 'https://elders.example/users/tess', 'en om wie het ging');
|
|---|
| 42 | });
|
|---|
| 43 |
|
|---|
| 44 | test('nieuwste eerst, want geschiedenis leest van achteren', () => {
|
|---|
| 45 | AP.onGuardianshipEvent('kid', { kind: 'committed', ward: 'https://test.example/ap/users/kid' });
|
|---|
| 46 | const log = AP.listGuardianEvents('kid');
|
|---|
| 47 | assert.equal(log[0].kind, 'committed');
|
|---|
| 48 | assert.equal(log[1].kind, 'offer_rejected');
|
|---|
| 49 | });
|
|---|
| 50 |
|
|---|
| 51 | test('het logboek van een ander account blijft van hem', () => {
|
|---|
| 52 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,0)')
|
|---|
| 53 | .run('s2', 'mum', 'mum', 'u1');
|
|---|
| 54 | assert.deepEqual(AP.listGuardianEvents('mum'), [],
|
|---|
| 55 | 'wat er over een kind is besloten is niet van iedereen');
|
|---|
| 56 | });
|
|---|
| 57 |
|
|---|
| 58 | test('het loopt niet vol', () => {
|
|---|
| 59 | // Een logboek dat oneindig groeit wordt een logboek dat niemand opent, en
|
|---|
| 60 | // hier ligt het in dezelfde database als alles wat wél om aandacht vraagt.
|
|---|
| 61 | for (let i = 0; i < AP.GUARDIAN_EVENT_KEEP + 25; i++) {
|
|---|
| 62 | AP.onGuardianshipEvent('mum', { kind: 'setting', n: i });
|
|---|
| 63 | }
|
|---|
| 64 | const n = db.prepare('SELECT COUNT(*) AS n FROM ap_guardian_events WHERE slug = ?').get('mum').n;
|
|---|
| 65 | assert.equal(n, AP.GUARDIAN_EVENT_KEEP, 'afgekapt op de bewaargrens');
|
|---|
| 66 | // En het zijn de JONGSTE die blijven: afkappen aan de verkeerde kant zou het
|
|---|
| 67 | // logboek precies onbruikbaar maken op het moment dat er iets gebeurt.
|
|---|
| 68 | assert.equal(AP.listGuardianEvents('mum', 1)[0].n, AP.GUARDIAN_EVENT_KEEP + 24);
|
|---|
| 69 | });
|
|---|
| 70 |
|
|---|
| 71 | test('een kapotte gebeurtenis breekt de gebeurtenis zelf niet', () => {
|
|---|
| 72 | // Het logboek is bijzaak. Zou een schrijffout hier de commit of de weigering
|
|---|
| 73 | // meesleuren, dan is de cure erger dan de kwaal.
|
|---|
| 74 | assert.doesNotThrow(() => AP.onGuardianshipEvent('kid', { kind: 'setting', zelf: { a: null } }));
|
|---|
| 75 | assert.doesNotThrow(() => AP.onGuardianshipEvent('kid', {}));
|
|---|
| 76 | assert.doesNotThrow(() => AP.onGuardianshipEvent(null, { kind: 'setting' }));
|
|---|
| 77 | });
|
|---|
| 78 |
|
|---|
| 79 | // ── De collectie die de apps lezen (shaer:log) ────────────────────────────
|
|---|
| 80 | // Het logboek staat NAAST shaer:queues. Alles daarin wacht op een antwoord; dit
|
|---|
| 81 | // is wat er al besloten is. Een aparte sleutel dus, en een aparte vorm.
|
|---|
| 82 |
|
|---|
| 83 | test('de actor wijst het logboek aan, los van de wachtrijen', async () => {
|
|---|
| 84 | const G = await import('../src/services/guardianship/index.js');
|
|---|
| 85 | const props = G.guardianshipActorProps('https://test.example/ap/users/kid', 'kid');
|
|---|
| 86 | assert.equal(props['shaer:log'], 'https://test.example/ap/users/kid/log');
|
|---|
| 87 | assert.equal(props['shaer:queues'].log, undefined,
|
|---|
| 88 | 'geen geschiedenis onder een woord dat "wachtend" betekent');
|
|---|
| 89 | });
|
|---|
| 90 |
|
|---|
| 91 | test('elk item draagt zijn soort en zijn reden mee', async () => {
|
|---|
| 92 | const G = await import('../src/services/guardianship/index.js');
|
|---|
| 93 | const c = G.logCollection('https://test.example/ap/users/kid/log', 'kid',
|
|---|
| 94 | (s) => AP.listGuardianEvents(s, 50));
|
|---|
| 95 | assert.equal(c.type, 'OrderedCollection');
|
|---|
| 96 | const weigering = c.orderedItems.find((i) => i['shaer:kind'] === 'offer_rejected');
|
|---|
| 97 | assert.ok(weigering, 'de weigering staat erin');
|
|---|
| 98 | assert.equal(weigering['shaer:reason'], 'not_a_teapot',
|
|---|
| 99 | 'MET de reden: daarvoor bestaat dit ding');
|
|---|
| 100 | assert.equal(weigering['shaer:candidate'], 'https://elders.example/users/tess');
|
|---|
| 101 | assert.ok(weigering.published, 'en wanneer');
|
|---|
| 102 | // Geen AS2-werkwoord: een lapse-stem of een opgepakte hulpvraag is geen
|
|---|
| 103 | // Accept, en het zo noemen zou netter lezen dan het is.
|
|---|
| 104 | assert.equal(weigering.type, 'shaer:Event');
|
|---|
| 105 | });
|
|---|
| 106 |
|
|---|
| 107 | test('een leeg logboek is een lege collectie, geen fout', async () => {
|
|---|
| 108 | const G = await import('../src/services/guardianship/index.js');
|
|---|
| 109 | const c = G.logCollection('https://test.example/ap/users/niemand/log', 'niemand',
|
|---|
| 110 | (s) => AP.listGuardianEvents(s, 50));
|
|---|
| 111 | assert.equal(c.totalItems, 0);
|
|---|
| 112 | assert.deepEqual(c.orderedItems, []);
|
|---|
| 113 | });
|
|---|