| 1 | // Wie `first` belooft, moet een PAGINA leveren (open.audio, 15-8).
|
|---|
| 2 | //
|
|---|
| 3 | // De aanleiding: `/ap/users/dev/library` gaf 200, keurig met `first` en `last`,
|
|---|
| 4 | // maar `?page=1` gaf de bibliotheek OPNIEUW -- zelfde id, type Library, en een
|
|---|
| 5 | // `first` die weer naar zichzelf wees. open.audio volgde die verwijzing en gaf
|
|---|
| 6 | // een 500 terug op onze URL.
|
|---|
| 7 | //
|
|---|
| 8 | // De bouwer kon al pagineren; de ROUTE gaf `?page=` niet door. Daarom loopt deze
|
|---|
| 9 | // test over HTTP en niet over `buildLibrary()`: een unit-test op de bouwer stond
|
|---|
| 10 | // er al en was groen, precies terwijl het gat openstond. Zie
|
|---|
| 11 | // [verificatie-niet-zonder-slaagtest] -- een controle die niet kan falen op wat
|
|---|
| 12 | // je verandert is geen bewijs.
|
|---|
| 13 | //
|
|---|
| 14 | // Hij is bewust een LUS over alle collecties en geen rijtje losse gevallen: het
|
|---|
| 15 | // gat ontstond doordat vier routes de doorgifte wel hadden en twee niet, en een
|
|---|
| 16 | // per-collectie-test vraagt om precies diezelfde vergeetachtigheid.
|
|---|
| 17 | import { test } from 'node:test';
|
|---|
| 18 | import assert from 'node:assert/strict';
|
|---|
| 19 |
|
|---|
| 20 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 21 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 22 |
|
|---|
| 23 | const dbMod = await import('../src/config/database.js');
|
|---|
| 24 | const db = dbMod.default;
|
|---|
| 25 | dbMod.initializeDatabase();
|
|---|
| 26 | const express = (await import('express')).default;
|
|---|
| 27 | const routes = (await import('../src/routes/activitypub.js')).default;
|
|---|
| 28 |
|
|---|
| 29 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 30 | .run('u1', 'u1', 'u1@t', 'x', 'god');
|
|---|
| 31 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
|
|---|
| 32 | .run('s1', 'band', 'De Band', 'u1');
|
|---|
| 33 |
|
|---|
| 34 | const insM = db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)');
|
|---|
| 35 | const insT = db.prepare('INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, fedi_open, position) VALUES (?,?,?,?,?,?,1,?)');
|
|---|
| 36 | for (const n of [1, 2, 3]) {
|
|---|
| 37 | insM.run(`m${n}`, 's1', `t${n}.mp3`, `audio/t${n}.mp3`, 'audio/mpeg');
|
|---|
| 38 | insT.run(`t${n}`, 's1', `Nummer ${n}`, 'De Band', 120, `m${n}`, n);
|
|---|
| 39 | }
|
|---|
| 40 | db.prepare("INSERT INTO playlists (id, site_id, title, kind) VALUES ('plaat','s1','De Plaat','album')").run();
|
|---|
| 41 | db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('plaat','t1',1)").run();
|
|---|
| 42 | db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at)
|
|---|
| 43 | VALUES ('p1','s1','u1','hallo','Hallo','<p>x</p>','published','2026-08-01T00:00:00Z')`).run();
|
|---|
| 44 |
|
|---|
| 45 | const app = express(); app.use(routes);
|
|---|
| 46 | const server = app.listen(0);
|
|---|
| 47 | await new Promise((r) => server.once('listening', r));
|
|---|
| 48 | const base = `http://127.0.0.1:${server.address().port}`;
|
|---|
| 49 |
|
|---|
| 50 | const haal = async (pad) => {
|
|---|
| 51 | const r = await fetch(base + pad, { headers: { Accept: 'application/activity+json' } });
|
|---|
| 52 | return { status: r.status, body: r.status === 200 ? await r.json() : null };
|
|---|
| 53 | };
|
|---|
| 54 |
|
|---|
| 55 | // Elke collectie die een actor of een object naar buiten adverteert.
|
|---|
| 56 | const COLLECTIES = [
|
|---|
| 57 | '/ap/users/band/outbox',
|
|---|
| 58 | '/ap/users/band/followers',
|
|---|
| 59 | '/ap/users/band/following',
|
|---|
| 60 | '/ap/users/band/featured',
|
|---|
| 61 | '/ap/users/band/tracks',
|
|---|
| 62 | '/ap/users/band/playlists',
|
|---|
| 63 | '/ap/users/band/library',
|
|---|
| 64 | '/ap/users/band/library/followers',
|
|---|
| 65 | ];
|
|---|
| 66 |
|
|---|
| 67 | for (const pad of COLLECTIES) {
|
|---|
| 68 | test(`${pad}: first wijst naar een echte pagina`, async () => {
|
|---|
| 69 | const wortel = await haal(pad);
|
|---|
| 70 | assert.equal(wortel.status, 200, `${pad} gaf ${wortel.status}`);
|
|---|
| 71 |
|
|---|
| 72 | // Geen `first`? Dan belooft hij ook niets en valt er niets te controleren.
|
|---|
| 73 | if (!wortel.body.first) return;
|
|---|
| 74 |
|
|---|
| 75 | const eerste = typeof wortel.body.first === 'string' ? wortel.body.first : wortel.body.first.id;
|
|---|
| 76 | assert.ok(eerste, `${pad}: first zonder id`);
|
|---|
| 77 |
|
|---|
| 78 | const pagina = await haal(new URL(eerste).pathname + new URL(eerste).search);
|
|---|
| 79 | assert.equal(pagina.status, 200, `${pad}: first gaf ${pagina.status}`);
|
|---|
| 80 |
|
|---|
| 81 | // DE KERN: de pagina moet een pagina zijn en niet de collectie opnieuw.
|
|---|
| 82 | assert.match(pagina.body.type, /Page$/, `${pad}: first gaf type ${pagina.body.type}`);
|
|---|
| 83 | assert.equal(pagina.body.partOf, wortel.body.id, `${pad}: pagina hoort niet bij de collectie`);
|
|---|
| 84 | assert.notEqual(pagina.body.id, wortel.body.id, `${pad}: pagina deelt zijn id met de collectie`);
|
|---|
| 85 |
|
|---|
| 86 | // Een pagina MAG `first` dragen (AS2: CollectionPage erft van Collection,
|
|---|
| 87 | // en Funkwhale eist het zelfs op library-pagina's). Wat niet mag is een
|
|---|
| 88 | // `next` naar zichzelf: dat is de lus, en die zit in de verwijzing die een
|
|---|
| 89 | // lezer volgt, niet in de aanwezigheid van het veld.
|
|---|
| 90 | assert.notEqual(pagina.body.next, eerste, `${pad}: next wijst naar de pagina zelf`);
|
|---|
| 91 |
|
|---|
| 92 | // De items horen bij het type: geordend -> orderedItems, ongeordend ->
|
|---|
| 93 | // items. Een `Collection` met `orderedItems` is voor een strikte lezer leeg.
|
|---|
| 94 | const sleutel = pagina.body.type === 'CollectionPage' ? 'items' : 'orderedItems';
|
|---|
| 95 | assert.ok(Array.isArray(pagina.body[sleutel]), `${pad}: ${pagina.body.type} zonder ${sleutel}`);
|
|---|
| 96 | });
|
|---|
| 97 | }
|
|---|
| 98 |
|
|---|
| 99 | test.after(() => server.close());
|
|---|