source: Klonkt/test/guardianship.test.js@ e84ce32

main
Last change on this file since e84ce32 was e61c289, checked in by Robin Genis <roboburr@…>, 7 weeks ago

Guardian-queues als C2S-routes + push-typen help/guardian

De drie shaer:queues-collecties uit het actor-doc zijn nu echte owner-only
routes (Bearer, zelfde patroon als /blocked): offers, follows (leeg tot
gated follows bestaan) en wards. Contract gelijk aan de Shaer test-daemon,
dus de iOS/Android-dashboards lezen ze ongewijzigd.

Web-push kent twee nieuwe alert-typen: 'help' (hulpvraag van een ward,
niet gethrottled, standaard aan) en 'guardian' (adoptieverkeer). Teksten in
nl/en/de.

Changed files:
src/routes/activitypub.js

  • GET /ap/users/:slug/queues/{offers,follows,wards}, owner-only

src/services/PushService.js

  • alert-typen help + guardian (defaults aan, throttle 0/30s)

src/services/i18n.js

  • push.n_help_*, push.n_guard_* in nl/en/de

New file:
test/guardianship.test.js

  • pint het module-oppervlak: actor-props, handshake C2S+S2S beide kanten, queue-shapes, ward-mag-niet-guarden, helpRequest-props

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 5.4 KB
RevLine 
[e61c289]1// The guardianship module (FEP-633c): relations, actor props, the adoption
2// handshake and the dashboard queues. Pins the module's public surface so the
3// Shaer clients' contract stays stable.
4import { test } from 'node:test';
5import assert from 'node:assert/strict';
6
7process.env.DATABASE_PATH = ':memory:';
8process.env.PUBLIC_BASE_URL = 'https://test.example';
9
10const dbMod = await import('../src/config/database.js');
11const db = dbMod.default;
12dbMod.initializeDatabase();
13const AP = (await import('../src/services/ActivityPubService.js')).default;
14const G = await import('../src/services/guardianship/index.js');
15
16db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
17db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s1', 'parent', 'Parent', 'u1', 1);
18db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s2', 'kid', 'Kid', 'u1', 0);
19const parent = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
20const kid = db.prepare('SELECT * FROM sites WHERE id = ?').get('s2');
21const ME = 'https://test.example/ap/users/parent';
22const KID = 'https://test.example/ap/users/kid';
23
24// No network in tests: the handshake delivers via this stub.
25const sent = [];
26G.wireHandshake({
27 selfId: (slug) => `https://test.example/ap/users/${slug}`,
28 deliverTo: async (site, uri, activity) => { sent.push({ from: site.slug, to: uri, activity }); return true; },
29 deriveHandle: (uri) => '@' + String(uri).split('/').pop() + '@test.example',
30 onEvent: null,
31});
32
33test('actor doc advertises shaer:queues (and blocked stays)', () => {
34 const actor = AP.buildActor('https://test.example', parent);
35 assert.equal(actor.blocked, `${ME}/blocked`);
36 assert.deepEqual(actor['shaer:queues'], {
37 offers: `${ME}/queues/offers`,
38 follows: `${ME}/queues/follows`,
39 wards: `${ME}/queues/wards`,
40 });
41 assert.equal(actor['shaer:isGuardian'], undefined); // no wards yet
42});
43
44test('C2S Offer from the candidate records + delivers (FEP-633c 3)', async () => {
45 const r = await G.handleGuardianshipOutbox(parent, {
46 type: 'Offer',
47 object: { type: 'Relationship', subject: KID, relationship: 'shaer:Guardian', object: ME },
48 });
49 assert.equal(r.status, 202);
50 assert.equal(sent.length, 1);
51 assert.equal(sent[0].to, KID);
52 assert.equal(sent[0].activity.type, 'Offer');
53 const wards = G.listWards('parent');
54 assert.equal(wards.length, 1);
55 assert.equal(wards[0].status, 'offered');
56 // The guardian-to-be now reads as guardian; the actor doc follows.
57 const actor = AP.buildActor('https://test.example', parent);
58 assert.equal(actor['shaer:isGuardian'], true);
59});
60
61test('only the candidate may offer', async () => {
62 const r = await G.handleGuardianshipOutbox(parent, {
63 type: 'Offer',
64 object: { type: 'Relationship', subject: KID, relationship: 'shaer:Guardian', object: 'https://elders.test/u/x' },
65 });
66 assert.equal(r.status, 403);
67});
68
69test('inbound Offer parks in the ward queue; C2S Accept commits both ends', async () => {
70 // The kid's side receives the offer S2S.
71 const offerId = sent[0].activity.id;
72 const consumed = await G.handleGuardianshipInbox(kid, {
73 id: offerId, type: 'Offer', actor: ME,
74 object: { type: 'Relationship', subject: KID, relationship: 'shaer:Guardian', object: ME },
75 });
76 assert.equal(consumed, true);
77 assert.equal(G.listOffers('kid').length, 1);
78
79 // The kid accepts over C2S; the answer travels to the guardian.
80 const r = await G.handleGuardianshipOutbox(kid, { type: 'Accept', object: offerId });
81 assert.equal(r.status, 202);
82 assert.deepEqual(G.listGuardians('kid').map((g) => g.other_uri), [ME]);
83
84 // The guardian's side hears the Accept S2S and commits.
85 const ok = await G.handleGuardianshipInbox(parent, { type: 'Accept', actor: KID, object: offerId });
86 assert.equal(ok, true);
87 const wards = G.listWards('parent').filter((w) => w.status === 'accepted');
88 assert.deepEqual(wards.map((w) => w.other_uri), [KID]);
89
90 // The ward's actor doc now names its guardian (FEP-633c 2.1).
91 const actor = AP.buildActor('https://test.example', kid);
92 assert.deepEqual(actor['shaer:guardians'], [ME]);
93});
94
95test('queues serve the daemon contract shapes', () => {
96 const wardsQ = G.wardsCollection(`${ME}/queues/wards`, 'parent');
97 assert.equal(wardsQ.type, 'OrderedCollection');
98 assert.equal(wardsQ.totalItems, 1);
99 assert.equal(wardsQ.orderedItems[0].id, KID);
100 const followsQ = G.followsCollection(`${ME}/queues/follows`);
101 assert.deepEqual(followsQ.orderedItems, []);
102 const offersQ = G.offersCollection(`${ME}/queues/offers`, 'parent', ME);
103 assert.equal(offersQ.type, 'OrderedCollection'); // empty again after the accept
104 assert.equal(offersQ.totalItems, 0);
105});
106
107test('a ward cannot become a guardian (FEP-633c 1)', async () => {
108 const r = await G.handleGuardianshipOutbox(kid, {
109 type: 'Offer',
110 object: { type: 'Relationship', subject: 'https://other.test/u/y', relationship: 'shaer:Guardian', object: KID },
111 });
112 assert.equal(r.status, 403);
113 assert.equal(r.error, 'a_ward_cannot_guard');
114});
115
116test('helpRequest props only ride direct notes', () => {
117 assert.deepEqual(G.helpRequestProps({ visibility: 'direct', help_request: 1 }), { 'shaer:helpRequest': true });
118 assert.deepEqual(G.helpRequestProps({ visibility: 'public', help_request: 1 }), {});
119 assert.equal(G.isHelpRequest({ 'shaer:helpRequest': true }), true);
120 assert.equal(G.isHelpRequest({}), false);
121});
Note: See TracBrowser for help on using the repository browser.