source: Klonkt/test/channel-actor.test.js

main
Last change on this file was 4d81de5, checked in by Robin <roboburr@…>, 5 weeks ago

De feed vertelt ook WAT voor kanaal dit is

Funkwhale maakte van dev@… een kanaal en las de rss_url netjes uit
de Link-array op onze actor -- maar liet content_category leeg. Dat is de
aanwijzing: hij bouwt zo'n kanaal langs de RSS-kant op, en leest de categorie
daar. Niet uit category op de AP-actor.

Dus itunes-velden erbij op kanaalniveau: author, summary, image (de site-avatar,
absoluut gemaakt) en category. En per track de eigen artiest en hoes, naast de
duur die er al stond.

EN IK HEB MIJN EIGEN CATEGORIE-REGEL AANGESCHERPT. Die keek naar ELKE track,
ook een gated -- zodat een site met uitsluitend afgeschermde muziek toch "hier
is muziek" naar de hele fediverse riep. Dat botst met de regel die we overal
aanhouden: een gesloten track is AFWEZIG, niet stilletjes aanwezig. Nu tellen
alleen opengezette tracks, en zeggen de feed en de actor hetzelfde. Er staat
een test op die precies dat vastlegt: een gated track maakt er nog geen
muziekkanaal van.

Dat maakt de regel strenger dan hij vanmiddag was. Bewust: naar buiten toe is
een kanaal zonder publieke muziek geen muziekkanaal.

4 tests.

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

  • Property mode set to 100644
File size: 5.7 KB
Line 
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.
8import { test } from 'node:test';
9import assert from 'node:assert/strict';
10
11process.env.DATABASE_PATH = ':memory:';
12process.env.PUBLIC_BASE_URL = 'https://test.example';
13
14const dbMod = await import('../src/config/database.js');
15const db = dbMod.default;
16dbMod.initializeDatabase();
17const AP = (await import('../src/services/ActivityPubService.js')).default;
18
19const BASE = 'https://test.example';
20db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
21 .run('u1', 'u1', 'u1@t', 'x', 'god');
22db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
23 .run('s1', 'band', 'De Band', 'u1');
24const 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
26test('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
34test('zonder muziek geen category -- liever geen label dan een verkeerd label', () => {
35 assert.equal(AP.buildActor(BASE, site()).category, undefined);
36});
37
38test('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
49test('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
59const 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
72test('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
83test('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
88test('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
94test('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
102test('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});
Note: See TracBrowser for help on using the repository browser.