| 1 | // Standaardvormen in plaats van eigen dialect (shaer-nmw).
|
|---|
| 2 | //
|
|---|
| 3 | // Robins waarschuwing: geen Klonkt/Shaer-dialect schrijven waar AS2 of een FEP
|
|---|
| 4 | // het al regelt. Deze toetsen zijn het criterium uit die bead -- niet "onze app
|
|---|
| 5 | // snapt het", maar: HEEFT EEN GENERIEKE AP-LEZER ER IETS AAN. Daarom kijken ze
|
|---|
| 6 | // alleen naar standaardvelden en nooit naar een shaer:-property.
|
|---|
| 7 | //
|
|---|
| 8 | // In-memory SQLite. Run: npm test
|
|---|
| 9 | import { test } from 'node:test';
|
|---|
| 10 | import assert from 'node:assert/strict';
|
|---|
| 11 |
|
|---|
| 12 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 13 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 14 |
|
|---|
| 15 | const dbMod = await import('../src/config/database.js');
|
|---|
| 16 | dbMod.initializeDatabase();
|
|---|
| 17 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 18 |
|
|---|
| 19 | test('attributedTo draagt de actor als object, zodat elke client een byline heeft', () => {
|
|---|
| 20 | const uit = AP.actorObject('https://elders/u/tante', {
|
|---|
| 21 | name: 'Tante Til', handle: '@til@elders', icon: 'https://elders/til.png',
|
|---|
| 22 | url: 'https://elders/@til', emojis: { ':zwaai:': 'https://elders/zwaai.png' },
|
|---|
| 23 | });
|
|---|
| 24 | assert.equal(uit.id, 'https://elders/u/tante');
|
|---|
| 25 | assert.equal(uit.type, 'Person');
|
|---|
| 26 | assert.equal(uit.name, 'Tante Til');
|
|---|
| 27 | // De LOKALE naam, zoals AS2 hem bedoelt; de handle leidt een lezer af uit
|
|---|
| 28 | // preferredUsername plus de host van de id.
|
|---|
| 29 | assert.equal(uit.preferredUsername, 'til');
|
|---|
| 30 | assert.deepEqual(uit.icon, { type: 'Image', url: 'https://elders/til.png' });
|
|---|
| 31 | // FEP-9098 hoort in tag, niet in een eigen emoji-kaart.
|
|---|
| 32 | assert.equal(uit.tag[0].type, 'Emoji');
|
|---|
| 33 | assert.equal(uit.tag[0].name, ':zwaai:');
|
|---|
| 34 | });
|
|---|
| 35 |
|
|---|
| 36 | test('weten we niets van de persoon, dan blijft attributedTo de kale URI', () => {
|
|---|
| 37 | // Een leeg object zou beweren dat we hem kennen. Een URI is eerlijk en is
|
|---|
| 38 | // bovendien wat elke AP-server verwacht.
|
|---|
| 39 | assert.equal(AP.actorObject('https://elders/u/onbekend', undefined), 'https://elders/u/onbekend');
|
|---|
| 40 | assert.equal(AP.actorObject('https://elders/u/onbekend', {}), 'https://elders/u/onbekend');
|
|---|
| 41 | });
|
|---|
| 42 |
|
|---|
| 43 | test('een linkkaart is een AS2 preview met url, name en image', () => {
|
|---|
| 44 | const embed = JSON.stringify({
|
|---|
| 45 | url: 'https://nieuws.example/artikel', title: 'Een mooi artikel',
|
|---|
| 46 | author: { name: 'De Krant' }, media: [{ url: 'https://nieuws.example/p.jpg' }],
|
|---|
| 47 | });
|
|---|
| 48 | const p = AP.previewObject(embed, { playback: false });
|
|---|
| 49 | assert.equal(p.type, 'Page');
|
|---|
| 50 | assert.equal(p.url, 'https://nieuws.example/artikel');
|
|---|
| 51 | assert.equal(p.name, 'Een mooi artikel');
|
|---|
| 52 | assert.deepEqual(p.image, { type: 'Image', url: 'https://nieuws.example/p.jpg' });
|
|---|
| 53 | assert.equal(p.attributedTo.name, 'De Krant');
|
|---|
| 54 | // De spelerpagina blijft van ons: die hangt aan de guardian-poort en heeft
|
|---|
| 55 | // geen AS2-tegenhanger. Dicht = niet aanwezig.
|
|---|
| 56 | assert.ok(!p['shaer:playerUrl']);
|
|---|
| 57 | });
|
|---|
| 58 |
|
|---|
| 59 | test('een quote is de geciteerde Note zelf (FEP-044f), met ingesloten auteur', () => {
|
|---|
| 60 | const snapshot = JSON.stringify({
|
|---|
| 61 | url: 'https://elders/notes/1',
|
|---|
| 62 | author: { name: 'Til', handle: '@til@elders', icon: 'https://elders/til.png' },
|
|---|
| 63 | content: '<p>hallo</p>', published: '2026-08-01T10:00:00Z',
|
|---|
| 64 | media: [{ url: 'https://elders/foto.jpg', type: 'image/jpeg' }],
|
|---|
| 65 | });
|
|---|
| 66 | const q = AP.quoteObject(snapshot);
|
|---|
| 67 | assert.equal(q.type, 'Note');
|
|---|
| 68 | assert.equal(q.id, 'https://elders/notes/1');
|
|---|
| 69 | assert.equal(q.content, '<p>hallo</p>');
|
|---|
| 70 | assert.equal(q.published, '2026-08-01T10:00:00Z');
|
|---|
| 71 | // De auteur van het citaat is zelf ook een ingesloten actor, niet een
|
|---|
| 72 | // losse naam-kaart.
|
|---|
| 73 | assert.equal(q.attributedTo.type, 'Person');
|
|---|
| 74 | assert.equal(q.attributedTo.name, 'Til');
|
|---|
| 75 | assert.equal(q.attributedTo.preferredUsername, 'til');
|
|---|
| 76 | assert.deepEqual(q.attachment, [{ type: 'Document', mediaType: 'image/jpeg', url: 'https://elders/foto.jpg' }]);
|
|---|
| 77 | });
|
|---|
| 78 |
|
|---|
| 79 | test('niets te tonen levert niets op, geen leeg omhulsel', () => {
|
|---|
| 80 | assert.equal(AP.previewObject(null), undefined);
|
|---|
| 81 | assert.equal(AP.previewObject('{}'), undefined);
|
|---|
| 82 | assert.equal(AP.quoteObject(null), undefined);
|
|---|
| 83 | assert.equal(AP.quoteObject('rommel'), undefined);
|
|---|
| 84 | });
|
|---|
| 85 |
|
|---|
| 86 | // ── En dan het weghalen (shaer-nmw, tweede helft) ────────────────────
|
|---|
| 87 | //
|
|---|
| 88 | // Bewijzen dat de standaardvorm ERBIJ staat is de helft; de andere helft is dat
|
|---|
| 89 | // het dialect er niet meer NAAST staat. Zonder deze wacht sluipt een
|
|---|
| 90 | // shaer:author zo weer een nieuwe leg in en spreken we stilletjes weer onze
|
|---|
| 91 | // eigen taal. Daarom door de echte route, want daar wordt de vorm beslist.
|
|---|
| 92 | test('de C2S-leg spreekt geen dialect meer waar de standaard het regelt', async (t) => {
|
|---|
| 93 | const crypto = await import('crypto');
|
|---|
| 94 | const express = (await import('express')).default;
|
|---|
| 95 | const db = (await import('../src/config/database.js')).default;
|
|---|
| 96 | const routes = (await import('../src/routes/activitypub.js')).default;
|
|---|
| 97 |
|
|---|
| 98 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 99 | .run('u1', 'u1', 'u1@t', 'x', 'god');
|
|---|
| 100 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)')
|
|---|
| 101 | .run('s1', 'kind', 'kind', 'u1');
|
|---|
| 102 | db.prepare(`INSERT INTO ap_timeline (id, slug, author_uri, author_name, author_handle, author_icon,
|
|---|
| 103 | reblog_name, reblog_handle, content)
|
|---|
| 104 | VALUES ('https://elders/n1', 'kind', 'https://elders/u/til', 'Tante Til', '@til@elders',
|
|---|
| 105 | 'https://elders/til.png', 'Esmee', '@es@elders', '<p>hoi</p>')`).run();
|
|---|
| 106 |
|
|---|
| 107 | const bearer = 'test-token-' + 'b'.repeat(24);
|
|---|
| 108 | db.prepare('INSERT INTO oauth_tokens (token_hash, client_id, user_id, site_slug, scope) VALUES (?,?,?,?,?)')
|
|---|
| 109 | .run(crypto.createHash('sha256').update(bearer).digest('base64url'), 'c', 'u1', 'kind', 'read write');
|
|---|
| 110 |
|
|---|
| 111 | const app = express();
|
|---|
| 112 | app.use(routes);
|
|---|
| 113 | const server = app.listen(0);
|
|---|
| 114 | t.after(() => server.close());
|
|---|
| 115 | await new Promise((r) => server.once('listening', r));
|
|---|
| 116 | const doc = await (await fetch(`http://127.0.0.1:${server.address().port}/ap/users/kind/inbox`,
|
|---|
| 117 | { headers: { Authorization: `Bearer ${bearer}` } })).json();
|
|---|
| 118 |
|
|---|
| 119 | const tekst = JSON.stringify(doc);
|
|---|
| 120 | // Op de SLEUTEL, niet op een stuk tekst: 'shaer:quote' zit ook in
|
|---|
| 121 | // shaer:quoteCards, en dat is een poortnaam die blijft. Een te grove zeef
|
|---|
| 122 | // zou hier rood staan om iets dat klopt -- of erger, groen om iets dat niet
|
|---|
| 123 | // klopt.
|
|---|
| 124 | for (const weg of ['shaer:author', 'shaer:booster', 'shaer:quote', 'shaer:embed']) {
|
|---|
| 125 | assert.ok(!tekst.includes(`"${weg}":`), `${weg} hoort weg te zijn`);
|
|---|
| 126 | }
|
|---|
| 127 | // Wat WEL mag blijven, en waarom: per-lezer-interactiestatus heeft in AS2
|
|---|
| 128 | // geen tegenhanger (Mastodon lost het in zijn eigen REST-API op), en de
|
|---|
| 129 | // FEP-633c-termen zijn bewust van ons en in standaardisatie.
|
|---|
| 130 | assert.ok(tekst.includes('shaer:liked'), 'interactiestatus blijft, die heeft geen standaard');
|
|---|
| 131 |
|
|---|
| 132 | const item = doc.orderedItems.find((i) => i.object.id === 'https://elders/n1');
|
|---|
| 133 | assert.equal(item.type, 'Announce', 'een boost is de wrapper, niet een zijkanaal');
|
|---|
| 134 | assert.equal(item.object.attributedTo.name, 'Tante Til', 'en de byline zit in attributedTo');
|
|---|
| 135 | assert.equal(item.object.attributedTo.preferredUsername, 'til');
|
|---|
| 136 | });
|
|---|