| 1 | // Een track wijst naar de post die hem uitbrengt (shaer-0nh).
|
|---|
| 2 | //
|
|---|
| 3 | // Aanleiding: sinds Create(Audio) in de outbox staat (fb22f78) bouwt Shaer zijn
|
|---|
| 4 | // HomeBase-feed uit diezelfde outbox, en maakte het van elke Audio een lege
|
|---|
| 5 | // kaart -- een Audio heeft geen `content` en onze `url` is een Link-array.
|
|---|
| 6 | //
|
|---|
| 7 | // De relatie stond alleen in posts.content ([[track:]], [[playlist:]],
|
|---|
| 8 | // [[album:]]) en nergens op de draad. Nu wel: AS2-kern `context` wijst naar de
|
|---|
| 9 | // Note van de post, en de text/html-link vooraan wijst naar de pagina. Een
|
|---|
| 10 | // lezer die de post al heeft kan het nummer daarmee overslaan in plaats van er
|
|---|
| 11 | // een dode kaart van te maken.
|
|---|
| 12 | import { test } from 'node:test';
|
|---|
| 13 | import assert from 'node:assert/strict';
|
|---|
| 14 |
|
|---|
| 15 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 16 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 17 |
|
|---|
| 18 | const dbMod = await import('../src/config/database.js');
|
|---|
| 19 | const db = dbMod.default;
|
|---|
| 20 | dbMod.initializeDatabase();
|
|---|
| 21 | const AP = (await import('../src/services/ActivityPubService.js')).default;
|
|---|
| 22 |
|
|---|
| 23 | const BASE = 'https://test.example';
|
|---|
| 24 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 25 | .run('u1', 'u1', 'u1@t', 'x', 'god');
|
|---|
| 26 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
|
|---|
| 27 | .run('s1', 'band', 'De Band', 'u1');
|
|---|
| 28 |
|
|---|
| 29 | const insM = db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1000)');
|
|---|
| 30 | const insT = db.prepare('INSERT INTO audio_tracks (id, site_id, title, duration, media_id, album, fedi_open) VALUES (?,?,?,?,?,?,1)');
|
|---|
| 31 | for (const [id, titel, album] of [['t-los', 'Losse track', null], ['t-pl', 'Via playlist', null],
|
|---|
| 32 | ['t-alb', 'Via album', 'De Plaat'], ['t-wees', 'Nergens ingesloten', null]]) {
|
|---|
| 33 | insM.run('m-' + id, 's1', id + '.mp3', 'audio/' + id + '.mp3', 'audio/mpeg');
|
|---|
| 34 | insT.run(id, 's1', titel, 60, 'm-' + id, album);
|
|---|
| 35 | }
|
|---|
| 36 | db.prepare("INSERT INTO playlists (id, site_id, title, kind) VALUES ('plaat','s1','De Plaat','album')").run();
|
|---|
| 37 | db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('plaat','t-pl',1)").run();
|
|---|
| 38 |
|
|---|
| 39 | const insP = db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at)
|
|---|
| 40 | VALUES (?,?,?,?,?,?,'published',?)`);
|
|---|
| 41 | insP.run('p-los', 's1', 'u1', 'losse-post', 'Losse post', 'Hier: [[track:t-los]]', '2026-01-01T00:00:00Z');
|
|---|
| 42 | insP.run('p-pl', 's1', 'u1', 'playlist-post', 'Playlist-post', 'Nieuw album [[playlist:plaat]]', '2026-02-01T00:00:00Z');
|
|---|
| 43 | insP.run('p-alb', 's1', 'u1', 'album-post', 'Album-post', 'Luister [[album:De Plaat]]', '2026-03-01T00:00:00Z');
|
|---|
| 44 |
|
|---|
| 45 | const site = () => db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
|
|---|
| 46 | const audioVoor = (id) => {
|
|---|
| 47 | const r = AP.siteOpenTracks('s1').find((x) => x.id === id);
|
|---|
| 48 | return AP.buildTrackAudio(BASE, site(), r);
|
|---|
| 49 | };
|
|---|
| 50 |
|
|---|
| 51 | test('rechtstreeks ingesloten: context wijst naar de Note van die post', () => {
|
|---|
| 52 | const a = audioVoor('t-los');
|
|---|
| 53 | assert.equal(a.context, 'https://test.example/ap/notes/p-los');
|
|---|
| 54 | assert.deepEqual(a.url[0], { type: 'Link', href: 'https://test.example/losse-post', mediaType: 'text/html' },
|
|---|
| 55 | 'de post staat VOORAAN, zoals Funkwhale zijn trackpagina zet');
|
|---|
| 56 | assert.equal(a.url[1].mediaType, 'audio/mpeg', 'het bestand komt daarna');
|
|---|
| 57 | });
|
|---|
| 58 |
|
|---|
| 59 | test('via een playlist gevonden', () => {
|
|---|
| 60 | assert.equal(audioVoor('t-pl').context, 'https://test.example/ap/notes/p-pl');
|
|---|
| 61 | });
|
|---|
| 62 |
|
|---|
| 63 | test('via een albumnaam gevonden', () => {
|
|---|
| 64 | assert.equal(audioVoor('t-alb').context, 'https://test.example/ap/notes/p-alb');
|
|---|
| 65 | });
|
|---|
| 66 |
|
|---|
| 67 | test('een track die nergens is ingesloten krijgt GEEN context', () => {
|
|---|
| 68 | const a = audioVoor('t-wees');
|
|---|
| 69 | assert.equal(a.context, undefined, 'niets verzinnen als er geen post is');
|
|---|
| 70 | assert.equal(a.url.length, 1, 'dan ook geen text/html-link');
|
|---|
| 71 | assert.equal(a.url[0].mediaType, 'audio/mpeg');
|
|---|
| 72 | });
|
|---|
| 73 |
|
|---|
| 74 | test('rechtstreeks wint van playlist wint van album', () => {
|
|---|
| 75 | // t-pl zit in playlist "plaat"; die playlist is ook een album met dezelfde
|
|---|
| 76 | // naam. Sluit een NIEUWERE post hem rechtstreeks in, dan wint die -- de
|
|---|
| 77 | // specifiekste insluiting, niet de laatste.
|
|---|
| 78 | db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at)
|
|---|
| 79 | VALUES ('p-direct','s1','u1','direct-post','Direct','Deze: [[track:t-pl]]','published','2025-01-01T00:00:00Z')`).run();
|
|---|
| 80 | assert.equal(audioVoor('t-pl').context, 'https://test.example/ap/notes/p-direct',
|
|---|
| 81 | 'ouder maar specifieker gaat voor');
|
|---|
| 82 | db.prepare("DELETE FROM posts WHERE id = 'p-direct'").run();
|
|---|
| 83 | });
|
|---|
| 84 |
|
|---|
| 85 | test('een concept telt niet als host', () => {
|
|---|
| 86 | db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status)
|
|---|
| 87 | VALUES ('p-concept','s1','u1','concept','Concept','[[track:t-wees]]','draft')`).run();
|
|---|
| 88 | assert.equal(audioVoor('t-wees').context, undefined, 'alleen gepubliceerde posts hosten iets');
|
|---|
| 89 | db.prepare("DELETE FROM posts WHERE id = 'p-concept'").run();
|
|---|
| 90 | });
|
|---|
| 91 |
|
|---|
| 92 | test('de collectie doet EEN zoekopdracht en levert dezelfde koppelingen', () => {
|
|---|
| 93 | const col = AP.buildTrackCollection(BASE, site(), AP.siteOpenTracks('s1'));
|
|---|
| 94 | const perNaam = Object.fromEntries(col.orderedItems.map((a) => [a.name, a.context]));
|
|---|
| 95 | assert.equal(perNaam['Losse track'], 'https://test.example/ap/notes/p-los');
|
|---|
| 96 | assert.equal(perNaam['Via playlist'], 'https://test.example/ap/notes/p-pl');
|
|---|
| 97 | assert.equal(perNaam['Nergens ingesloten'], undefined);
|
|---|
| 98 | });
|
|---|
| 99 |
|
|---|
| 100 | test('en de outbox draagt de context ook', () => {
|
|---|
| 101 | const ob = AP.buildOutbox(BASE, site(), [], AP.siteOpenTracks('s1'));
|
|---|
| 102 | const c = ob.orderedItems.find((x) => (x.object || {}).name === 'Losse track');
|
|---|
| 103 | assert.equal(c.object.context, 'https://test.example/ap/notes/p-los');
|
|---|
| 104 | });
|
|---|