source: Klonkt/test/ap-playlist.test.js

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

Onze tracks zijn eersterangs Audio-objecten (shaer-0nh, stap 3)

De spiegel van wat sinds bdcb3a3 inkomend werkt. Onze muziek zat alleen als
naamloze bijlage in een Note: iets dat bestaat zolang je het omhullende object
vasthoudt. Nu is een track een DING met een eigen id, los op te halen, en in
twee playlists hetzelfde ding.

DE ACTOR-COLLECTIE IS DE KANONIEKE PLEK, niet de playlist (Robins vraag). Een
playlist is een keuze uit wat de artiest heeft uitgebracht; de collectie is dat
geheel. Een track die in geen enkele playlist stond was tot nu toe onzichtbaar
voor de federatie -- die staat er nu wel in.

GET /ap/users/:slug/tracks alles wat opengezet is
GET /ap/users/:slug/tracks/:id een track, met eigen @context
streams tracks eerst, dan playlists

url wordt een Link-array: de mediaType hoort bij de link, niet bij het
object. Zo doet Funkwhale het, en zo leest onze eigen inbox het sinds bdcb3a3 --
een andere Klonkt kan onze tracks dus als track opnemen in plaats van als
bijlage.

GEEN text/html-link erin. Klonkt heeft geen trackpagina: een track wordt getoond
binnen een post, en een post over vijf nummers is niet de pagina van dit ene
nummer. Liever geen link dan een link die iets anders belooft.

De poortregel loopt door: een gesloten track geeft 404, dezelfde "afwezig, niet
leeg" als in de collectie, zodat het bestaan van een gated nummer niet uit een
ander antwoord valt af te leiden.

ONDERWEG OPGERUIMD. De mediaType-afleiding stond twee keer functie-lokaal, met
een commentaar dat ze "dezelfde afleiding" waren. Dat was niet zo: de ene kende
video, de andere alleen beeld. Nu een kaart, op moduleniveau, met de volledige
map -- gedragsgelijk voor buildNote en een superset voor de playlist.

7 tests.

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

  • Property mode set to 100644
File size: 5.6 KB
RevLine 
[3e1aab1]1// Een playlist als AP-collectie (shaer-ayc, stap 1 van het Funkwhale-spoor).
2//
3// Vier beloftes: de collectie bestaat op een stabiele URI; alleen fedi_open-
4// tracks staan erin, met echte bestands-URL, en totalItems telt alleen die --
5// wie leest kan niet aftellen wat er achter de poort staat; de volgorde is de
6// playlist-volgorde, niet de invoegvolgorde; en een playlist van site A is op
7// de actor van site B een 404, want tenancy loopt door alles heen.
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 express = (await import('express')).default;
18const routes = (await import('../src/routes/activitypub.js')).default;
19
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) VALUES (?,?,?,?,1)')
23 .run('s1', 'band', 'De Band', 'u1');
24db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public) VALUES (?,?,?,?,1)')
25 .run('s2', 'ander', 'Andere Site', 'u1');
26
27// Drie tracks: twee open, een gated. De gated staat MIDDEN in de volgorde,
28// zodat een fout die hem als lege rij meetelt ook de volgorde zou breken.
29const insMedia = db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1000)');
30const insTrack = db.prepare('INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, fedi_open, cover_url) VALUES (?,?,?,?,?,?,?,?)');
31insMedia.run('m1', 's1', 'een.mp3', 'audio/een.mp3', 'audio/mpeg');
32insMedia.run('m2', 's1', 'twee.mp3', 'audio/twee.mp3', 'audio/mpeg');
33insMedia.run('m3', 's1', 'drie.mp3', 'audio/drie.mp3', 'audio/mpeg');
34insTrack.run('t1', 's1', 'Opening', 'De Band', 215, 'm1', 1, '/media/hoes1.jpg');
35insTrack.run('t2', 's1', 'Achter de poort', 'De Band', 180, 'm2', 0, null);
36insTrack.run('t3', 's1', 'Finale', 'De Band', 240, 'm3', 1, null);
37
38db.prepare("INSERT INTO playlists (id, site_id, title, artist, year, cover_url, kind) VALUES ('nachtlicht','s1','Nachtlicht','De Band',2026,'/media/album.jpg','album')").run();
39const insPT = db.prepare('INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES (?,?,?)');
40// Invoegvolgorde is met opzet NIET de positievolgorde.
41insPT.run('nachtlicht', 't3', 3);
42insPT.run('nachtlicht', 't1', 1);
43insPT.run('nachtlicht', 't2', 2);
44
45const app = express();
46app.use(routes);
47const server = app.listen(0);
48await new Promise((r) => server.once('listening', r));
49const base = `http://127.0.0.1:${server.address().port}`;
50const getJson = async (path) => {
51 const r = await fetch(base + path, { headers: { Accept: 'application/activity+json' } });
52 return { status: r.status, body: r.status === 200 ? await r.json() : null, type: r.headers.get('content-type') || '' };
53};
54
55test('de collectie bestaat, als AS2, met stabiele id en attributedTo de actor', async () => {
56 const { status, body, type } = await getJson('/ap/users/band/playlists/nachtlicht');
57 assert.equal(status, 200);
58 assert.match(type, /application\/activity\+json/);
59 assert.equal(body.type, 'OrderedCollection');
60 assert.equal(body.id, 'https://test.example/ap/users/band/playlists/nachtlicht');
61 assert.equal(body.attributedTo, 'https://test.example/ap/users/band');
62 assert.equal(body.name, 'Nachtlicht');
63 assert.equal(body.summary, 'De Band · 2026');
64 assert.equal(body.icon.url, 'https://test.example/media/album.jpg');
65});
66
67test('alleen fedi_open-tracks, totalItems telt alleen die, en de poort lekt geen bestandsnaam', async () => {
68 const { body } = await getJson('/ap/users/band/playlists/nachtlicht');
69 assert.equal(body.totalItems, 2, 'de gated track telt niet mee');
70 assert.equal(body.orderedItems.length, 2);
71 const alles = JSON.stringify(body);
72 assert.ok(!alles.includes('twee.mp3'), 'de gated bestandsnaam staat nergens in de collectie');
73 assert.ok(!alles.includes('Achter de poort'), 'ook de titel van de gated track niet');
74});
75
76test('de volgorde is de playlist-volgorde, en elke rij is een speelbare Audio', async () => {
77 const { body } = await getJson('/ap/users/band/playlists/nachtlicht');
78 assert.deepEqual(body.orderedItems.map((a) => a.name), ['Opening', 'Finale']);
79 for (const a of body.orderedItems) {
80 assert.equal(a.type, 'Audio');
[39a9d21]81 // Sinds stap 3: een eigen id, en url als Link-array -- de mediaType hoort
82 // bij de link, niet bij het object.
83 assert.match(a.id, /^https:\/\/test\.example\/ap\/users\/band\/tracks\//);
84 assert.ok(Array.isArray(a.url), 'url is een Link-array');
85 assert.match(a.url[0].href, /^https:\/\/test\.example\/audio\/stream\//);
86 assert.equal(a.url[0].mediaType, 'audio/mpeg');
[3e1aab1]87 }
88 assert.equal(body.orderedItems[0].duration, 'PT215S');
89 assert.equal(body.orderedItems[0].summary, 'De Band');
90 // Track zonder eigen hoes erft de albumhoes -- zelfde regel als post-audio,
91 // waar de postcover invalt.
92 assert.equal(body.orderedItems[1].icon.url, 'https://test.example/media/album.jpg');
93});
94
95test('tenancy: dezelfde playlist op de verkeerde actor is een 404', async () => {
96 const { status } = await getJson('/ap/users/ander/playlists/nachtlicht');
97 assert.equal(status, 404);
98});
99
100test('onbekende playlist en onbekende site zijn allebei een 404', async () => {
101 assert.equal((await getJson('/ap/users/band/playlists/bestaat-niet')).status, 404);
102 assert.equal((await getJson('/ap/users/niemand/playlists/nachtlicht')).status, 404);
103});
104
105test.after(() => server.close());
Note: See TracBrowser for help on using the repository browser.