| 1 | // De Klonkt-actor als kanaal, en Audio als eigen soort in de tijdlijn
|
|---|
| 2 | // (shaer-0nh, Robins besluit 7-8).
|
|---|
| 3 | //
|
|---|
| 4 | // Twee kanten. Uitgaand: de actor adverteert zijn webpagina en zijn RSS-feed
|
|---|
| 5 | // als Link-array, met een category zodra er muziek is. Inkomend: een
|
|---|
| 6 | // Create(Audio) van een kanaal wordt een tijdlijnrij met een echte speler --
|
|---|
| 7 | // zonder de Audio tot Note om te vormen, want die soort willen we houden.
|
|---|
| 8 | import { test } from 'node:test';
|
|---|
| 9 | import assert from 'node:assert/strict';
|
|---|
| 10 |
|
|---|
| 11 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 12 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 13 |
|
|---|
| 14 | const dbMod = await import('../src/config/database.js');
|
|---|
| 15 | const db = dbMod.default;
|
|---|
| 16 | dbMod.initializeDatabase();
|
|---|
| 17 | const AP = (await import('../src/services/ActivityPubService.js')).default;
|
|---|
| 18 |
|
|---|
| 19 | const BASE = 'https://test.example';
|
|---|
| 20 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 21 | .run('u1', 'u1', 'u1@t', 'x', 'god');
|
|---|
| 22 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
|
|---|
| 23 | .run('s1', 'band', 'De Band', 'u1');
|
|---|
| 24 | 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();
|
|---|
| 25 |
|
|---|
| 26 | test('de actor draagt zijn webpagina en zijn RSS-feed als Link-array', () => {
|
|---|
| 27 | const a = AP.buildActor(BASE, site());
|
|---|
| 28 | assert.ok(Array.isArray(a.url), 'url is een array, geen string');
|
|---|
| 29 | assert.deepEqual(a.url[0], { type: 'Link', href: 'https://test.example/', mediaType: 'text/html' },
|
|---|
| 30 | 'de webpagina staat VOORAAN: wie er maar een verwacht, pakt de eerste');
|
|---|
| 31 | assert.deepEqual(a.url[1], { type: 'Link', href: 'https://test.example/feed.xml', mediaType: 'application/rss+xml' });
|
|---|
| 32 | });
|
|---|
| 33 |
|
|---|
| 34 | test('zonder muziek geen category -- liever geen label dan een verkeerd label', () => {
|
|---|
| 35 | assert.equal(AP.buildActor(BASE, site()).category, undefined);
|
|---|
| 36 | });
|
|---|
| 37 |
|
|---|
| 38 | test('een GATED track maakt er nog geen muziekkanaal van', () => {
|
|---|
| 39 | // Aangescherpt: eerst telde elke track mee, ook een afgeschermde -- dan riep
|
|---|
| 40 | // een site met uitsluitend gated muziek toch "hier is muziek" naar buiten.
|
|---|
| 41 | // Afwezig is afwezig, ook in een categorie.
|
|---|
| 42 | db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)')
|
|---|
| 43 | .run('m0', 's1', 'dicht.mp3', 'audio/dicht.mp3', 'audio/mpeg');
|
|---|
| 44 | db.prepare('INSERT INTO audio_tracks (id, site_id, title, media_id, fedi_open) VALUES (?,?,?,?,0)')
|
|---|
| 45 | .run('t0', 's1', 'Achter de poort', 'm0');
|
|---|
| 46 | assert.equal(AP.buildActor(BASE, site()).category, undefined);
|
|---|
| 47 | });
|
|---|
| 48 |
|
|---|
| 49 | test('met OPENGEZETTE muziek is de category "music"', () => {
|
|---|
| 50 | db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)')
|
|---|
| 51 | .run('m1', 's1', 'a.mp3', 'audio/a.mp3', 'audio/mpeg');
|
|---|
| 52 | db.prepare('INSERT INTO audio_tracks (id, site_id, title, media_id, fedi_open) VALUES (?,?,?,?,1)')
|
|---|
| 53 | .run('t1', 's1', 'Een nummer', 'm1');
|
|---|
| 54 | assert.equal(AP.buildActor(BASE, site()).category, 'music');
|
|---|
| 55 | });
|
|---|
| 56 |
|
|---|
| 57 | // ── inkomend ────────────────────────────────────────────────────────────────
|
|---|
| 58 |
|
|---|
| 59 | const AUDIO = {
|
|---|
| 60 | id: 'https://audio.example/federation/music/tracks/1',
|
|---|
| 61 | type: 'Audio',
|
|---|
| 62 | name: '0METAL MARIO - PINK - 140 bpm',
|
|---|
| 63 | duration: 'PT103S',
|
|---|
| 64 | published: '2026-08-07T12:00:00Z',
|
|---|
| 65 | image: { type: 'Image', url: 'https://audio.example/covers/1.jpg', mediaType: 'image/jpeg' },
|
|---|
| 66 | url: [
|
|---|
| 67 | { type: 'Link', href: 'https://audio.example/library/tracks/1', mediaType: 'text/html' },
|
|---|
| 68 | { type: 'Link', href: 'https://audio.example/media/1.mp3', mediaType: 'audio/mpeg' },
|
|---|
| 69 | ],
|
|---|
| 70 | };
|
|---|
| 71 |
|
|---|
| 72 | test('een Audio blijft een Audio en levert een speler plus hoes', () => {
|
|---|
| 73 | const f = AP.timelineFields(AUDIO);
|
|---|
| 74 | assert.equal(AUDIO.type, 'Audio', 'het bronobject is niet tot Note omgevormd');
|
|---|
| 75 | assert.equal(f.url, 'https://audio.example/library/tracks/1', 'de text/html-link is de link van het item');
|
|---|
| 76 | assert.deepEqual(f.atts, [
|
|---|
| 77 | { url: 'https://audio.example/covers/1.jpg', type: 'image/jpeg' },
|
|---|
| 78 | { url: 'https://audio.example/media/1.mp3', type: 'audio/mpeg' },
|
|---|
| 79 | ], 'eerst kijken, dan luisteren -- en audio/* maakt er in de Krant een speler van');
|
|---|
| 80 | assert.match(f.html, /0METAL MARIO/, 'de titel is de inhoud; een Audio heeft geen content');
|
|---|
| 81 | });
|
|---|
| 82 |
|
|---|
| 83 | test('een titel met HTML erin gaat door de sanitizer', () => {
|
|---|
| 84 | const f = AP.timelineFields({ ...AUDIO, name: 'Track <script>alert(1)</script>' });
|
|---|
| 85 | assert.ok(!/<script/i.test(f.html), 'geen script uit een vreemde titel');
|
|---|
| 86 | });
|
|---|
| 87 |
|
|---|
| 88 | test('een Audio zonder audio-link levert geen speler, maar breekt ook niet', () => {
|
|---|
| 89 | const f = AP.timelineFields({ ...AUDIO, url: [{ type: 'Link', href: 'https://audio.example/x', mediaType: 'text/html' }] });
|
|---|
| 90 | assert.ok(!f.atts.some((a) => /^audio\//.test(a.type)), 'niets om af te spelen');
|
|---|
| 91 | assert.equal(f.url, 'https://audio.example/x');
|
|---|
| 92 | });
|
|---|
| 93 |
|
|---|
| 94 | test('een kale string-url telt als webpagina, nooit als geluid', () => {
|
|---|
| 95 | // Zonder mediaType is een speler eropzetten fout: dan bieden we een
|
|---|
| 96 | // HTML-pagina als audiobestand aan.
|
|---|
| 97 | const f = AP.timelineFields({ ...AUDIO, url: 'https://audio.example/los' });
|
|---|
| 98 | assert.equal(f.url, 'https://audio.example/los');
|
|---|
| 99 | assert.ok(!f.atts.some((a) => /^audio\//.test(a.type)));
|
|---|
| 100 | });
|
|---|
| 101 |
|
|---|
| 102 | test('een gewone Note gedraagt zich onveranderd', () => {
|
|---|
| 103 | const f = AP.timelineFields({
|
|---|
| 104 | id: 'https://elders.example/n/1', type: 'Note', content: '<p>hallo</p>',
|
|---|
| 105 | url: 'https://elders.example/n/1',
|
|---|
| 106 | attachment: [{ type: 'Document', url: 'https://elders.example/p.jpg', mediaType: 'image/jpeg' }],
|
|---|
| 107 | });
|
|---|
| 108 | assert.match(f.html, /hallo/);
|
|---|
| 109 | assert.deepEqual(f.atts, [{ url: 'https://elders.example/p.jpg', type: 'image/jpeg' }]);
|
|---|
| 110 | assert.equal(f.url, 'https://elders.example/n/1');
|
|---|
| 111 | });
|
|---|