| 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.
|
|---|
| 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 | const express = (await import('express')).default;
|
|---|
| 19 | const routes = (await import('../src/routes/activitypub.js')).default;
|
|---|
| 20 |
|
|---|
| 21 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 22 | .run('u1', 'u1', 'u1@t', 'x', 'god');
|
|---|
| 23 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
|
|---|
| 24 | .run('s1', 'band', 'De Band', 'u1');
|
|---|
| 25 |
|
|---|
| 26 | const insM = db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)');
|
|---|
| 27 | const insT = db.prepare('INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, cover_url, fedi_open, position) VALUES (?,?,?,?,?,?,?,?,?)');
|
|---|
| 28 | insM.run('m1', 's1', 'open.mp3', 'audio/open.mp3', 'audio/mpeg');
|
|---|
| 29 | insM.run('m2', 's1', 'dicht.mp3', 'audio/dicht.mp3', 'audio/mpeg');
|
|---|
| 30 | insM.run('m3', 's1', 'los.mp3', 'audio/los.mp3', 'audio/mpeg');
|
|---|
| 31 | insT.run('t-open', 's1', 'Open nummer', 'De Band', 203, 'm1', '/media/hoes.jpg', 1, 1);
|
|---|
| 32 | insT.run('t-dicht', 's1', 'Gesloten nummer', 'De Band', 100, 'm2', null, 0, 2);
|
|---|
| 33 | insT.run('t-los', 's1', 'Zonder playlist', 'De Band', 90, 'm3', null, 1, 3);
|
|---|
| 34 |
|
|---|
| 35 | db.prepare("INSERT INTO playlists (id, site_id, title, kind, cover_url) VALUES ('plaat','s1','De Plaat','album','/media/plaathoes.jpg')").run();
|
|---|
| 36 | db.prepare("INSERT INTO playlists (id, site_id, title, kind) VALUES ('mix','s1','De Mix','playlist')").run();
|
|---|
| 37 | const insPT = db.prepare('INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES (?,?,?)');
|
|---|
| 38 | insPT.run('plaat', 't-open', 1);
|
|---|
| 39 | insPT.run('plaat', 't-dicht', 2);
|
|---|
| 40 | insPT.run('mix', 't-open', 1);
|
|---|
| 41 |
|
|---|
| 42 | const app = express(); app.use(routes);
|
|---|
| 43 | const server = app.listen(0);
|
|---|
| 44 | await new Promise((r) => server.once('listening', r));
|
|---|
| 45 | const base = `http://127.0.0.1:${server.address().port}`;
|
|---|
| 46 | const 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 |
|
|---|
| 51 | test('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 |
|
|---|
| 59 | test('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 |
|
|---|
| 69 | test('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 | // De Link draagt sinds de veldvergelijking met Funkwhale ook de
|
|---|
| 78 | // bestandsgegevens: die horen bij DEZE representatie, niet bij het nummer.
|
|---|
| 79 | assert.equal(a.url.length, 1);
|
|---|
| 80 | assert.equal(a.url[0].type, 'Link');
|
|---|
| 81 | assert.equal(a.url[0].href, 'https://test.example/audio/stream/open.mp3');
|
|---|
| 82 | assert.equal(a.url[0].mediaType, 'audio/mpeg', 'de mediaType hoort bij de link, niet bij het object');
|
|---|
| 83 | assert.equal(a.mediaType, undefined, 'en dus NIET op het object');
|
|---|
| 84 | assert.equal(a.icon.url, 'https://test.example/media/hoes.jpg');
|
|---|
| 85 | });
|
|---|
| 86 |
|
|---|
| 87 | test('los op te halen, met eigen @context', async () => {
|
|---|
| 88 | const { status, body } = await haal('/ap/users/band/tracks/t-open');
|
|---|
| 89 | assert.equal(status, 200);
|
|---|
| 90 | assert.equal(body.id, 'https://test.example/ap/users/band/tracks/t-open');
|
|---|
| 91 | assert.ok(body['@context'], 'standalone draagt zijn eigen context');
|
|---|
| 92 | });
|
|---|
| 93 |
|
|---|
| 94 | test('een gesloten track is AFWEZIG, niet leeg', async () => {
|
|---|
| 95 | assert.equal((await haal('/ap/users/band/tracks/t-dicht')).status, 404);
|
|---|
| 96 | assert.equal((await haal('/ap/users/band/tracks/bestaatniet')).status, 404);
|
|---|
| 97 | });
|
|---|
| 98 |
|
|---|
| 99 | test('dezelfde track in twee playlists is HETZELFDE ding', async () => {
|
|---|
| 100 | const a = (await haal('/ap/users/band/playlists/plaat')).body.orderedItems[0];
|
|---|
| 101 | const b = (await haal('/ap/users/band/playlists/mix')).body.orderedItems[0];
|
|---|
| 102 | assert.equal(a.id, b.id);
|
|---|
| 103 | assert.equal(a.id, 'https://test.example/ap/users/band/tracks/t-open');
|
|---|
| 104 | });
|
|---|
| 105 |
|
|---|
| 106 | test('de playlisthoes springt in voor een track zonder eigen hoes', async () => {
|
|---|
| 107 | // t-los heeft geen cover_url; in de plaat-collectie zit hij niet, dus we
|
|---|
| 108 | // toetsen de terugval via een playlist die hem wel bevat.
|
|---|
| 109 | db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('plaat','t-los',3)").run();
|
|---|
| 110 | const items = (await haal('/ap/users/band/playlists/plaat')).body.orderedItems;
|
|---|
| 111 | const los = items.find((a) => a.name === 'Zonder playlist');
|
|---|
| 112 | assert.equal(los.icon.url, 'https://test.example/media/plaathoes.jpg');
|
|---|
| 113 | // En de track met een EIGEN hoes houdt die.
|
|---|
| 114 | assert.equal(items.find((a) => a.name === 'Open nummer').icon.url, 'https://test.example/media/hoes.jpg');
|
|---|
| 115 | });
|
|---|
| 116 |
|
|---|
| 117 | test.after(() => server.close());
|
|---|