source: Klonkt/test/ap-tracks.test.js@ 39a9d21

main
Last change on this file since 39a9d21 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.7 KB
Line 
1// Onze tracks als eersterangs Audio-objecten (shaer-0nh, stap 3).
2//
3// De actor-collectie is de kanonieke plek: een playlist is een KEUZE daaruit.
4// Een track die in geen playlist staat hoort er dus wel in, en een track die in
5// twee playlists staat is twee keer HETZELFDE ding -- zelfde id.
6//
7// De poortregel blijft: een gesloten track is afwezig, niet leeg.
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;
18const express = (await import('express')).default;
19const routes = (await import('../src/routes/activitypub.js')).default;
20
21db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
22 .run('u1', 'u1', 'u1@t', 'x', 'god');
23db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
24 .run('s1', 'band', 'De Band', 'u1');
25
26const insM = db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)');
27const insT = db.prepare('INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, cover_url, fedi_open, position) VALUES (?,?,?,?,?,?,?,?,?)');
28insM.run('m1', 's1', 'open.mp3', 'audio/open.mp3', 'audio/mpeg');
29insM.run('m2', 's1', 'dicht.mp3', 'audio/dicht.mp3', 'audio/mpeg');
30insM.run('m3', 's1', 'los.mp3', 'audio/los.mp3', 'audio/mpeg');
31insT.run('t-open', 's1', 'Open nummer', 'De Band', 203, 'm1', '/media/hoes.jpg', 1, 1);
32insT.run('t-dicht', 's1', 'Gesloten nummer', 'De Band', 100, 'm2', null, 0, 2);
33insT.run('t-los', 's1', 'Zonder playlist', 'De Band', 90, 'm3', null, 1, 3);
34
35db.prepare("INSERT INTO playlists (id, site_id, title, kind, cover_url) VALUES ('plaat','s1','De Plaat','album','/media/plaathoes.jpg')").run();
36db.prepare("INSERT INTO playlists (id, site_id, title, kind) VALUES ('mix','s1','De Mix','playlist')").run();
37const insPT = db.prepare('INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES (?,?,?)');
38insPT.run('plaat', 't-open', 1);
39insPT.run('plaat', 't-dicht', 2);
40insPT.run('mix', 't-open', 1);
41
42const app = express(); app.use(routes);
43const server = app.listen(0);
44await new Promise((r) => server.once('listening', r));
45const base = `http://127.0.0.1:${server.address().port}`;
46const haal = async (p) => {
47 const r = await fetch(base + p, { headers: { Accept: 'application/activity+json' } });
48 return { status: r.status, body: r.status === 200 ? await r.json() : null };
49};
50
51test('de actor wijst naar de tracks EN de playlists, tracks eerst', async () => {
52 const { body } = await haal('/ap/users/band');
53 assert.deepEqual(body.streams, [
54 'https://test.example/ap/users/band/tracks',
55 'https://test.example/ap/users/band/playlists',
56 ]);
57});
58
59test('de collectie draagt elke OPEN track, ook zonder playlist', async () => {
60 const { body } = await haal('/ap/users/band/tracks');
61 assert.equal(body.type, 'OrderedCollection');
62 assert.equal(body.totalItems, 2, 'twee open tracks; de gesloten telt niet mee');
63 const namen = body.orderedItems.map((a) => a.name);
64 assert.deepEqual(namen, ['Open nummer', 'Zonder playlist'],
65 'ook de track die in geen enkele playlist staat hoort hier');
66 assert.ok(!JSON.stringify(body).includes('Gesloten nummer'), 'een gated titel lekt niet');
67});
68
69test('een track heeft een eigen id en url als Link-array', async () => {
70 const { body } = await haal('/ap/users/band/tracks');
71 const a = body.orderedItems[0];
72 assert.equal(a.id, 'https://test.example/ap/users/band/tracks/t-open');
73 assert.equal(a.type, 'Audio');
74 assert.equal(a.attributedTo, 'https://test.example/ap/users/band');
75 assert.equal(a.duration, 'PT203S');
76 assert.equal(a.summary, 'De Band');
77 assert.deepEqual(a.url, [{
78 type: 'Link', href: 'https://test.example/audio/stream/open.mp3', mediaType: 'audio/mpeg',
79 }], 'de mediaType hoort bij de link, niet bij het object');
80 assert.equal(a.icon.url, 'https://test.example/media/hoes.jpg');
81});
82
83test('los op te halen, met eigen @context', async () => {
84 const { status, body } = await haal('/ap/users/band/tracks/t-open');
85 assert.equal(status, 200);
86 assert.equal(body.id, 'https://test.example/ap/users/band/tracks/t-open');
87 assert.ok(body['@context'], 'standalone draagt zijn eigen context');
88});
89
90test('een gesloten track is AFWEZIG, niet leeg', async () => {
91 assert.equal((await haal('/ap/users/band/tracks/t-dicht')).status, 404);
92 assert.equal((await haal('/ap/users/band/tracks/bestaatniet')).status, 404);
93});
94
95test('dezelfde track in twee playlists is HETZELFDE ding', async () => {
96 const a = (await haal('/ap/users/band/playlists/plaat')).body.orderedItems[0];
97 const b = (await haal('/ap/users/band/playlists/mix')).body.orderedItems[0];
98 assert.equal(a.id, b.id);
99 assert.equal(a.id, 'https://test.example/ap/users/band/tracks/t-open');
100});
101
102test('de playlisthoes springt in voor een track zonder eigen hoes', async () => {
103 // t-los heeft geen cover_url; in de plaat-collectie zit hij niet, dus we
104 // toetsen de terugval via een playlist die hem wel bevat.
105 db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('plaat','t-los',3)").run();
106 const items = (await haal('/ap/users/band/playlists/plaat')).body.orderedItems;
107 const los = items.find((a) => a.name === 'Zonder playlist');
108 assert.equal(los.icon.url, 'https://test.example/media/plaathoes.jpg');
109 // En de track met een EIGEN hoes houdt die.
110 assert.equal(items.find((a) => a.name === 'Open nummer').icon.url, 'https://test.example/media/hoes.jpg');
111});
112
113test.after(() => server.close());
Note: See TracBrowser for help on using the repository browser.