source: Klonkt/test/outgoing-follow-gate.test.js@ 3a0ca0f

main
Last change on this file since 3a0ca0f was 3a0ca0f, checked in by roboburr <roboburr@…>, 4 weeks ago

De volgpoort zat alleen in de app, en volgverzoeken beslissen bij meerderheid

Barts twee meldingen (8-8), en de eerste is een echte vondst.

  1. WAAROM ESMEE'S VOLGVERZOEKEN NOOIT AANKWAMEN

Niet wat Bart vreesde -- het was niet zo dat alleen een intern account ze kon
beantwoorden. Die weg werkt: gateOutgoingFollow levert een Offer af bij een
guardian elders, en dat is precies wat dev als guardian van @mee had moeten
krijgen.

Er kwam alleen nooit iets om af te leveren. De poort stond in case 'Follow' van
de C2S-outbox, dus alleen als je via Shaer volgt. Volgde het kind vanuit Klonkts
eigen webinterface, dan werd er geen verzoek aangemaakt, ging er niets naar de
guardians, en was er dus ook niets te beantwoorden. Op dev staat geen enkele rij
in ap_follow_reviews en nergens een follow-approval-regel in de log.

Exact dezelfde deur-naast-de-poort als bij de antwoordpoort vanmiddag: de gate in
C2S, het webpad eromheen. De controle staat nu in followActor zelf, NA het
oplossen van de handle -- zou hij alleen naar de ruwe invoer kijken, dan is elke
@naam@server een sluiproute. approved is de enige doorlaat, voor
performApprovedFollow; zonder dat stuit een goedgekeurd verzoek opnieuw op de
poort en wacht het voor eeuwig.

Drie webroutes zeiden "Je volgt nu X" terwijl het verzoek bij de guardians lag.
Dat is de leugen die de poort waardeloos maakt: het kind denkt dat het gebeurd
is. Er is nu een derde uitkomst.

DE MUTATIE GAF EERST NUL FOUTEN -- er stond niets op deze poort. Nu vier toetsen,
inclusief het handle-geval en het goedgekeurde pad.

  1. VOLGVERZOEKEN BIJ EENVOUDIGE MEERDERHEID

Barts besluit: 1 van 2 is voldoende. followThreshold = ceil(n/2), voor beide
richtingen, want "mag dit kind met deze persoon te maken hebben" is dezelfde
vraag en twee drempels zou een guardian nooit kunnen uitleggen.

BEWUST SOEPELER DAN DE POORTDREMPEL (2 van 2): een gate opent een deur voor alles
wat daarna komt, een volgverzoek gaat over een persoon en is met ontvolgen terug
te draaien. En het is geen versoepeling maar een AANSCHERPING -- dit stond op
'any', een enkele ja hoeveel guardians er ook waren.

De 'all'-stand is weg: hij werd nergens gezet, dus was het een keuze die niemand
kon maken. De toets die hem bewaakte is herschreven, niet verwijderd.

Race naar de drempel zoals de poorttelling, met dezelfde TODO erbij (shaer-8vt):
wie antwoordt weet niet dat hij de doorslag geeft.

Suite 742/742; beide mutaties rood.

  • Property mode set to 100644
File size: 7.2 KB
Line 
1// FEP-633c §5.3, the direction that was never gated (bead shaer-p729).
2//
3// A ward's own follow used to go straight out; the guardians got a note
4// afterwards, which is informing, not gating — the door is already open by the
5// time the message lands. Now it waits, with two exceptions that are not
6// favours but the same decision already taken: the ward's own guardian, and
7// someone a guardian already admitted through the inbound gate.
8//
9// The mutual shortcut only counts followers who came through that gate. A
10// follower a free actor collected before it was ever a ward was never seen by
11// a guardian, so following them back is a new question. Rows that predate the
12// marker are grandfathered (Barts besluit, 3-8).
13import { test } from 'node:test';
14import assert from 'node:assert/strict';
15
16process.env.DATABASE_PATH = ':memory:';
17process.env.PUBLIC_BASE_URL = 'https://test.example';
18
19const dbMod = await import('../src/config/database.js');
20const db = dbMod.default;
21dbMod.initializeDatabase();
22const AP = (await import('../src/services/ActivityPubService.js')).default;
23const G = await import('../src/services/guardianship/index.js');
24
25const BASE = 'https://test.example';
26const local = (slug) => `${BASE}/ap/users/${slug}`;
27const STRANGER = 'https://elders.example/users/stranger';
28const PAL = 'https://elders.example/users/pal';
29const OLDPAL = 'https://elders.example/users/oldpal';
30
31db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
32 .run('u1', 'u1', 'u1@test', 'x', 'god');
33let n = 0;
34function site(slug) {
35 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)')
36 .run(`s${++n}`, slug, slug, 'u1', n === 1 ? 1 : 0);
37 return db.prepare('SELECT * FROM sites WHERE slug = ?').get(slug);
38}
39const guards = (slug, other) =>
40 db.prepare("INSERT INTO ap_guardianships (slug, role, other_uri, status) VALUES (?, 'ward', ?, 'accepted')")
41 .run(slug, other);
42const follower = (slug, uri, gateApproved) =>
43 db.prepare('INSERT INTO ap_followers (slug, actor_uri, inbox, gate_approved) VALUES (?,?,?,?)')
44 .run(slug, uri, `${uri}/inbox`, gateApproved ? 1 : 0);
45
46const kid = site('kid');
47site('mum');
48guards('kid', local('mum')); // kid is a ward, watched by mum
49follower('kid', PAL, true); // came through the §5.3 gate
50follower('kid', OLDPAL, false); // followed back when kid was still free
51
52const free = site('freebird'); // no guardians at all
53
54test('a free actor is not gated at all', async () => {
55 assert.equal(await AP.gateOutgoingFollow(free, STRANGER), null,
56 'guardianship is the only thing that gates a follow; a free account keeps its own counsel');
57});
58
59test('a stranger has to wait for the guardians', async () => {
60 const held = await AP.gateOutgoingFollow(kid, STRANGER);
61 assert.ok(held, 'held, not sent');
62 assert.equal(held.status, 'pending');
63 assert.equal(held.target_uri, STRANGER);
64});
65
66test('following your own guardian needs nobody\'s permission', async () => {
67 assert.equal(await AP.gateOutgoingFollow(kid, local('mum')), null,
68 'asking mum whether you may follow mum is not a question');
69});
70
71test('a follower the guardians already admitted may be followed back', async () => {
72 assert.equal(await AP.gateOutgoingFollow(kid, PAL), null,
73 'a guardian said yes to this person by name; asking twice teaches people to stop reading');
74});
75
76test('but a follower from before the guardians existed is a fresh question', async () => {
77 const held = await AP.gateOutgoingFollow(kid, OLDPAL);
78 assert.ok(held, 'never went through the gate, so nobody ever vetted them');
79 assert.equal(held.status, 'pending');
80});
81
82test('asking twice does not queue the same request twice', async () => {
83 const again = await AP.gateOutgoingFollow(kid, STRANGER);
84 assert.ok(again);
85 assert.equal(G.outgoing.listForWard('kid').filter((o) => o.target_uri === STRANGER).length, 1);
86});
87
88test('the guardians see it in their own queue, apart from the inbound one', () => {
89 const q = G.outgoingFollowsCollection(`${local('kid')}/queues/outgoing-follows`, 'kid', local('mum'));
90 const mine = q.orderedItems.filter((o) => o['shaer:target'] === STRANGER);
91 assert.equal(mine.length, 1);
92 assert.equal(mine[0]['shaer:direction'], 'outgoing',
93 'a guardian must be able to tell "wants to follow your ward" from "your ward wants to follow"');
94 assert.equal(mine[0]['shaer:myVote'], false);
95});
96
97test('a guardian approving lets it through from then on', async () => {
98 const pending = G.outgoing.listForWard('kid').find((o) => o.target_uri === STRANGER);
99 const r = G.outgoing.decide(pending.id, local('mum'), 'approve', [local('mum')]);
100 assert.equal(r.outcome, 'approved');
101 assert.equal(await AP.gateOutgoingFollow(kid, STRANGER), null,
102 'the row stays behind as the record, so an unfollow and refollow is not a second question');
103});
104
105test('a refusal is remembered too, and does not re-ask by re-tapping', async () => {
106 const held = await AP.gateOutgoingFollow(kid, OLDPAL);
107 const r = G.outgoing.decide(held.id, local('mum'), 'reject', [local('mum')]);
108 assert.equal(r.outcome, 'rejected');
109 const again = await AP.gateOutgoingFollow(kid, OLDPAL);
110 assert.equal(again.status, 'denied', 'tapping follow again does not put it back in front of mum');
111});
112
113// ── De poort zit in followActor, niet alleen in C2S (Barts melding 8-8) ──
114//
115// Esmee's volgverzoeken kwamen nooit bij haar guardians aan. Niet omdat een
116// guardian elders ze niet kon beantwoorden -- die weg werkt -- maar omdat er
117// nooit een verzoek werd aangemaakt: de poort stond in `case 'Follow'` van de
118// C2S-outbox, en dus alleen als je via Shaer volgt. Vanuit Klonkts eigen
119// webinterface liep je er zo omheen.
120//
121// Dezelfde deur-naast-de-poort als bij de antwoordpoort (shaer-r4c), en de reden
122// dat die mutatie 0 fouten gaf: er stond niets op.
123
124test('een ward die vanaf het WEB volgt wordt ook tegengehouden', async () => {
125 const uit = await AP.followActor(kid, 'https://elders.example/users/webvriend');
126 assert.equal(uit.held, true, 'vastgehouden, niet gevolgd');
127 assert.equal(uit.status, 'pending');
128 // En er ligt echt iets voor de guardians, anders is "held" een leeg gebaar.
129 assert.ok(G.outgoing.findFor('kid', 'https://elders.example/users/webvriend'));
130});
131
132test('ook als het kind met een HANDLE volgt', async () => {
133 // De poort staat NA het oplossen. Zou hij alleen naar de ruwe invoer kijken,
134 // dan is elke @naam@server een sluiproute -- de fout die we hier repareren,
135 // een maat kleiner.
136 const uit = await AP.followActor(kid, 'https://elders.example/users/handlevriend');
137 assert.equal(uit.held, true);
138});
139
140test('een goedgekeurd verzoek komt er WEL doorheen', async () => {
141 // Zonder deze doorlaat stuit een goedgekeurd verzoek opnieuw op de poort en
142 // wacht het voor eeuwig -- de poort zou zichzelf voeden.
143 const uit = await AP.followActor(kid, 'https://elders.example/users/webvriend', false, { approved: true });
144 assert.notEqual(uit.held, true);
145});
146
147test('een site ZONDER guardians merkt er niets van', async () => {
148 // Een volwassen account is geen ward. Zou de poort daar ook dichtklappen, dan
149 // kan niemand op deze instance nog iemand volgen.
150 const vrij = site('vrij');
151 const uit = await AP.followActor(vrij, 'https://elders.example/users/iemand');
152 assert.notEqual(uit && uit.held, true);
153});
Note: See TracBrowser for help on using the repository browser.