| 1 | // De outbox pagineert nu ECHT door (shaer-sk4).
|
|---|
| 2 | //
|
|---|
| 3 | // Er stond geen paginering maar een KAP: de route haalde twintig posts uit SQL
|
|---|
| 4 | // en hield daar twintig items van over. Alles daarvoor was niet op een volgende
|
|---|
| 5 | // pagina maar helemaal onbereikbaar.
|
|---|
| 6 | //
|
|---|
| 7 | // Posts en tracks vlechten op datum, dus een offset over die twee kan niet met
|
|---|
| 8 | // twee losse queries -- je weet niet hoeveel van elk er in pagina drie horen.
|
|---|
| 9 | import { test } from 'node:test';
|
|---|
| 10 | import assert from 'node:assert/strict';
|
|---|
| 11 |
|
|---|
| 12 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 13 | process.env.PUBLIC_BASE_URL = 'https://ons.test';
|
|---|
| 14 |
|
|---|
| 15 | const dbMod = await import('../src/config/database.js');
|
|---|
| 16 | const db = dbMod.default;
|
|---|
| 17 | dbMod.initializeDatabase();
|
|---|
| 18 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 19 |
|
|---|
| 20 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 21 | .run('u1', 'u1', 'u1@t', 'x', 'god');
|
|---|
| 22 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'dev', 'Dev', 'u1');
|
|---|
| 23 |
|
|---|
| 24 | // 45 posts en 5 open tracks, met dagen ertussen zodat de volgorde vaststaat.
|
|---|
| 25 | const dag = (n) => new Date(Date.UTC(2026, 0, 1, 12) + (n - 1) * 86400000).toISOString();
|
|---|
| 26 | for (let i = 1; i <= 45; i++) {
|
|---|
| 27 | db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at)
|
|---|
| 28 | VALUES (?,?,?,?,?,?,'published',?)`)
|
|---|
| 29 | .run(`p${i}`, 's1', 'u1', `post-${i}`, `Post ${i}`, '<p>x</p>', dag(i));
|
|---|
| 30 | }
|
|---|
| 31 | const insM = db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1000)');
|
|---|
| 32 | const insT = db.prepare('INSERT INTO audio_tracks (id, site_id, title, duration, media_id, fedi_open, created_at) VALUES (?,?,?,?,?,1,?)');
|
|---|
| 33 | for (let i = 1; i <= 5; i++) {
|
|---|
| 34 | insM.run(`m${i}`, 's1', `t${i}.mp3`, `audio/t${i}.mp3`, 'audio/mpeg');
|
|---|
| 35 | insT.run(`t${i}`, 's1', `Track ${i}`, 100, `m${i}`, dag(20 + i));
|
|---|
| 36 | }
|
|---|
| 37 |
|
|---|
| 38 | test('de telling is het GEHEEL, niet de pagina', () => {
|
|---|
| 39 | const { totaal } = AP.outboxSlice('s1', { offset: 0, limit: 20 });
|
|---|
| 40 | assert.equal(totaal, 50, '45 posts + 5 tracks');
|
|---|
| 41 | });
|
|---|
| 42 |
|
|---|
| 43 | test('een pagina haalt uit BEIDE bronnen, gevlochten op datum', () => {
|
|---|
| 44 | // Pagina 2 valt precies over de tracks van dag 21..25 heen; met twee losse
|
|---|
| 45 | // queries en een offset per tabel zou dat niet kloppen.
|
|---|
| 46 | const p2 = AP.outboxSlice('s1', { offset: 20, limit: 20 });
|
|---|
| 47 | assert.equal(p2.posts.length + p2.tracks.length, 20);
|
|---|
| 48 | assert.ok(p2.tracks.length > 0, 'de tracks zitten midden in de reeks');
|
|---|
| 49 | assert.ok(p2.posts.length > 0);
|
|---|
| 50 | });
|
|---|
| 51 |
|
|---|
| 52 | test('opeenvolgende paginas overlappen niet en laten niets vallen', () => {
|
|---|
| 53 | const zie = new Set();
|
|---|
| 54 | let som = 0;
|
|---|
| 55 | for (let n = 1; n <= 3; n++) {
|
|---|
| 56 | const s = AP.outboxSlice('s1', { offset: (n - 1) * 20, limit: 20 });
|
|---|
| 57 | for (const p of s.posts) zie.add('p:' + p.id);
|
|---|
| 58 | for (const t of s.tracks) zie.add('t:' + t.id);
|
|---|
| 59 | som += s.posts.length + s.tracks.length;
|
|---|
| 60 | }
|
|---|
| 61 | assert.equal(som, 50, 'drie paginas dekken alles');
|
|---|
| 62 | assert.equal(zie.size, 50, 'en niets twee keer');
|
|---|
| 63 | });
|
|---|
| 64 |
|
|---|
| 65 | test('de laatste pagina is korter en de pagina erna is leeg', () => {
|
|---|
| 66 | assert.equal(AP.outboxSlice('s1', { offset: 40, limit: 20 }).posts.length
|
|---|
| 67 | + AP.outboxSlice('s1', { offset: 40, limit: 20 }).tracks.length, 10);
|
|---|
| 68 | const voorbij = AP.outboxSlice('s1', { offset: 60, limit: 20 });
|
|---|
| 69 | assert.equal(voorbij.posts.length + voorbij.tracks.length, 0);
|
|---|
| 70 | });
|
|---|
| 71 |
|
|---|
| 72 | test('de collectie biedt een next zolang er meer is', () => {
|
|---|
| 73 | const site = db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
|
|---|
| 74 | const { posts, tracks, totaal } = AP.outboxSlice('s1', { offset: 0, limit: 20 });
|
|---|
| 75 | const ob = AP.buildOutbox('https://ons.test', site, posts, tracks, { page: 1, totalItems: totaal, alGesneden: true });
|
|---|
| 76 | assert.equal(ob.totalItems, 50);
|
|---|
| 77 | assert.equal(ob.orderedItems.length, 20);
|
|---|
| 78 | assert.equal(ob.next, 'https://ons.test/ap/users/dev/outbox?page=2',
|
|---|
| 79 | 'zonder alGesneden zou een volle pagina zichzelf als de enige zien');
|
|---|
| 80 | });
|
|---|
| 81 |
|
|---|
| 82 | test('een fans-only post blijft buiten de telling van een vreemde', () => {
|
|---|
| 83 | db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at, fan_only)
|
|---|
| 84 | VALUES (?,?,?,?,?,?,'published',?,1)`)
|
|---|
| 85 | .run('geheim', 's1', 'u1', 'geheim', 'Geheim', '<p>x</p>', dag(28));
|
|---|
| 86 | assert.equal(AP.outboxSlice('s1', { fanOnly: false }).totaal, 50, 'een vreemde telt hem niet mee');
|
|---|
| 87 | assert.equal(AP.outboxSlice('s1', { fanOnly: true }).totaal, 51, 'een vriend wel');
|
|---|
| 88 | });
|
|---|