| 1 | // De outbox vertelt de discografie mee (shaer-0nh, stap 4).
|
|---|
| 2 | //
|
|---|
| 3 | // Tracks verschijnen als Create(Audio) in de outbox, door de posts heen op
|
|---|
| 4 | // datum. NIET als bezorging naar volgers: Mastodon neemt Audio aan als
|
|---|
| 5 | // statustype, dus bij een album-post zou dezelfde muziek twee keer in hun
|
|---|
| 6 | // tijdlijn komen -- een keer als bijlage bij de Note, en dan nog los.
|
|---|
| 7 | //
|
|---|
| 8 | // En de deur blijft dicht: een geblokkeerde bezoeker krijgt een lege outbox,
|
|---|
| 9 | // ook nu daar tracks in kunnen zitten.
|
|---|
| 10 | import { test } from 'node:test';
|
|---|
| 11 | import assert from 'node:assert/strict';
|
|---|
| 12 |
|
|---|
| 13 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 14 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 15 |
|
|---|
| 16 | const dbMod = await import('../src/config/database.js');
|
|---|
| 17 | const db = dbMod.default;
|
|---|
| 18 | dbMod.initializeDatabase();
|
|---|
| 19 | const AP = (await import('../src/services/ActivityPubService.js')).default;
|
|---|
| 20 |
|
|---|
| 21 | const BASE = 'https://test.example';
|
|---|
| 22 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 23 | .run('u1', 'u1', 'u1@t', 'x', 'god');
|
|---|
| 24 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
|
|---|
| 25 | .run('s1', 'band', 'De Band', 'u1');
|
|---|
| 26 | const site = () => db.prepare("SELECT s.*, (SELECT slug FROM sites WHERE is_primary = 1) AS primary_slug FROM sites s WHERE s.id = 's1'").get();
|
|---|
| 27 |
|
|---|
| 28 | db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)')
|
|---|
| 29 | .run('m1', 's1', 'open.mp3', 'audio/open.mp3', 'audio/mpeg');
|
|---|
| 30 | db.prepare('INSERT INTO audio_tracks (id, site_id, title, duration, media_id, fedi_open, created_at) VALUES (?,?,?,?,?,1,?)')
|
|---|
| 31 | .run('t1', 's1', 'Het nummer', 100, 'm1', '2026-03-01 12:00:00');
|
|---|
| 32 | db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)')
|
|---|
| 33 | .run('m2', 's1', 'dicht.mp3', 'audio/dicht.mp3', 'audio/mpeg');
|
|---|
| 34 | db.prepare('INSERT INTO audio_tracks (id, site_id, title, duration, media_id, fedi_open) VALUES (?,?,?,?,?,0)')
|
|---|
| 35 | .run('t2', 's1', 'Achter de poort', 90, 'm2');
|
|---|
| 36 |
|
|---|
| 37 | const POST_OUD = { id: 'p1', slug: 'p1', title: 'Oud', content: '<p>oud</p>', status: 'published', published_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z' };
|
|---|
| 38 | const POST_NIEUW = { id: 'p2', slug: 'p2', title: 'Nieuw', content: '<p>nieuw</p>', status: 'published', published_at: '2026-06-01T00:00:00Z', created_at: '2026-06-01T00:00:00Z' };
|
|---|
| 39 |
|
|---|
| 40 | test('een track staat als Create(Audio) in de outbox', () => {
|
|---|
| 41 | const ob = AP.buildOutbox(BASE, site(), [], AP.siteOpenTracks('s1'));
|
|---|
| 42 | assert.equal(ob.totalItems, 1);
|
|---|
| 43 | const c = ob.orderedItems[0];
|
|---|
| 44 | assert.equal(c.type, 'Create');
|
|---|
| 45 | assert.equal(c.object.type, 'Audio', 'het blijft een Audio, geen Note');
|
|---|
| 46 | assert.equal(c.object.id, 'https://test.example/ap/users/band/tracks/t1');
|
|---|
| 47 | assert.equal(c.id, 'https://test.example/ap/users/band/tracks/t1#create',
|
|---|
| 48 | 'stabiel id: twee keer ophalen is niet twee keer nieuws');
|
|---|
| 49 | assert.deepEqual(c.to, ['https://www.w3.org/ns/activitystreams#Public']);
|
|---|
| 50 | });
|
|---|
| 51 |
|
|---|
| 52 | test('posts en tracks staan door elkaar op datum, nieuwste eerst', () => {
|
|---|
| 53 | const ob = AP.buildOutbox(BASE, site(), [POST_NIEUW, POST_OUD], AP.siteOpenTracks('s1'));
|
|---|
| 54 | assert.equal(ob.totalItems, 3);
|
|---|
| 55 | assert.deepEqual(ob.orderedItems.map((c) => c.object.type), ['Note', 'Audio', 'Note'],
|
|---|
| 56 | 'juni-post, maart-track, januari-post');
|
|---|
| 57 | });
|
|---|
| 58 |
|
|---|
| 59 | test('een gesloten track staat er niet in, en lekt ook zijn titel niet', () => {
|
|---|
| 60 | const ob = AP.buildOutbox(BASE, site(), [], AP.siteOpenTracks('s1'));
|
|---|
| 61 | assert.ok(!JSON.stringify(ob).includes('Achter de poort'));
|
|---|
| 62 | assert.ok(!JSON.stringify(ob).includes('dicht.mp3'));
|
|---|
| 63 | });
|
|---|
| 64 |
|
|---|
| 65 | test('zonder tracks-argument blijft de outbox precies zoals hij was', () => {
|
|---|
| 66 | // De bouwer haalt NIETS zelf op: dat is wat de blocked-tak in de route
|
|---|
| 67 | // beschermt. Zou hij dat wel doen, dan leverde hij dwars door een dichte
|
|---|
| 68 | // deur heen.
|
|---|
| 69 | const ob = AP.buildOutbox(BASE, site(), [POST_NIEUW]);
|
|---|
| 70 | assert.equal(ob.totalItems, 1);
|
|---|
| 71 | assert.equal(ob.orderedItems[0].object.type, 'Note');
|
|---|
| 72 | });
|
|---|
| 73 |
|
|---|
| 74 | test('de deur dicht: een lege outbox blijft leeg, ook met muziek in de kast', () => {
|
|---|
| 75 | const ob = AP.buildOutbox(BASE, site(), [], []);
|
|---|
| 76 | assert.equal(ob.totalItems, 0);
|
|---|
| 77 | assert.deepEqual(ob.orderedItems, []);
|
|---|
| 78 | });
|
|---|