| 1 | // De exporter (shaer-1a6), getoetst aan het formaat in docs/EXPORT-FORMAT.md.
|
|---|
| 2 | //
|
|---|
| 3 | // De acceptatietest uit dat document is de MOEILIJKSTE post die Klonkt kan
|
|---|
| 4 | // maken, niet de makkelijkste: een poll met bijlagen, een quote-kaart, een
|
|---|
| 5 | // content warning en betaalde toegang. Die staat hieronder als eerste.
|
|---|
| 6 | //
|
|---|
| 7 | // Run: npm test
|
|---|
| 8 |
|
|---|
| 9 | import { test } from 'node:test';
|
|---|
| 10 | import assert from 'node:assert/strict';
|
|---|
| 11 | import fs from 'fs';
|
|---|
| 12 | import os from 'os';
|
|---|
| 13 | import path from 'path';
|
|---|
| 14 | import crypto from 'crypto';
|
|---|
| 15 |
|
|---|
| 16 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 17 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 18 |
|
|---|
| 19 | // De media-map moet bestaan voordat paths.js hem uitleest.
|
|---|
| 20 | const MEDIA = fs.mkdtempSync(path.join(os.tmpdir(), 'klonkt-media-'));
|
|---|
| 21 | process.env.MEDIA_PATH = MEDIA;
|
|---|
| 22 |
|
|---|
| 23 | const dbMod = await import('../src/config/database.js');
|
|---|
| 24 | const db = dbMod.default;
|
|---|
| 25 | dbMod.initializeDatabase();
|
|---|
| 26 | const AX = await import('../src/services/ArchiveExportService.js');
|
|---|
| 27 |
|
|---|
| 28 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 29 | .run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 30 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Mijn site', 'u1');
|
|---|
| 31 |
|
|---|
| 32 | fs.mkdirSync(path.join(MEDIA, '2026'), { recursive: true });
|
|---|
| 33 | const PLAATJE = Buffer.from('dit-zijn-de-bytes-van-een-plaatje');
|
|---|
| 34 | fs.writeFileSync(path.join(MEDIA, '2026', 'cover.jpg'), PLAATJE);
|
|---|
| 35 | const PLAATJE_HASH = crypto.createHash('sha256').update(PLAATJE).digest('hex');
|
|---|
| 36 |
|
|---|
| 37 | function post(id, extra = {}) {
|
|---|
| 38 | const kol = {
|
|---|
| 39 | id, site_id: 's1', slug: id, author_id: 'u1', title: 'Titel', content: '<p>inhoud</p>',
|
|---|
| 40 | status: 'published', published_at: '2026-08-01 10:00:00', created_at: '2026-08-01 10:00:00',
|
|---|
| 41 | updated_at: '2026-08-01 10:00:00', ...extra,
|
|---|
| 42 | };
|
|---|
| 43 | const namen = Object.keys(kol);
|
|---|
| 44 | db.prepare(`INSERT INTO posts (${namen.join(',')}) VALUES (${namen.map(() => '?').join(',')})`)
|
|---|
| 45 | .run(...namen.map((n) => kol[n]));
|
|---|
| 46 | }
|
|---|
| 47 |
|
|---|
| 48 | const lees = (uit, pad) => JSON.parse(uit.files.get(pad).toString('utf8'));
|
|---|
| 49 |
|
|---|
| 50 | test('de moeilijkste post overleeft de export heel', () => {
|
|---|
| 51 | post('zwaar', {
|
|---|
| 52 | title: 'Een zware post',
|
|---|
| 53 | content: '<p>body</p><img src="/media/2026/cover.jpg" alt="cover">',
|
|---|
| 54 | content_warning: 'spoiler', nsfw: 1, paid: 1, paid_min_cents: 500,
|
|---|
| 55 | quote_uri: 'https://mstdn.social/users/x/statuses/1', quote_actor: 'https://mstdn.social/users/x',
|
|---|
| 56 | poll_json: JSON.stringify({ multiple: false, options: [{ name: 'ja' }, { name: 'nee' }], endTime: '2026-08-09T10:00:00Z' }),
|
|---|
| 57 | language: 'nl', tags: 'muziek, code',
|
|---|
| 58 | });
|
|---|
| 59 | const uit = AX.buildArchive('me');
|
|---|
| 60 | const o = lees(uit, 'posts/zwaar.json');
|
|---|
| 61 |
|
|---|
| 62 | assert.equal(o.type, 'Question', 'een poll exporteert als Question');
|
|---|
| 63 | assert.deepEqual(o.oneOf.map((x) => x.name), ['ja', 'nee']);
|
|---|
| 64 | assert.equal(o.endTime, '2026-08-09T10:00:00Z');
|
|---|
| 65 | assert.equal(o.summary, 'spoiler', 'de content warning is de AS2 summary');
|
|---|
| 66 | assert.equal(o.sensitive, true);
|
|---|
| 67 | assert.equal(o.quoteUrl, 'https://mstdn.social/users/x/statuses/1');
|
|---|
| 68 | assert.equal(o['shaer:quoteActor'], 'https://mstdn.social/users/x');
|
|---|
| 69 | assert.equal(o['shaer:paidMinCents'], 500);
|
|---|
| 70 | assert.deepEqual(o.contentMap, { nl: '<p>body</p><img src="/media/2026/cover.jpg" alt="cover">' });
|
|---|
| 71 | assert.deepEqual(o.tag.map((t) => t.name), ['#muziek', '#code']);
|
|---|
| 72 | });
|
|---|
| 73 |
|
|---|
| 74 | test('een betaalde post exporteert de VOLLEDIGE inhoud, niet de teaser', () => {
|
|---|
| 75 | // buildNote() federeert bewust alleen een teaser. Zou de export die projectie
|
|---|
| 76 | // volgen, dan archiveer je je eigen betaalde post als samenvatting.
|
|---|
| 77 | const o = lees(AX.buildArchive('me'), 'posts/zwaar.json');
|
|---|
| 78 | assert.match(o.content, /<p>body<\/p>/);
|
|---|
| 79 | assert.equal(o['shaer:paid'], true);
|
|---|
| 80 | });
|
|---|
| 81 |
|
|---|
| 82 | test('de titel staat in name, niet in de content gebakken', () => {
|
|---|
| 83 | // Ook een verschil met de federatie-projectie: Mastodon negeert `name`, dus
|
|---|
| 84 | // daar gaat de titel de content in. Een archief hoort dat niet over te nemen.
|
|---|
| 85 | const o = lees(AX.buildArchive('me'), 'posts/zwaar.json');
|
|---|
| 86 | assert.equal(o.name, 'Een zware post');
|
|---|
| 87 | assert.equal(o.type, 'Question');
|
|---|
| 88 | assert.doesNotMatch(o.content, /<strong>Een zware post<\/strong>/);
|
|---|
| 89 | });
|
|---|
| 90 |
|
|---|
| 91 | test('media reizen mee als bytes, gesleuteld op hun inhoud', () => {
|
|---|
| 92 | const uit = AX.buildArchive('me');
|
|---|
| 93 | const o = lees(uit, 'posts/zwaar.json');
|
|---|
| 94 | const a = o.attachment[0];
|
|---|
| 95 | assert.equal(a['shaer:availability'], 'included');
|
|---|
| 96 | assert.equal(a['shaer:sha256'], PLAATJE_HASH);
|
|---|
| 97 | assert.equal(a.url, `media/${PLAATJE_HASH}.jpg`, 'de verwijzing is container-relatief');
|
|---|
| 98 | assert.equal(a['shaer:originalUrl'], 'https://klonkt.test/media/2026/cover.jpg');
|
|---|
| 99 | assert.deepEqual(uit.files.get(a.url), PLAATJE, 'en de bytes zitten er echt in');
|
|---|
| 100 | });
|
|---|
| 101 |
|
|---|
| 102 | test('een verdwenen bestand wordt gemeld, niet weggelaten', () => {
|
|---|
| 103 | // De derde mediastaat. Een lege medialijst zou een leugen zijn en een
|
|---|
| 104 | // verwijzing naar een ontbrekend bestand een kapot archief.
|
|---|
| 105 | post('kwijt', { content: '<p>x</p><img src="/media/2026/weg.jpg">' });
|
|---|
| 106 | const uit = AX.buildArchive('me');
|
|---|
| 107 | const a = lees(uit, 'posts/kwijt.json').attachment[0];
|
|---|
| 108 | assert.equal(a['shaer:availability'], 'missing');
|
|---|
| 109 | assert.equal(a.url, 'https://klonkt.test/media/2026/weg.jpg', 'de oorspronkelijke plek blijft staan');
|
|---|
| 110 | assert.equal(uit.counts.mediaMissing, 1);
|
|---|
| 111 | assert.equal(uit.missing[0].post, 'kwijt');
|
|---|
| 112 | });
|
|---|
| 113 |
|
|---|
| 114 | test('een pad buiten de media-map wordt niet gevolgd', () => {
|
|---|
| 115 | // Zonder deze grens trekt een verzonnen pad in oude inhoud een willekeurig
|
|---|
| 116 | // bestand van de schijf het archief in.
|
|---|
| 117 | post('sluipweg', { content: '<p>x</p><img src="/media/../../../etc/passwd">' });
|
|---|
| 118 | const uit = AX.buildArchive('me');
|
|---|
| 119 | const a = lees(uit, 'posts/sluipweg.json').attachment[0];
|
|---|
| 120 | assert.equal(a['shaer:availability'], 'missing');
|
|---|
| 121 | });
|
|---|
| 122 |
|
|---|
| 123 | test('antwoorden van anderen staan apart en zijn gemarkeerd als archief', () => {
|
|---|
| 124 | db.prepare(`INSERT INTO ap_interactions (kind, post_id, object_uri, actor_uri, actor_name, content, created_at)
|
|---|
| 125 | VALUES ('reply', 'zwaar', ?, ?, 'Anna', '<p>hoi</p>', '2026-08-02 10:00:00')`)
|
|---|
| 126 | .run('https://mstdn.social/users/anna/statuses/9', 'https://mstdn.social/users/anna');
|
|---|
| 127 | const uit = AX.buildArchive('me');
|
|---|
| 128 | const c = lees(uit, 'replies/zwaar.json');
|
|---|
| 129 | assert.equal(c['shaer:archive'], true, 'hierop keert een importer: nooit opnieuw bezorgen');
|
|---|
| 130 | assert.equal(c.totalItems, 1);
|
|---|
| 131 | assert.equal(c.orderedItems[0].id, 'https://mstdn.social/users/anna/statuses/9');
|
|---|
| 132 | assert.equal(uit.counts.replies, 1);
|
|---|
| 133 | });
|
|---|
| 134 |
|
|---|
| 135 | test('een gehoste track reist mee, met de gegevens die alleen in de database staan', () => {
|
|---|
| 136 | // Sinds v2 gaat audio NIET meer als media-bijlage mee. Dat kon ook nooit:
|
|---|
| 137 | // gehoste audio staat buiten MEDIA_ROOT, de exporter rekende er met
|
|---|
| 138 | // path.relative een /media/../audio/x.mp3 van, en de importer weigerde dat
|
|---|
| 139 | // terecht. Resultaat op soundfabrics.nl: 13 tracks in de lijst, nul bestanden.
|
|---|
| 140 | // Nu heeft het archief een eigen audio/-gebied.
|
|---|
| 141 | fs.mkdirSync(path.join(MEDIA, 'audio'), { recursive: true });
|
|---|
| 142 | const WAV = Buffer.from('RIFF-nep-audio');
|
|---|
| 143 | fs.writeFileSync(path.join(MEDIA, 'audio', 't1.mp3'), WAV);
|
|---|
| 144 | db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)')
|
|---|
| 145 | .run('m1', 's1', 't1.mp3', 'audio/mpeg', WAV.length, path.join(MEDIA, 'audio', 't1.mp3'));
|
|---|
| 146 | db.prepare(`INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, credit, license, link_spotify)
|
|---|
| 147 | VALUES ('tr1','s1','Kanonnen','Robin',212,'m1','opname 2026','CC BY','https://open.spotify.com/track/x')`).run();
|
|---|
| 148 | post('metaudio', { content: '<p>luister</p>[[track:tr1]]' });
|
|---|
| 149 |
|
|---|
| 150 | const uit = AX.buildArchive('me');
|
|---|
| 151 | const o = lees(uit, 'posts/metaudio.json');
|
|---|
| 152 | const t = o['shaer:audio'][0];
|
|---|
| 153 | assert.match(o.content, /\[\[track:tr1\]\]/, 'de shorthand blijft in de bron staan');
|
|---|
| 154 | assert.equal(t.name, 'Kanonnen');
|
|---|
| 155 | assert.equal(t.artist, 'Robin');
|
|---|
| 156 | assert.equal(t.license, 'CC BY');
|
|---|
| 157 | assert.deepEqual(t.url, ['https://open.spotify.com/track/x']);
|
|---|
| 158 |
|
|---|
| 159 | // Dit is de assertie die er echt toe doet: de BYTES zitten in het archief.
|
|---|
| 160 | assert.ok(String(t['shaer:media']).startsWith('audio/'), 'de track wijst naar het audio-gebied');
|
|---|
| 161 | assert.deepEqual(uit.files.get(t['shaer:media']), WAV, 'en daar staat het geluid ook echt');
|
|---|
| 162 |
|
|---|
| 163 | // En hij staat in de bibliotheek, los van de post.
|
|---|
| 164 | const lib = JSON.parse(uit.files.get('tracks.json').toString('utf8'));
|
|---|
| 165 | const rij = lib.orderedItems.find((x) => x.id === 'tr1');
|
|---|
| 166 | assert.equal(rij['shaer:availability'], 'included');
|
|---|
| 167 | assert.equal(rij['shaer:file'], t['shaer:media']);
|
|---|
| 168 | assert.equal(uit.counts.tracks, 1);
|
|---|
| 169 | });
|
|---|
| 170 |
|
|---|
| 171 | test('de HELE bibliotheek gaat mee, ook wat in geen enkel bericht staat', () => {
|
|---|
| 172 | // De reden dat dit bestaat: op sound-fabrics.com stonden 140 tracks en gingen
|
|---|
| 173 | // er 14 mee, want alleen wat met [[track:]] in een bericht stond werd
|
|---|
| 174 | // geexporteerd. Een verhuizing die je bibliotheek achterlaat is er geen.
|
|---|
| 175 | fs.mkdirSync(path.join(MEDIA, 'audio'), { recursive: true });
|
|---|
| 176 | const B = Buffer.from('RIFF-los-nummer');
|
|---|
| 177 | fs.writeFileSync(path.join(MEDIA, 'audio', 'los.mp3'), B);
|
|---|
| 178 | db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)')
|
|---|
| 179 | .run('m9', 's1', 'los.mp3', 'audio/mpeg', B.length, path.join(MEDIA, 'audio', 'los.mp3'));
|
|---|
| 180 | db.prepare("INSERT INTO audio_tracks (id, site_id, title, media_id) VALUES ('los','s1','Los nummer','m9')").run();
|
|---|
| 181 |
|
|---|
| 182 | const uit = AX.buildArchive('me');
|
|---|
| 183 | const lib = JSON.parse(uit.files.get('tracks.json').toString('utf8'));
|
|---|
| 184 | const rij = lib.orderedItems.find((x) => x.id === 'los');
|
|---|
| 185 | assert.ok(rij, 'een track zonder bericht hoort er gewoon in');
|
|---|
| 186 | assert.deepEqual(uit.files.get(rij['shaer:file']), B);
|
|---|
| 187 | });
|
|---|
| 188 |
|
|---|
| 189 | test('een track waarvan het bestand zoek is wordt gemeld, niet stil weggelaten', () => {
|
|---|
| 190 | db.prepare("INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES ('mz','s1','zoek.mp3','audio/mpeg',1,'/bestaat/niet/zoek.mp3')").run();
|
|---|
| 191 | db.prepare("INSERT INTO audio_tracks (id, site_id, title, media_id) VALUES ('zoek','s1','Zoek','mz')").run();
|
|---|
| 192 | const uit = AX.buildArchive('me');
|
|---|
| 193 | const rij = JSON.parse(uit.files.get('tracks.json').toString('utf8')).orderedItems.find((x) => x.id === 'zoek');
|
|---|
| 194 | assert.equal(rij['shaer:availability'], 'missing');
|
|---|
| 195 | assert.equal(rij['shaer:file'], undefined, 'geen verwijzing naar een bestand dat er niet is');
|
|---|
| 196 | assert.ok(uit.counts.audioMissing >= 1);
|
|---|
| 197 | assert.ok(uit.missing.some((m) => m.track === 'Zoek'), 'en de beller kan het aan de gebruiker melden');
|
|---|
| 198 | });
|
|---|
| 199 |
|
|---|
| 200 | test('audio wordt gezocht zoals de SPELER hem zoekt, niet zoals de database hem onthoudt', async () => {
|
|---|
| 201 | // De stille moordenaar op sound-fabrics.com: na een dataverhuizing wees
|
|---|
| 202 | // storage_path voor 124 van de 139 tracks nog naar /srv/prutfolio/storage/audio.
|
|---|
| 203 | // De speler merkte er niets van, want die pakt AUDIO_ROOT + bestandsnaam en
|
|---|
| 204 | // negeert storage_path. De exporter las wel storage_path, vond niets, en liet
|
|---|
| 205 | // 124 nummers weg zonder dat iemand het zag.
|
|---|
| 206 | //
|
|---|
| 207 | // Deze test pint de VOLGORDE vast met een nep-fs, want dat is precies waar de
|
|---|
| 208 | // twee uit elkaar liepen. AUDIO_ROOT ligt bij import van paths.js al vast, dus
|
|---|
| 209 | // hem hier omzetten kan niet meer; de volgorde toetsen wel.
|
|---|
| 210 | const { resolveAudioPath, AUDIO_ROOT } = await import('../src/config/paths.js');
|
|---|
| 211 | const gevraagd = [];
|
|---|
| 212 | const nepFs = {
|
|---|
| 213 | statSync(p) {
|
|---|
| 214 | gevraagd.push(p);
|
|---|
| 215 | if (p === path.join(AUDIO_ROOT, 'x.mp3')) return { isFile: () => true };
|
|---|
| 216 | throw new Error('ENOENT');
|
|---|
| 217 | },
|
|---|
| 218 | };
|
|---|
| 219 | const uit = resolveAudioPath('/een/heel/oud/pad/x.mp3', nepFs);
|
|---|
| 220 | assert.equal(uit, path.join(AUDIO_ROOT, 'x.mp3'), 'de speler-plek wint');
|
|---|
| 221 | assert.equal(gevraagd[0], path.join(AUDIO_ROOT, 'x.mp3'), 'en wordt als EERSTE geprobeerd');
|
|---|
| 222 |
|
|---|
| 223 | // Staat hij daar niet, dan telt het opgeslagen pad alsnog: installaties die
|
|---|
| 224 | // hun audio ergens anders hebben mogen niet stuk.
|
|---|
| 225 | const gevraagd2 = [];
|
|---|
| 226 | const nepFs2 = {
|
|---|
| 227 | statSync(p) {
|
|---|
| 228 | gevraagd2.push(p);
|
|---|
| 229 | if (p === path.resolve('/ergens/anders/y.mp3')) return { isFile: () => true };
|
|---|
| 230 | throw new Error('ENOENT');
|
|---|
| 231 | },
|
|---|
| 232 | };
|
|---|
| 233 | assert.equal(resolveAudioPath('/ergens/anders/y.mp3', nepFs2), path.resolve('/ergens/anders/y.mp3'));
|
|---|
| 234 | assert.equal(resolveAudioPath('', nepFs2), null);
|
|---|
| 235 | });
|
|---|
| 236 |
|
|---|
| 237 | test('de poster van een audio-bijlage gaat mee', () => {
|
|---|
| 238 | // Gevonden bij de export van echte beta-data: c2s_attachments draagt naast url
|
|---|
| 239 | // ook een poster, en die viel er stil uit.
|
|---|
| 240 | fs.writeFileSync(path.join(MEDIA, '2026', 'opname.m4a'), Buffer.from('audio'));
|
|---|
| 241 | fs.writeFileSync(path.join(MEDIA, '2026', 'opname.m4a.poster.webp'), Buffer.from('poster'));
|
|---|
| 242 | post('metposter', {
|
|---|
| 243 | c2s_attachments: JSON.stringify([{
|
|---|
| 244 | url: '/media/2026/opname.m4a', mediaType: 'audio/mp4', name: 'opname.m4a',
|
|---|
| 245 | poster: '/media/2026/opname.m4a.poster.webp',
|
|---|
| 246 | }]),
|
|---|
| 247 | });
|
|---|
| 248 | const o = lees(AX.buildArchive('me'), 'posts/metposter.json');
|
|---|
| 249 | assert.equal(o.attachment.length, 2, 'de opname en de poster');
|
|---|
| 250 | assert.ok(o.attachment.every((a) => a['shaer:availability'] === 'included'));
|
|---|
| 251 | });
|
|---|
| 252 |
|
|---|
| 253 | test('een SQL-tijdstempel wordt als UTC gelezen, niet als lokale tijd', () => {
|
|---|
| 254 | // SQLite schrijft CURRENT_TIMESTAMP in UTC zonder zone erbij. Date.parse leest
|
|---|
| 255 | // die vorm als LOKALE tijd, en dan schuift elk tijdstempel in het archief mee
|
|---|
| 256 | // met de machine die de export draait -- in Amsterdam twee uur.
|
|---|
| 257 | //
|
|---|
| 258 | // Deze test is alleen zinvol onder een NIET-UTC tijdzone; draai hem daarom ook
|
|---|
| 259 | // eens als: TZ=Europe/Amsterdam node --test test/archive-export.test.js
|
|---|
| 260 | post('tijdstip', { published_at: '2026-07-01 12:56:10', created_at: '2026-07-01 12:56:10' });
|
|---|
| 261 | const o = lees(AX.buildArchive('me'), 'posts/tijdstip.json');
|
|---|
| 262 | assert.equal(o.published, '2026-07-01T12:56:10.000Z',
|
|---|
| 263 | 'het moment uit de database is UTC en hoort dat te blijven');
|
|---|
| 264 | });
|
|---|
| 265 |
|
|---|
| 266 | test('een concept gaat gewoon mee', () => {
|
|---|
| 267 | post('concept', { status: 'draft', published_at: null });
|
|---|
| 268 | const o = lees(AX.buildArchive('me'), 'posts/concept.json');
|
|---|
| 269 | assert.equal(o['shaer:status'], 'draft');
|
|---|
| 270 | });
|
|---|
| 271 |
|
|---|
| 272 | test('twee exports van ongewijzigde inhoud zijn gelijk, op exportedAt na', () => {
|
|---|
| 273 | const a = AX.buildArchive('me', { exportedAt: 'X' });
|
|---|
| 274 | const b = AX.buildArchive('me', { exportedAt: 'Y' });
|
|---|
| 275 | assert.deepEqual([...a.files.keys()].sort(), [...b.files.keys()].sort());
|
|---|
| 276 | for (const pad of a.files.keys()) {
|
|---|
| 277 | if (pad === 'manifest.json') continue;
|
|---|
| 278 | assert.deepEqual(a.files.get(pad), b.files.get(pad), `${pad} verschilt`);
|
|---|
| 279 | }
|
|---|
| 280 | assert.deepEqual(a.manifest.files, b.manifest.files, 'de checksum-kaart hoort gelijk te zijn');
|
|---|
| 281 | });
|
|---|
| 282 |
|
|---|
| 283 | test('en de zip die eruit komt ook', () => {
|
|---|
| 284 | const a = AX.buildArchive('me', { exportedAt: 'X' });
|
|---|
| 285 | const b = AX.buildArchive('me', { exportedAt: 'X' });
|
|---|
| 286 | assert.deepEqual(AX.zipArchive(a.files), AX.zipArchive(b.files), 'zonder vaste mtime en volgorde is dit toeval');
|
|---|
| 287 | });
|
|---|
| 288 |
|
|---|
| 289 | test('de zip is door unzip te lezen', async () => {
|
|---|
| 290 | // Een zelfgeschreven zip die alleen zichzelf kan lezen is geen uitwisselformaat.
|
|---|
| 291 | const uit = AX.buildArchive('me');
|
|---|
| 292 | const zip = path.join(MEDIA, 'test.zip');
|
|---|
| 293 | fs.writeFileSync(zip, AX.zipArchive(uit.files));
|
|---|
| 294 | const { execFileSync } = await import('child_process');
|
|---|
| 295 | let lijst;
|
|---|
| 296 | try { lijst = execFileSync('unzip', ['-l', zip], { encoding: 'utf8' }); }
|
|---|
| 297 | catch (e) { if (e.code === 'ENOENT') return; throw e; } // geen unzip op deze machine
|
|---|
| 298 | assert.match(lijst, /manifest\.json/);
|
|---|
| 299 | assert.match(lijst, /posts\/zwaar\.json/);
|
|---|
| 300 | });
|
|---|
| 301 |
|
|---|
| 302 | test('het manifest draagt de origin, want daar hangt het behoud van AP-ids aan', () => {
|
|---|
| 303 | const m = AX.buildArchive('me').manifest;
|
|---|
| 304 | assert.equal(m.origin, 'https://klonkt.test');
|
|---|
| 305 | assert.equal(m.formatVersion, 2, 'v2 = audio zit er echt in');
|
|---|
| 306 | assert.equal(m.actor, 'https://klonkt.test/ap/users/me');
|
|---|
| 307 | });
|
|---|
| 308 |
|
|---|
| 309 | test('posts van een ANDERE site komen er niet in', () => {
|
|---|
| 310 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s2', 'buur', 'Buur', 'u1');
|
|---|
| 311 | db.prepare('INSERT INTO posts (id, site_id, slug, author_id, title, content, status) VALUES (?,?,?,?,?,?,?)')
|
|---|
| 312 | .run('vanbuur', 's2', 'vanbuur', 'u1', 'Niet van mij', '<p>x</p>', 'published');
|
|---|
| 313 | assert.equal(AX.buildArchive('me').files.has('posts/vanbuur.json'), false);
|
|---|
| 314 | });
|
|---|
| 315 |
|
|---|
| 316 | test.after(() => { try { fs.rmSync(MEDIA, { recursive: true, force: true }); } catch { /* niets */ } });
|
|---|