// One embed pipeline (shaer-277): AP first (FEP semantics), then a known
// provider, then oEmbed, then a plain link. All four end in the same shape.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
resolveEmbed, findOEmbedEndpoint, looksLikeAPObject, fromOEmbed,
} from '../src/services/EmbedResolver.js';
const AP_NOTE = { id: 'https://s/objects/1', type: 'Note', content: '
hi
', attributedTo: 'https://s/users/alice', url: 'https://s/@alice/1' };
const OEMBED_PAGE = 't';
const OEMBED_JSON = { type: 'video', title: 'A talk', author_name: 'Ada', provider_name: 'Vid', html: '', thumbnail_url: 'https://v.example/t.jpg' };
const io = (over = {}) => ({
getAP: async () => null,
getPage: async () => null,
getJSON: async () => null,
actorOf: async () => ({ name: 'Alice', handle: '@alice@s', icon: null }),
provider: () => null,
...over,
});
test('findOEmbedEndpoint reads the json+oembed link and decodes entities', () => {
assert.equal(findOEmbedEndpoint(OEMBED_PAGE), 'https://v.example/oembed?url=x&f=json');
assert.equal(findOEmbedEndpoint(''), null);
// the XML flavour is not parsed
assert.equal(findOEmbedEndpoint(''), null);
});
test('looksLikeAPObject accepts quotable content, rejects actors and activities', () => {
assert.ok(looksLikeAPObject(AP_NOTE));
assert.ok(looksLikeAPObject({ id: 'https://s/v/1', type: 'Video' }));
assert.ok(!looksLikeAPObject({ id: 'https://s/users/a', type: 'Person' }));
assert.ok(!looksLikeAPObject({ id: 'https://s/a/1', type: 'Create' }));
assert.ok(!looksLikeAPObject({ type: 'Note' })); // no id
assert.ok(!looksLikeAPObject(null));
});
test('an ActivityPub object resolves over AP, never over oEmbed', async () => {
let pageFetched = false;
const r = await resolveEmbed('https://s/@alice/1', io({
getAP: async () => AP_NOTE,
getPage: async () => { pageFetched = true; return OEMBED_PAGE; },
getJSON: async () => OEMBED_JSON,
provider: () => ({ provider: 'youtube', id: 'x' }), // must not win either
}));
assert.equal(r.kind, 'ap');
assert.equal(r.id, 'https://s/objects/1');
assert.equal(r.attributedTo, 'https://s/users/alice');
assert.equal(r.author.handle, '@alice@s');
assert.equal(r.url, 'https://s/@alice/1');
assert.ok(!pageFetched, 'AP wins before any oEmbed discovery happens');
});
// Regression: a hardcoded provider list used to short-circuit here and return a
// card with no title and no thumbnail, so a YouTube link ended up storing
// nothing at all. There is no provider list any more; every non-fediverse URL
// takes the generic path, which is exactly what gives it a thumbnail.
test('a video host is not special-cased and still gets a real card', async () => {
const r = await resolveEmbed('https://youtu.be/abcdefghijk', io({
provider: () => ({ provider: 'youtube', id: 'abcdefghijk' }), // ignored on purpose
getPage: async () => OEMBED_PAGE,
getJSON: async () => OEMBED_JSON,
}));
assert.equal(r.kind, 'oembed');
assert.equal(r.title, 'A talk');
assert.ok(r.media[0].url, 'and it has a thumbnail, which the old path never produced');
});
test('anything else goes through oEmbed discovery', async () => {
const r = await resolveEmbed('https://v.example/watch/1', io({
getPage: async () => OEMBED_PAGE,
getJSON: async () => OEMBED_JSON,
}));
assert.equal(r.kind, 'oembed');
assert.equal(r.title, 'A talk');
assert.equal(r.author.name, 'Ada');
assert.equal(r.provider, 'Vid');
assert.equal(r.media[0].url, 'https://v.example/t.jpg');
assert.ok(r.html.startsWith('