source: Klonkt/test/ap-collection-paging.test.js@ 5e16b8a

main
Last change on this file since 5e16b8a was 5e16b8a, checked in by Robin <roboburr@…>, 4 weeks ago

AP: eerste pagina serveren op elke collectie die first belooft

/ap/users/dev/library gaf 200 met first en last, maar ?page=1 gaf de
bibliotheek opnieuw: zelfde id, type Library, first weer naar zichzelf.
open.audio volgde die verwijzing bij "search a remote object" en gaf een
500 op onze URL terug.

pagedCollection kon dit al; vier routes gaven ?page= door en vier niet
(featured, tracks, playlists, library, library/followers). De bouwers
nemen nu een page aan en de routes reiken paginaNr(req) aan.

De test loopt over HTTP en in een lus over alle collecties. Over HTTP
omdat de unit-test op buildLibrary er al stond en groen was terwijl het
gat openstond -- het zat in de doorgifte, niet in de bouwer. In een lus
omdat het gat juist ontstond doordat de helft van de routes het wel had:
een rijtje losse gevallen vraagt om dezelfde vergeetachtigheid. Hij vond
meteen featured, die ik in deze ronde ook was vergeten.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 4.3 KB
Line 
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.
17import { test } from 'node:test';
18import assert from 'node:assert/strict';
19
20process.env.DATABASE_PATH = ':memory:';
21process.env.PUBLIC_BASE_URL = 'https://test.example';
22
23const dbMod = await import('../src/config/database.js');
24const db = dbMod.default;
25dbMod.initializeDatabase();
26const express = (await import('express')).default;
27const routes = (await import('../src/routes/activitypub.js')).default;
28
29db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
30 .run('u1', 'u1', 'u1@t', 'x', 'god');
31db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
32 .run('s1', 'band', 'De Band', 'u1');
33
34const insM = db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1)');
35const insT = db.prepare('INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, fedi_open, position) VALUES (?,?,?,?,?,?,1,?)');
36for (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}
40db.prepare("INSERT INTO playlists (id, site_id, title, kind) VALUES ('plaat','s1','De Plaat','album')").run();
41db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('plaat','t1',1)").run();
42db.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
45const app = express(); app.use(routes);
46const server = app.listen(0);
47await new Promise((r) => server.once('listening', r));
48const base = `http://127.0.0.1:${server.address().port}`;
49
50const 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.
56const 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
67for (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 // En hij mag niet zelf weer een `first` dragen -- dat is de lus waar
87 // open.audio in liep.
88 assert.equal(pagina.body.first, undefined, `${pad}: de pagina draagt zelf een first`);
89 });
90}
91
92test.after(() => server.close());
Note: See TracBrowser for help on using the repository browser.