source: Klonkt/test/activitypub-as2.test.js

main
Last change on this file was 21257e4, checked in by Robin <roboburr@…>, 3 weeks ago

Test: @container is een JSON-LD-sleutelwoord, geen vocabulaire

De AS2-test loopt ook door @context zelf, en een termdefinitie mag naast
@id en @type een container dragen -- artist_credit is bij Funkwhale een
@list. Hij viel dus over een correcte declaratie.

Deze regel had bij de vorige commit moeten zitten. Ik ketende
node --test | grep met && aan het uitrollen, en grep slaagt ook als er
"not ok" in staat: de keten liep door en er stond drie tests lang een
rode build op dev. De uitkomst van een test lees je van zijn exitcode af,
niet van of er tekst uit komt.

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

  • Property mode set to 100644
File size: 6.5 KB
Line 
1// AS2 / JSON-LD validity guard.
2//
3// Every property key and `type` value our ActivityPub objects emit MUST be either an AS2-core
4// (or security/v1) term OR declared in the federation @context (AP_CONTEXT). A new feature that
5// emits an undeclared term fails this test → declare it in AP_CONTEXT (extensions) or add it to
6// the AS2 allowlist below. This keeps Klonkt's output valid AS2/JSON-LD forever — not just
7// "Mastodon tolerates it". No extra deps; in-memory SQLite. Run: npm test
8import { test } from 'node:test';
9import assert from 'node:assert/strict';
10
11process.env.DATABASE_PATH = ':memory:';
12process.env.PUBLIC_BASE_URL = 'https://test.example';
13
14const dbMod = await import('../src/config/database.js');
15const db = dbMod.default;
16dbMod.initializeDatabase();
17const AP = (await import('../src/services/ActivityPubService.js')).default;
18
19const BASE = 'https://test.example';
20
21// AS2-core + security/v1 vocabulary Klonkt uses (stable — only extend when AS2/security itself
22// adds a term we adopt). JSON-LD keywords included.
23const AS2 = new Set([
24 // `@container` hoort in dit rijtje omdat de verzamelaar hieronder OOK door
25 // @context zelf loopt, en een termdefinitie mag naast @id en @type ook een
26 // container dragen (artist_credit is bij Funkwhale een @list). Het is een
27 // JSON-LD-sleutelwoord, geen vocabulaire -- die laatste is wat deze test moet
28 // vangen. Zonder deze regel faalde hij op een correcte declaratie.
29 '@context', '@id', '@type', '@container',
30 'id', 'type', 'actor', 'object', 'target', 'to', 'cc',
31 'content', 'name', 'summary', 'url', 'href', 'mediaType',
32 'published', 'updated', 'attributedTo', 'inReplyTo', 'replies',
33 // AS2-kern: de context waarbinnen een object bestaat. Een track wijst ermee
34 // naar de post die hem uitbrengt (shaer-0nh).
35 'context',
36 'attachment', 'tag', 'icon', 'image', 'duration',
37 'contentMap', 'nameMap', 'summaryMap', // AS2 @language-map counterparts of content/name/summary
38 'totalItems', 'orderedItems', 'items', 'first', 'last', 'partOf', 'next', 'prev',
39 'preferredUsername', 'inbox', 'outbox', 'followers', 'following', 'endpoints', 'sharedInbox',
40 // ActivityPub §5.6: the private blocked collection (owner-only GET).
41 'blocked',
42 // ActivityPub §4.1: supplementary collections on the actor — Klonkt wijst
43 // ermee naar de playlist-lijst (shaer-ayc).
44 'streams',
45 // FEP-633c (Guardians): the owner-only dashboard queues on the actor; the
46 // sub-keys are the daemon-contract collection names the Shaer clients read.
47 // `guardians` is the availability queue (3.6.1: never public, owner-only).
48 // `outgoingFollows` is §5.3 turned around: the ward's own follow requests,
49 // waiting for the guardians (shaer-p729).
50 // `help` is de vragenlijst met haar STAAT (5.2.1): de apps lazen hulpvragen uit
51 // de feed en wisten niet of er al iemand op af was.
52 'shaer:queues', 'offers', 'follows', 'outgoingFollows', 'wards', 'guardians',
53 // §4.2: het logboek staat NAAST de wachtrijen, want het wacht nergens op.
54 'shaer:log', 'help',
55 // Elke OPEN hulpvraag zit in de collectie; daarom mag een app uit afwezigheid
56 // concluderen dat iets niet open is (shaer-6wt).
57 'shaer:openComplete',
58 // ActivityPub §4.1 `endpoints` vocabulary (same category as sharedInbox), used for C2S.
59 'oauthAuthorizationEndpoint', 'oauthTokenEndpoint', 'uploadMedia',
60 'publicKey', 'owner', 'publicKeyPem',
61 'Note', 'Person', 'Create', 'Update', 'Delete', 'Tombstone', 'Announce', 'Like', 'Follow',
62 'Accept', 'Reject', 'Undo', 'Add', 'Remove', 'Flag', 'Document', 'Image', 'Audio', 'Video',
63 'Mention', 'Link', 'Collection', 'OrderedCollection', 'OrderedCollectionPage',
64]);
65
66// The extension terms = exactly the keys declared in AP_CONTEXT's term-definition object.
67const ctxTerms = new Set();
68for (const part of AP.AP_CONTEXT) if (part && typeof part === 'object') for (const k of Object.keys(part)) ctxTerms.add(k);
69const allowed = new Set([...AS2, ...ctxTerms]);
70
71// Collect every property key + every `type` string value, recursively.
72function collect(obj, keys = new Set()) {
73 if (Array.isArray(obj)) { for (const x of obj) collect(x, keys); return keys; }
74 if (obj && typeof obj === 'object') {
75 for (const [k, v] of Object.entries(obj)) {
76 keys.add(k);
77 if (k === 'type' && typeof v === 'string') keys.add(v);
78 if (/Map$/.test(k)) continue; // a @language map (contentMap/…): its keys are BCP-47 tags, not vocab terms
79 collect(v, keys);
80 }
81 }
82 return keys;
83}
84function assertValid(obj, label) {
85 const undeclared = [...collect(obj)].filter((k) => !allowed.has(k));
86 assert.deepEqual(undeclared, [],
87 `${label}: undeclared AS2/JSON-LD term(s) — declare in AP_CONTEXT (extension) or the AS2 allowlist: ${undeclared.join(', ')}`);
88}
89
90// Seed one site that exercises the extension-heavy actor fields (profile links → PropertyValue,
91// photo → icon, primary → featured).
92db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
93db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary, profile_links, profile_photo) VALUES (?,?,?,?,?,?,?)')
94 .run('s1', 'demo', 'Demo', 'u1', 1, JSON.stringify([{ platform: 'website', url: 'https://x.test' }]), '/media/x.png');
95const site = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
96site.primary_slug = 'demo';
97
98// Kitchen-sink note: nsfw (→ sensitive + summary), a hashtag (→ Hashtag), a cover (→ attachment).
99const post = {
100 id: 'p1', slug: 'hello', title: 'Hi', content: '<p>hello #music</p>',
101 nsfw: 1, content_warning: 'cw', tags: JSON.stringify(['mood']),
102 cover_image_url: '/media/c.webp', cover_alt: 'A cover', language: 'en',
103 published_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z',
104};
105
106test('actor is valid AS2 (every term declared)', () => assertValid(AP.buildActor(BASE, site), 'actor'));
107test('create+note is valid AS2 (every term declared)', () => assertValid(AP.buildCreate(BASE, site, post), 'create/note'));
108test('outbox/followers/featured collections are valid AS2', () => {
109 assertValid(AP.buildOutbox(BASE, site, [post]), 'outbox');
110 assertValid(AP.buildFollowers(BASE, site, 3), 'followers');
111 assertValid(AP.buildFollowing(BASE, site, 2), 'following');
112 assertValid(AP.buildFeatured(BASE, site, [post]), 'featured');
113});
114test('AP_CONTEXT declares every extension term we rely on', () => {
115 for (const t of ['sensitive', 'Hashtag', 'manuallyApprovesFollowers', 'discoverable', 'featured', 'PropertyValue', 'embedUrl'])
116 assert.ok(ctxTerms.has(t), `AP_CONTEXT must declare "${t}"`);
117});
Note: See TracBrowser for help on using the repository browser.