source: Klonkt/test/guardianship.test.js@ 1a2d8ac

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

Offers-queue draagt de daemon-hulpvelden (accept-knop in de apps)

De Shaer-clients renderen hun accepteer-knop uit de shaer:-hulpvelden die
de test-daemon op elk offer-item zet (shaer:ward, candidate, needsMyAccept,
iAmCandidate, ...). Klonkt's offers-queue gaf een kale AS2-Offer, waardoor
de parser lege velden zag en de knop nooit verscheen. Nu draagt elk item
beide vormen: de AS2 Relationship én de hulpvelden. Klonkts flow is
één-fase (de Accept van de ward maakt het meteen echt), dus needsMyAccept
geldt de ward-kant en readyToCommit blijft false.

Changed files:
src/services/guardianship/queues.js

  • offersCollection: shaer:ward/candidate/existingGuardians/acceptedBy/ needsMyAccept/readyToCommit/iAmCandidate per item

test/guardianship.test.js

  • pint de hulpvelden op de kid-queue

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

  • Property mode set to 100644
File size: 5.9 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
[1a2d8ac]79 // The kid's offers queue carries the daemon-contract helper fields the
80 // Shaer clients render their accept button from.
81 const kidQ = G.offersCollection(`${KID}/queues/offers`, 'kid', KID);
82 assert.equal(kidQ.totalItems, 1);
83 assert.equal(kidQ.orderedItems[0]['shaer:needsMyAccept'], true);
84 assert.equal(kidQ.orderedItems[0]['shaer:iAmCandidate'], false);
85 assert.equal(kidQ.orderedItems[0]['shaer:ward'], KID);
86 assert.equal(kidQ.orderedItems[0]['shaer:candidate'], ME);
87
[e61c289]88 // The kid accepts over C2S; the answer travels to the guardian.
89 const r = await G.handleGuardianshipOutbox(kid, { type: 'Accept', object: offerId });
90 assert.equal(r.status, 202);
91 assert.deepEqual(G.listGuardians('kid').map((g) => g.other_uri), [ME]);
92
93 // The guardian's side hears the Accept S2S and commits.
94 const ok = await G.handleGuardianshipInbox(parent, { type: 'Accept', actor: KID, object: offerId });
95 assert.equal(ok, true);
96 const wards = G.listWards('parent').filter((w) => w.status === 'accepted');
97 assert.deepEqual(wards.map((w) => w.other_uri), [KID]);
98
99 // The ward's actor doc now names its guardian (FEP-633c 2.1).
100 const actor = AP.buildActor('https://test.example', kid);
101 assert.deepEqual(actor['shaer:guardians'], [ME]);
102});
103
104test('queues serve the daemon contract shapes', () => {
105 const wardsQ = G.wardsCollection(`${ME}/queues/wards`, 'parent');
106 assert.equal(wardsQ.type, 'OrderedCollection');
107 assert.equal(wardsQ.totalItems, 1);
108 assert.equal(wardsQ.orderedItems[0].id, KID);
109 const followsQ = G.followsCollection(`${ME}/queues/follows`);
110 assert.deepEqual(followsQ.orderedItems, []);
111 const offersQ = G.offersCollection(`${ME}/queues/offers`, 'parent', ME);
112 assert.equal(offersQ.type, 'OrderedCollection'); // empty again after the accept
113 assert.equal(offersQ.totalItems, 0);
114});
115
116test('a ward cannot become a guardian (FEP-633c 1)', async () => {
117 const r = await G.handleGuardianshipOutbox(kid, {
118 type: 'Offer',
119 object: { type: 'Relationship', subject: 'https://other.test/u/y', relationship: 'shaer:Guardian', object: KID },
120 });
121 assert.equal(r.status, 403);
122 assert.equal(r.error, 'a_ward_cannot_guard');
123});
124
125test('helpRequest props only ride direct notes', () => {
126 assert.deepEqual(G.helpRequestProps({ visibility: 'direct', help_request: 1 }), { 'shaer:helpRequest': true });
127 assert.deepEqual(G.helpRequestProps({ visibility: 'public', help_request: 1 }), {});
128 assert.equal(G.isHelpRequest({ 'shaer:helpRequest': true }), true);
129 assert.equal(G.isHelpRequest({}), false);
130});
Note: See TracBrowser for help on using the repository browser.