source: Klonkt/test/object-links.test.js@ 3ccca13

main
Last change on this file since 3ccca13 was 6fd0e20, checked in by Robin Genis <roboburr@…>, 6 weeks ago

FEP-044f: Klonkt resolvet de geciteerde post voor een ingebedde quote-kaart

De client kan remote AP-objecten niet gesigneerd ophalen, dus Klonkt resolvet de
quote emit-side tot een compacte, gesaneerde snapshot en serveert die als
shaer:quote op de C2S inbox-read. De client rendert daarmee een geneste kaart
(auteur + avatar + tekst + evt. thumbnail) i.p.v. alleen de link-chip.

resolveQuote(note) haalt de quoted post op (SSRF-safe apGetJson), pakt de auteur
(fetchActor + actorInfo) en saneert de content met dezelfde sanitizer als elke
andere note (kindveilig blijft gelden). Best-effort: faalt de fetch, dan blijft
quote_json leeg en valt de client terug op de chip.

Bron-URL via quoteHrefOf: object-level quote (FEP-044f) of een quote-rel
FEP-e232 Link. Resolutie op inbound (fire-and-forget, blokkeert de inbox-response
niet), op outbox-backfill (await) en in self-heal (v10 -> v11, met COALESCE-gedrag
zodat een tijdelijk onbereikbare quoted post de cache niet leegt).

Changed files:
src/config/database.js

  • ap_timeline.quote_json kolom

src/services/ActivityPubService.js

  • quoteHrefOf(note): quoted-post-URL uit object-quote of quote-rel Link
  • resolveQuote(note): async snapshot {url, author, content, published, media}
  • timelineQuote(quoteJson): snapshot terug voor de inbox-read
  • inbound (fire-and-forget), backfill (await), self-heal v11 (await + COALESCE)
  • timelineQuote in de default-export

src/routes/activitypub.js

  • inbox-read serveert shaer:quote (de geresolvede snapshot)

test/object-links.test.js

  • quoteHrefOf (object-quote + quote-rel Link + geen) en timelineQuote round-trip

remarks: 180 tests groen (was 178). resolveQuote zelf is netwerk, niet in de unit-test.

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

  • Property mode set to 100644
File size: 4.5 KB
Line 
1// FEP-e232: Klonkt keeps inbound object-link (quote/ref) tags and serves them on the C2S inbox read.
2import { test } from 'node:test';
3import assert from 'node:assert/strict';
4process.env.DATABASE_PATH = ':memory:';
5process.env.PUBLIC_BASE_URL = 'https://test.example';
6const dbMod = await import('../src/config/database.js');
7dbMod.initializeDatabase();
8const { extractObjectLinkTags, timelineObjectLinks, extractQuoteUrl, extractLinkJson, quoteHrefOf, timelineQuote } = await import('../src/services/ActivityPubService.js');
9const AP = { extractObjectLinkTags, timelineObjectLinks, extractQuoteUrl, extractLinkJson, quoteHrefOf, timelineQuote };
10
11test('extractObjectLinkTags keeps AS2-profiled ld+json and activity+json Links; drops plain links and mentions', () => {
12 const tag = [
13 { type: 'Link', mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', href: 'https://s/objects/1', name: '#1374' },
14 { type: 'Link', mediaType: 'application/activity+json', href: 'https://s/objects/2', rel: ['https://misskey-hub.net/ns#_misskey_quote'] },
15 { type: 'Link', mediaType: 'text/html', href: 'https://plain.example/page' }, // plain hyperlink → dropped
16 { type: 'Mention', href: 'https://s/u/x' },
17 ];
18 const json = AP.extractObjectLinkTags(tag);
19 assert.ok(json);
20 const back = AP.timelineObjectLinks(json);
21 assert.equal(back.length, 2);
22 assert.equal(back[0].href, 'https://s/objects/1');
23 assert.equal(back[0].name, '#1374');
24 assert.equal(back[1].mediaType, 'application/activity+json');
25});
26
27test('no object links → null / undefined (nothing served)', () => {
28 assert.equal(AP.extractObjectLinkTags([{ type: 'Link', mediaType: 'text/html', href: 'https://x' }]), null);
29 assert.equal(AP.extractObjectLinkTags([{ type: 'Hashtag', name: '#hi' }]), null);
30 assert.equal(AP.timelineObjectLinks(null), undefined);
31 assert.equal(AP.timelineObjectLinks('not json'), undefined);
32});
33
34// FEP-044f: object-level quote properties are the common representation.
35test('extractQuoteUrl reads quote / quoteUrl / quoteUri / _misskey_quote (string or embedded object)', () => {
36 assert.equal(AP.extractQuoteUrl({ quote: 'https://s/objects/9' }), 'https://s/objects/9');
37 assert.equal(AP.extractQuoteUrl({ quoteUrl: 'https://s/q1' }), 'https://s/q1');
38 assert.equal(AP.extractQuoteUrl({ quoteUri: 'https://s/q2' }), 'https://s/q2');
39 assert.equal(AP.extractQuoteUrl({ _misskey_quote: 'https://s/q3' }), 'https://s/q3');
40 assert.equal(AP.extractQuoteUrl({ quote: { type: 'Link', href: 'https://s/q4' } }), 'https://s/q4');
41 assert.equal(AP.extractQuoteUrl({ content: 'no quote' }), null);
42});
43
44test('extractLinkJson normalises an object-level quote into one FEP-e232 Link (rel _misskey_quote)', () => {
45 const json = AP.extractLinkJson({ content: 'nice', quoteUrl: 'https://s/objects/9' });
46 const arr = AP.timelineObjectLinks(json);
47 assert.equal(arr.length, 1);
48 assert.equal(arr[0].href, 'https://s/objects/9');
49 assert.ok(arr[0].rel.some((r) => r.includes('quote')));
50});
51
52test('extractLinkJson merges a real FEP-e232 Link with an object-level quote, deduped by href', () => {
53 const note = {
54 tag: [{ type: 'Link', mediaType: 'application/activity+json', href: 'https://s/objects/9' }],
55 quoteUrl: 'https://s/objects/9', // same target → not duplicated
56 };
57 const arr = AP.timelineObjectLinks(AP.extractLinkJson(note));
58 assert.equal(arr.length, 1);
59 assert.equal(arr[0].href, 'https://s/objects/9');
60});
61
62// FEP-044f embedded quote card: the quoted-post URL feeds the resolver.
63test('quoteHrefOf reads an object-level quote or a quote-rel FEP-e232 Link', () => {
64 assert.equal(AP.quoteHrefOf({ quoteUrl: 'https://s/q1' }), 'https://s/q1');
65 assert.equal(AP.quoteHrefOf({
66 tag: [{ type: 'Link', href: 'https://s/q2', rel: 'https://misskey-hub.net/ns#_misskey_quote' }],
67 }), 'https://s/q2');
68 assert.equal(AP.quoteHrefOf({ content: 'plain, no quote' }), null);
69 // a plain (non-quote) FEP-e232 reference is not a quote target
70 assert.equal(AP.quoteHrefOf({ tag: [{ type: 'Link', href: 'https://s/ref', rel: 'mention' }] }), null);
71});
72
73test('timelineQuote round-trips the stored snapshot; junk → undefined', () => {
74 const snap = JSON.stringify({ url: 'https://s/q', author: { name: 'A', handle: '@a@s', icon: null }, content: '<p>hi</p>' });
75 const back = AP.timelineQuote(snap);
76 assert.equal(back.url, 'https://s/q');
77 assert.equal(back.author.handle, '@a@s');
78 assert.equal(AP.timelineQuote(null), undefined);
79 assert.equal(AP.timelineQuote('not json'), undefined);
80});
Note: See TracBrowser for help on using the repository browser.