// De Klonkt-actor als kanaal, en Audio als eigen soort in de tijdlijn
// (shaer-0nh, Robins besluit 7-8).
//
// Twee kanten. Uitgaand: de actor adverteert zijn webpagina en zijn RSS-feed
// als Link-array, met een category zodra er muziek is. Inkomend: een
// Create(Audio) van een kanaal wordt een tijdlijnrij met een echte speler --
// zonder de Audio tot Note om te vormen, want die soort willen we houden.
import { test } from 'node:test';
import assert from 'node:assert/strict';
process.env.DATABASE_PATH = ':memory:';
process.env.PUBLIC_BASE_URL = 'https://test.example';
const dbMod = await import('../src/config/database.js');
const db = dbMod.default;
dbMod.initializeDatabase();
const AP = (await import('../src/services/ActivityPubService.js')).default;
const BASE = 'https://test.example';
db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
.run('u1', 'u1', 'u1@t', 'x', 'god');
db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
.run('s1', 'band', 'De Band', 'u1');
const site = () => db.prepare("SELECT s.*, (SELECT slug FROM sites WHERE is_primary = 1) AS primary_slug FROM sites s WHERE s.id = 's1'").get();
test('de actor draagt zijn webpagina en zijn RSS-feed als Link-array', () => {
const a = AP.buildActor(BASE, site());
assert.ok(Array.isArray(a.url), 'url is een array, geen string');
assert.deepEqual(a.url[0], { type: 'Link', href: 'https://test.example/', mediaType: 'text/html' },
'de webpagina staat VOORAAN: wie er maar een verwacht, pakt de eerste');
assert.deepEqual(a.url[1], { type: 'Link', href: 'https://test.example/feed.xml', mediaType: 'application/rss+xml' });
});
test('zonder muziek geen category -- liever geen label dan een verkeerd label', () => {
assert.equal(AP.buildActor(BASE, site()).category, undefined);
});
test('met muziek is de category "music"', () => {
db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)')
.run('m1', 's1', 'a.mp3', 'audio/a.mp3', 'audio/mpeg');
db.prepare('INSERT INTO audio_tracks (id, site_id, title, media_id) VALUES (?,?,?,?)')
.run('t1', 's1', 'Een nummer', 'm1');
assert.equal(AP.buildActor(BASE, site()).category, 'music');
});
// ── inkomend ────────────────────────────────────────────────────────────────
const AUDIO = {
id: 'https://audio.example/federation/music/tracks/1',
type: 'Audio',
name: '0METAL MARIO - PINK - 140 bpm',
duration: 'PT103S',
published: '2026-08-07T12:00:00Z',
image: { type: 'Image', url: 'https://audio.example/covers/1.jpg', mediaType: 'image/jpeg' },
url: [
{ type: 'Link', href: 'https://audio.example/library/tracks/1', mediaType: 'text/html' },
{ type: 'Link', href: 'https://audio.example/media/1.mp3', mediaType: 'audio/mpeg' },
],
};
test('een Audio blijft een Audio en levert een speler plus hoes', () => {
const f = AP.timelineFields(AUDIO);
assert.equal(AUDIO.type, 'Audio', 'het bronobject is niet tot Note omgevormd');
assert.equal(f.url, 'https://audio.example/library/tracks/1', 'de text/html-link is de link van het item');
assert.deepEqual(f.atts, [
{ url: 'https://audio.example/covers/1.jpg', type: 'image/jpeg' },
{ url: 'https://audio.example/media/1.mp3', type: 'audio/mpeg' },
], 'eerst kijken, dan luisteren -- en audio/* maakt er in de Krant een speler van');
assert.match(f.html, /0METAL MARIO/, 'de titel is de inhoud; een Audio heeft geen content');
});
test('een titel met HTML erin gaat door de sanitizer', () => {
const f = AP.timelineFields({ ...AUDIO, name: 'Track ' });
assert.ok(!/