| 1 | // De kaart op je EIGEN post (shaer-k3f): een URL in wat je publiceert wordt
|
|---|
| 2 | // een quote- of embed-snapshot, langs dezelfde pijplijn als een binnenkomende
|
|---|
| 3 | // post -- en de composer-preview loopt door diezelfde pijplijn, zodat de
|
|---|
| 4 | // preview nooit iets belooft dat de post niet waarmaakt.
|
|---|
| 5 | //
|
|---|
| 6 | // In-memory SQLite, gestubde fetch op TEST-NET-3. Run: npm test
|
|---|
| 7 | import { test } from 'node:test';
|
|---|
| 8 | import assert from 'node:assert/strict';
|
|---|
| 9 |
|
|---|
| 10 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 11 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 12 |
|
|---|
| 13 | const dbMod = await import('../src/config/database.js');
|
|---|
| 14 | const db = dbMod.default;
|
|---|
| 15 | dbMod.initializeDatabase();
|
|---|
| 16 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 17 |
|
|---|
| 18 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 19 | .run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 20 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'schrijver', 'Schrijver', 'u1');
|
|---|
| 21 | const site = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
|
|---|
| 22 |
|
|---|
| 23 | const EXTERN = 'https://203.0.113.60/artikel';
|
|---|
| 24 | const APNOTE = 'https://203.0.113.61/notes/toot-1';
|
|---|
| 25 | const APAUTEUR = 'https://203.0.113.61/users/schrijfster';
|
|---|
| 26 |
|
|---|
| 27 | const OG_PAGINA = `<!doctype html><html><head>
|
|---|
| 28 | <meta property="og:title" content="Een mooi artikel">
|
|---|
| 29 | <meta property="og:image" content="https://203.0.113.60/plaatje.jpg">
|
|---|
| 30 | </head><body>tekst</body></html>`;
|
|---|
| 31 |
|
|---|
| 32 | globalThis.fetch = async (url, opts = {}) => {
|
|---|
| 33 | const u = String(url);
|
|---|
| 34 | const accepts = String((opts.headers && (opts.headers.Accept || opts.headers.accept)) || '');
|
|---|
| 35 | const json = (o) => new Response(JSON.stringify(o), { status: 200, headers: { 'content-type': 'application/activity+json' } });
|
|---|
| 36 | const html = (h) => new Response(h, { status: 200, headers: { 'content-type': 'text/html' } });
|
|---|
| 37 | if (u === EXTERN) return html(OG_PAGINA); // geen AP, wel OpenGraph
|
|---|
| 38 | if (u === APNOTE) {
|
|---|
| 39 | // Een echte fediverse-note: op de AP-vraag AP-json, anders een kale pagina.
|
|---|
| 40 | if (accepts.includes('activity+json')) return json({ id: APNOTE, type: 'Note', attributedTo: APAUTEUR, content: '<p>de toot</p>', published: '2026-08-01T10:00:00Z' });
|
|---|
| 41 | return html('<html><body>toot</body></html>');
|
|---|
| 42 | }
|
|---|
| 43 | if (u === APAUTEUR) return json({ id: APAUTEUR, type: 'Person', preferredUsername: 'schrijfster', name: 'Schrijfster', inbox: `${APAUTEUR}/inbox` });
|
|---|
| 44 | return new Response('not found', { status: 404 });
|
|---|
| 45 | };
|
|---|
| 46 |
|
|---|
| 47 | test('een externe link in een eigen post wordt een embed-snapshot op de post', async () => {
|
|---|
| 48 | // PLATTE tekst, geen anker: zo komt een post uit de app binnen (het
|
|---|
| 49 | // C2S-pad bakt niet voor het opslaan). Op het toestel bleef precies dit
|
|---|
| 50 | // geval een kale link terwijl de web-editor wel een kaart kreeg.
|
|---|
| 51 | db.prepare("INSERT INTO posts (id, site_id, author_id, slug, title, content, status) VALUES ('p1','s1','u1','p1','', ?, 'published')")
|
|---|
| 52 | .run(`<p>Kijk: ${EXTERN}</p>`);
|
|---|
| 53 | const post = db.prepare("SELECT * FROM posts WHERE id = 'p1'").get();
|
|---|
| 54 | await AP.deliverCreate(site, post);
|
|---|
| 55 | const rij = db.prepare("SELECT quote_json, embed_json FROM posts WHERE id = 'p1'").get();
|
|---|
| 56 | assert.ok(rij.embed_json, 'embed-snapshot opgeslagen');
|
|---|
| 57 | const kaart = JSON.parse(rij.embed_json);
|
|---|
| 58 | assert.equal(kaart.title, 'Een mooi artikel');
|
|---|
| 59 | assert.ok(!rij.quote_json, 'geen quote: het is geen fediverse-object');
|
|---|
| 60 | });
|
|---|
| 61 |
|
|---|
| 62 | test('een fediverse-link wordt een quote-snapshot, geen embed', async () => {
|
|---|
| 63 | db.prepare("INSERT INTO posts (id, site_id, author_id, slug, title, content, status) VALUES ('p2','s1','u1','p2','', ?, 'published')")
|
|---|
| 64 | .run(`<p>Dit! <a href="${APNOTE}">${APNOTE}</a></p>`);
|
|---|
| 65 | const post = db.prepare("SELECT * FROM posts WHERE id = 'p2'").get();
|
|---|
| 66 | await AP.deliverCreate(site, post);
|
|---|
| 67 | const rij = db.prepare("SELECT quote_uri, quote_json, embed_json FROM posts WHERE id = 'p2'").get();
|
|---|
| 68 | assert.equal(rij.quote_uri, APNOTE);
|
|---|
| 69 | assert.ok(rij.quote_json, 'quote-snapshot opgeslagen');
|
|---|
| 70 | const kaart = JSON.parse(rij.quote_json);
|
|---|
| 71 | assert.ok(kaart.content.includes('de toot'));
|
|---|
| 72 | assert.equal(kaart.author.name, 'Schrijfster');
|
|---|
| 73 | assert.ok(!rij.embed_json, 'geen embed naast de quote');
|
|---|
| 74 | });
|
|---|
| 75 |
|
|---|
| 76 | test('previewCard geeft voor dezelfde twee links dezelfde twee kaarten', async () => {
|
|---|
| 77 | const extern = await AP.previewCard(EXTERN);
|
|---|
| 78 | assert.ok(extern.embedJson, 'externe link -> embed');
|
|---|
| 79 | assert.equal(JSON.parse(extern.embedJson).title, 'Een mooi artikel');
|
|---|
| 80 | assert.ok(!extern.quoteJson);
|
|---|
| 81 |
|
|---|
| 82 | const ap = await AP.previewCard(APNOTE);
|
|---|
| 83 | assert.ok(ap.quoteJson, 'fediverse-link -> quote');
|
|---|
| 84 | assert.equal(JSON.parse(ap.quoteJson).author.handle, '@schrijfster@203.0.113.61');
|
|---|
| 85 | assert.ok(!ap.embedJson);
|
|---|
| 86 |
|
|---|
| 87 | // Rommel is geen kaart en geen fout.
|
|---|
| 88 | assert.deepEqual(await AP.previewCard('geen url'), {});
|
|---|
| 89 | });
|
|---|
| 90 |
|
|---|
| 91 |
|
|---|
| 92 | test('actorInfo kiest de html-Link uit een url-array (geen object-stringing)', async () => {
|
|---|
| 93 | // Barts vondst (8-8): buildActor serveert `url` als array van Links
|
|---|
| 94 | // (profiel + RSS), en een consument die dat niet kent stringde de array tot
|
|---|
| 95 | // "[object Object],[object Object]" in de mention-href van een HULPVRAAG.
|
|---|
| 96 | // De thread-byline loopt door dezelfde actorInfo, dus die legt het vast.
|
|---|
| 97 | const HOST = 'https://203.0.113.62';
|
|---|
| 98 | const NOTE = `${HOST}/notes/n1`;
|
|---|
| 99 | const AUTEUR = `${HOST}/users/array`;
|
|---|
| 100 | const vorige = globalThis.fetch;
|
|---|
| 101 | globalThis.fetch = async (url) => {
|
|---|
| 102 | const u = String(url);
|
|---|
| 103 | const json = (o) => new Response(JSON.stringify(o), { status: 200, headers: { 'content-type': 'application/activity+json' } });
|
|---|
| 104 | if (u === NOTE) return json({
|
|---|
| 105 | id: NOTE, type: 'Note', attributedTo: AUTEUR, content: '<p>bron</p>',
|
|---|
| 106 | replies: { type: 'Collection', first: { type: 'CollectionPage', items: [
|
|---|
| 107 | { id: `${NOTE}/r1`, type: 'Note', attributedTo: AUTEUR, inReplyTo: NOTE, content: '<p>antwoord</p>', published: '2026-08-02T10:00:00Z' },
|
|---|
| 108 | ] } },
|
|---|
| 109 | });
|
|---|
| 110 | if (u === AUTEUR) return json({
|
|---|
| 111 | id: AUTEUR, type: 'Person', preferredUsername: 'array', inbox: `${AUTEUR}/inbox`,
|
|---|
| 112 | url: [
|
|---|
| 113 | { type: 'Link', href: `${HOST}/`, mediaType: 'text/html' },
|
|---|
| 114 | { type: 'Link', href: `${HOST}/feed.xml`, mediaType: 'application/rss+xml' },
|
|---|
| 115 | ],
|
|---|
| 116 | });
|
|---|
| 117 | return vorige(url);
|
|---|
| 118 | };
|
|---|
| 119 | const uit = await AP.getThread('schrijver', NOTE);
|
|---|
| 120 | globalThis.fetch = vorige;
|
|---|
| 121 | assert.equal(uit.notes.length, 1);
|
|---|
| 122 | const auteur = uit.notes[0]['shaer:author'];
|
|---|
| 123 | assert.equal(auteur.url, `${HOST}/`, 'de html-Link, niet de array gestringd');
|
|---|
| 124 | assert.ok(!String(auteur.url).includes('object'), 'nergens [object Object]');
|
|---|
| 125 | });
|
|---|