| 1 | // De importer (shaer-pmr), getoetst aan het formaat in docs/EXPORT-FORMAT.md.
|
|---|
| 2 | //
|
|---|
| 3 | // De zwaarste toets is de RONDGANG: exporteer de moeilijkste post die Klonkt kan
|
|---|
| 4 | // maken, gooi hem weg, lees hem terug, en kijk of er nog hetzelfde staat. Een
|
|---|
| 5 | // exporter en een importer die elk apart kloppen maar niet op elkaar aansluiten
|
|---|
| 6 | // zijn samen niets waard.
|
|---|
| 7 | //
|
|---|
| 8 | // Run: npm test
|
|---|
| 9 |
|
|---|
| 10 | import { test } from 'node:test';
|
|---|
| 11 | import assert from 'node:assert/strict';
|
|---|
| 12 | import fs from 'fs';
|
|---|
| 13 | import os from 'os';
|
|---|
| 14 | import path from 'path';
|
|---|
| 15 |
|
|---|
| 16 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 17 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 18 | const MEDIA = fs.mkdtempSync(path.join(os.tmpdir(), 'klonkt-imp-'));
|
|---|
| 19 | process.env.MEDIA_PATH = MEDIA;
|
|---|
| 20 |
|
|---|
| 21 | const dbMod = await import('../src/config/database.js');
|
|---|
| 22 | const db = dbMod.default;
|
|---|
| 23 | dbMod.initializeDatabase();
|
|---|
| 24 | const AX = await import('../src/services/ArchiveExportService.js');
|
|---|
| 25 | const AI = await import('../src/services/ArchiveImportService.js');
|
|---|
| 26 |
|
|---|
| 27 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 28 | .run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 29 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Mijn site', 'u1');
|
|---|
| 30 |
|
|---|
| 31 | fs.mkdirSync(path.join(MEDIA, '2026'), { recursive: true });
|
|---|
| 32 | const PLAATJE = Buffer.from('bytes-van-de-cover');
|
|---|
| 33 | fs.writeFileSync(path.join(MEDIA, '2026', 'cover.jpg'), PLAATJE);
|
|---|
| 34 |
|
|---|
| 35 | db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, excerpt, status, published_at,
|
|---|
| 36 | created_at, updated_at, content_warning, nsfw, paid, paid_min_cents, quote_uri, quote_actor,
|
|---|
| 37 | poll_json, language, tags, pinned, view_count)
|
|---|
| 38 | VALUES ('zwaar','s1','zware-post','u1','Een zware post',
|
|---|
| 39 | '<p>body</p><img src="/media/2026/cover.jpg" alt="cover">','de teaser','published',
|
|---|
| 40 | '2026-08-01 10:00:00','2026-08-01 10:00:00','2026-08-01 12:00:00','spoiler',1,1,500,
|
|---|
| 41 | 'https://mstdn.social/users/x/statuses/1','https://mstdn.social/users/x',
|
|---|
| 42 | '{"multiple":false,"options":[{"name":"ja"},{"name":"nee"}],"endTime":"2026-08-09T10:00:00Z"}',
|
|---|
| 43 | 'nl','muziek, code',1,42)`).run();
|
|---|
| 44 | db.prepare(`INSERT INTO ap_interactions (kind, post_id, object_uri, actor_uri, actor_name, actor_handle, content, published, created_at)
|
|---|
| 45 | VALUES ('reply','zwaar',?,?,'Anna','@anna@mstdn.social','<p>hoi</p>','2026-08-02T10:00:00Z','2026-08-02 10:00:00')`)
|
|---|
| 46 | .run('https://mstdn.social/users/anna/statuses/9', 'https://mstdn.social/users/anna');
|
|---|
| 47 |
|
|---|
| 48 | const ARCHIEF = AX.buildArchive('me', { exportedAt: 'X' });
|
|---|
| 49 | const leeg = () => {
|
|---|
| 50 | db.prepare('DELETE FROM posts').run();
|
|---|
| 51 | db.prepare('DELETE FROM ap_interactions').run();
|
|---|
| 52 | try { fs.rmSync(path.join(MEDIA, '2026'), { recursive: true, force: true }); } catch { /* niets */ }
|
|---|
| 53 | };
|
|---|
| 54 |
|
|---|
| 55 | test('de rondgang: wat eruit ging komt er hetzelfde weer in', () => {
|
|---|
| 56 | leeg();
|
|---|
| 57 | const r = AI.importArchive(ARCHIEF.files, { slug: 'me' });
|
|---|
| 58 | assert.equal(r.posts, 1);
|
|---|
| 59 | assert.equal(r.idsBehouden, true, 'zelfde origin, dus de AP-ids blijven');
|
|---|
| 60 |
|
|---|
| 61 | const p = db.prepare('SELECT * FROM posts WHERE id = ?').get('zwaar');
|
|---|
| 62 | assert.ok(p, 'het post-id is behouden -- daar wijzen de boosts elders naar');
|
|---|
| 63 | assert.equal(p.title, 'Een zware post');
|
|---|
| 64 | assert.equal(p.slug, 'zware-post');
|
|---|
| 65 | assert.match(p.content, /<p>body<\/p>/);
|
|---|
| 66 | assert.equal(p.excerpt, 'de teaser');
|
|---|
| 67 | assert.equal(p.content_warning, 'spoiler');
|
|---|
| 68 | assert.equal(p.nsfw, 1);
|
|---|
| 69 | assert.equal(p.paid, 1);
|
|---|
| 70 | assert.equal(p.paid_min_cents, 500);
|
|---|
| 71 | assert.equal(p.quote_uri, 'https://mstdn.social/users/x/statuses/1');
|
|---|
| 72 | assert.equal(p.language, 'nl');
|
|---|
| 73 | assert.equal(p.tags, 'muziek, code');
|
|---|
| 74 | assert.equal(p.pinned, 1);
|
|---|
| 75 | assert.equal(p.view_count, 42);
|
|---|
| 76 | assert.deepEqual(JSON.parse(p.poll_json).options.map((o) => o.name), ['ja', 'nee']);
|
|---|
| 77 | });
|
|---|
| 78 |
|
|---|
| 79 | test('en de media staan terug op hun oude plek, want de content wijst daarheen', () => {
|
|---|
| 80 | const op = path.join(MEDIA, '2026', 'cover.jpg');
|
|---|
| 81 | assert.ok(fs.existsSync(op), 'anders is de <img> in de body een dode verwijzing');
|
|---|
| 82 | assert.deepEqual(fs.readFileSync(op), PLAATJE);
|
|---|
| 83 | });
|
|---|
| 84 |
|
|---|
| 85 | test('de antwoorden komen terug als archief', () => {
|
|---|
| 86 | const r = db.prepare("SELECT * FROM ap_interactions WHERE post_id = 'zwaar'").all();
|
|---|
| 87 | assert.equal(r.length, 1);
|
|---|
| 88 | assert.equal(r[0].actor_name, 'Anna');
|
|---|
| 89 | assert.equal(r[0].object_uri, 'https://mstdn.social/users/anna/statuses/9');
|
|---|
| 90 | });
|
|---|
| 91 |
|
|---|
| 92 | test('twee keer importeren levert geen dubbele posts op', () => {
|
|---|
| 93 | const voor = db.prepare('SELECT COUNT(*) AS n FROM posts').get().n;
|
|---|
| 94 | const r = AI.importArchive(ARCHIEF.files, { slug: 'me' });
|
|---|
| 95 | assert.equal(r.overgeslagen, 1, 'de bestaande post wordt overgeslagen, niet overschreven');
|
|---|
| 96 | assert.equal(r.posts, 0);
|
|---|
| 97 | assert.equal(db.prepare('SELECT COUNT(*) AS n FROM posts').get().n, voor);
|
|---|
| 98 | });
|
|---|
| 99 |
|
|---|
| 100 | test('overschrijven gebeurt alleen als je erom vraagt', () => {
|
|---|
| 101 | db.prepare("UPDATE posts SET title = 'handmatig gewijzigd' WHERE id = 'zwaar'").run();
|
|---|
| 102 | AI.importArchive(ARCHIEF.files, { slug: 'me' });
|
|---|
| 103 | assert.equal(db.prepare("SELECT title FROM posts WHERE id = 'zwaar'").get().title, 'handmatig gewijzigd',
|
|---|
| 104 | 'zonder --overwrite blijft wat er staat staan');
|
|---|
| 105 | const r = AI.importArchive(ARCHIEF.files, { slug: 'me', overwrite: true });
|
|---|
| 106 | assert.equal(r.overschreven, 1);
|
|---|
| 107 | assert.equal(db.prepare("SELECT title FROM posts WHERE id = 'zwaar'").get().title, 'Een zware post');
|
|---|
| 108 | });
|
|---|
| 109 |
|
|---|
| 110 | test('een droogloop schrijft niets', () => {
|
|---|
| 111 | leeg();
|
|---|
| 112 | const r = AI.importArchive(ARCHIEF.files, { slug: 'me', dryRun: true });
|
|---|
| 113 | assert.equal(r.posts, 1, 'hij vertelt wel wat er zou gebeuren');
|
|---|
| 114 | assert.equal(db.prepare('SELECT COUNT(*) AS n FROM posts').get().n, 0);
|
|---|
| 115 | assert.equal(fs.existsSync(path.join(MEDIA, '2026', 'cover.jpg')), false);
|
|---|
| 116 | });
|
|---|
| 117 |
|
|---|
| 118 | test('een nieuwere formaatversie wordt in zijn GEHEEL geweigerd', () => {
|
|---|
| 119 | // Niet half lezen: een half begrepen herstel ziet eruit alsof het gelukt is.
|
|---|
| 120 | const files = new Map(ARCHIEF.files);
|
|---|
| 121 | const m = JSON.parse(files.get('manifest.json').toString());
|
|---|
| 122 | m.formatVersion = 99;
|
|---|
| 123 | files.set('manifest.json', Buffer.from(JSON.stringify(m)));
|
|---|
| 124 | assert.throws(() => AI.importArchive(files, { slug: 'me' }), /nieuwer dan deze Klonkt kent/);
|
|---|
| 125 | assert.equal(db.prepare('SELECT COUNT(*) AS n FROM posts').get().n, 0, 'en er is niets geschreven');
|
|---|
| 126 | });
|
|---|
| 127 |
|
|---|
| 128 | test('een andere origin levert NIEUWE ids op, met een waarschuwing', () => {
|
|---|
| 129 | // Oude ids houden op een ander domein zou objecten publiceren onder een id dat
|
|---|
| 130 | // je niet beheert -- andere servers halen dat daar op, en het is bovendien een
|
|---|
| 131 | // vervalsingsoppervlak.
|
|---|
| 132 | leeg();
|
|---|
| 133 | const files = new Map(ARCHIEF.files);
|
|---|
| 134 | const m = JSON.parse(files.get('manifest.json').toString());
|
|---|
| 135 | m.origin = 'https://ergens-anders.test';
|
|---|
| 136 | files.set('manifest.json', Buffer.from(JSON.stringify(m)));
|
|---|
| 137 | const r = AI.importArchive(files, { slug: 'me' });
|
|---|
| 138 | assert.equal(r.idsBehouden, false);
|
|---|
| 139 | assert.match(r.waarschuwingen.join(' '), /origin verschilt/);
|
|---|
| 140 | assert.equal(db.prepare("SELECT COUNT(*) AS n FROM posts WHERE id = 'zwaar'").get().n, 0);
|
|---|
| 141 | assert.equal(db.prepare('SELECT COUNT(*) AS n FROM posts').get().n, 1, 'wel geimporteerd, met een nieuw id');
|
|---|
| 142 | });
|
|---|
| 143 |
|
|---|
| 144 | test('ontbrekende media worden geteld en gemeld', () => {
|
|---|
| 145 | leeg();
|
|---|
| 146 | const files = new Map(ARCHIEF.files);
|
|---|
| 147 | const pad = [...files.keys()].find((k) => k.startsWith('posts/'));
|
|---|
| 148 | const o = JSON.parse(files.get(pad).toString());
|
|---|
| 149 | o.attachment = [{ type: 'Image', mediaType: 'image/jpeg', url: 'https://oud.test/media/weg.jpg', 'shaer:availability': 'missing', 'shaer:originalUrl': 'https://oud.test/media/weg.jpg' }];
|
|---|
| 150 | files.set(pad, Buffer.from(JSON.stringify(o)));
|
|---|
| 151 | const r = AI.importArchive(files, { slug: 'me' });
|
|---|
| 152 | assert.equal(r.mediaMissing, 1);
|
|---|
| 153 | assert.equal(r.gemist[0].url, 'https://oud.test/media/weg.jpg');
|
|---|
| 154 | });
|
|---|
| 155 |
|
|---|
| 156 | test('een archief dat zijn eigen bestand mist wordt apart gemeld', () => {
|
|---|
| 157 | // 'included' met een ontbrekend bestand is een KAPOT archief, geen verdwenen
|
|---|
| 158 | // media. Die twee op een hoop gooien verbergt een echt probleem.
|
|---|
| 159 | leeg();
|
|---|
| 160 | const files = new Map(ARCHIEF.files);
|
|---|
| 161 | for (const k of [...files.keys()]) if (k.startsWith('media/')) files.delete(k);
|
|---|
| 162 | const r = AI.importArchive(files, { slug: 'me' });
|
|---|
| 163 | assert.equal(r.mediaMissing, 0);
|
|---|
| 164 | assert.match(r.waarschuwingen.join(' '), /dat er niet in zit/);
|
|---|
| 165 | });
|
|---|
| 166 |
|
|---|
| 167 | test('een bestand met een verkeerde checksum wordt niet weggeschreven', () => {
|
|---|
| 168 | leeg();
|
|---|
| 169 | const files = new Map(ARCHIEF.files);
|
|---|
| 170 | const k = [...files.keys()].find((x) => x.startsWith('media/'));
|
|---|
| 171 | files.set(k, Buffer.from('iets heel anders'));
|
|---|
| 172 | const r = AI.importArchive(files, { slug: 'me' });
|
|---|
| 173 | assert.match(r.waarschuwingen.join(' '), /checksum klopt niet/);
|
|---|
| 174 | assert.equal(fs.existsSync(path.join(MEDIA, '2026', 'cover.jpg')), false);
|
|---|
| 175 | });
|
|---|
| 176 |
|
|---|
| 177 | test('een antwoordenbundel zonder archiefmarkering wordt overgeslagen', () => {
|
|---|
| 178 | leeg();
|
|---|
| 179 | const files = new Map(ARCHIEF.files);
|
|---|
| 180 | const pad = [...files.keys()].find((k) => k.startsWith('replies/'));
|
|---|
| 181 | const c = JSON.parse(files.get(pad).toString());
|
|---|
| 182 | delete c['shaer:archive'];
|
|---|
| 183 | files.set(pad, Buffer.from(JSON.stringify(c)));
|
|---|
| 184 | const r = AI.importArchive(files, { slug: 'me' });
|
|---|
| 185 | assert.equal(r.replies, 0);
|
|---|
| 186 | assert.match(r.waarschuwingen.join(' '), /niet gemarkeerd als archief/);
|
|---|
| 187 | });
|
|---|
| 188 |
|
|---|
| 189 | test('een pad buiten de media-map wordt niet geschreven', () => {
|
|---|
| 190 | leeg();
|
|---|
| 191 | const files = new Map(ARCHIEF.files);
|
|---|
| 192 | const pad = [...files.keys()].find((k) => k.startsWith('posts/'));
|
|---|
| 193 | const o = JSON.parse(files.get(pad).toString());
|
|---|
| 194 | const mk = [...files.keys()].find((k) => k.startsWith('media/'));
|
|---|
| 195 | o.attachment = [{ type: 'Image', mediaType: 'image/jpeg', url: mk, 'shaer:availability': 'included', 'shaer:originalUrl': '/media/../../../tmp/klonkt-inbraak' }];
|
|---|
| 196 | files.set(pad, Buffer.from(JSON.stringify(o)));
|
|---|
| 197 | AI.importArchive(files, { slug: 'me' });
|
|---|
| 198 | assert.equal(fs.existsSync('/tmp/klonkt-inbraak'), false);
|
|---|
| 199 | });
|
|---|
| 200 |
|
|---|
| 201 | test('readable/ wordt nooit gelezen', () => {
|
|---|
| 202 | // Dat is de hele reden dat het afgeleid is: twee bronnen die allebei gelezen
|
|---|
| 203 | // worden gaan uiteenlopen.
|
|---|
| 204 | leeg();
|
|---|
| 205 | const files = new Map(ARCHIEF.files);
|
|---|
| 206 | for (const k of [...files.keys()]) if (k.startsWith('readable/')) files.set(k, Buffer.from('ONZIN'));
|
|---|
| 207 | const r = AI.importArchive(files, { slug: 'me' });
|
|---|
| 208 | assert.equal(r.posts, 1);
|
|---|
| 209 | assert.match(db.prepare("SELECT content FROM posts WHERE id = 'zwaar'").get().content, /<p>body<\/p>/);
|
|---|
| 210 | });
|
|---|
| 211 |
|
|---|
| 212 | test('de zip-lezer haalt hetzelfde eruit als de map-lezer', () => {
|
|---|
| 213 | const zip = path.join(MEDIA, 'rond.zip');
|
|---|
| 214 | fs.writeFileSync(zip, AX.zipArchive(ARCHIEF.files));
|
|---|
| 215 | const uit = AI.readArchive(zip);
|
|---|
| 216 | assert.deepEqual([...uit.keys()].sort(), [...ARCHIEF.files.keys()].sort());
|
|---|
| 217 | for (const k of uit.keys()) assert.deepEqual(uit.get(k), ARCHIEF.files.get(k), `${k} verschilt`);
|
|---|
| 218 | });
|
|---|
| 219 |
|
|---|
| 220 | test('een GECOMPRIMEERDE zip van elders wordt ook gelezen', async () => {
|
|---|
| 221 | // Onze eigen zip is store-only. Zou de lezer alleen dat aankunnen, dan is het
|
|---|
| 222 | // geen uitwisselformaat maar een privé-doosje.
|
|---|
| 223 | const zlib = await import('zlib');
|
|---|
| 224 | const inhoud = Buffer.from('{"formatVersion":1,"origin":"x"}');
|
|---|
| 225 | const naam = Buffer.from('manifest.json', 'utf8');
|
|---|
| 226 | const comp = zlib.deflateRawSync(inhoud);
|
|---|
| 227 | const crc = (() => {
|
|---|
| 228 | let c = -1;
|
|---|
| 229 | for (const b of inhoud) { c ^= b; for (let k = 0; k < 8; k++) c = c & 1 ? (c >>> 1) ^ 0xEDB88320 : c >>> 1; }
|
|---|
| 230 | return (c ^ -1) >>> 0;
|
|---|
| 231 | })();
|
|---|
| 232 | const lh = Buffer.alloc(30);
|
|---|
| 233 | lh.writeUInt32LE(0x04034b50, 0); lh.writeUInt16LE(20, 4); lh.writeUInt16LE(8, 8);
|
|---|
| 234 | lh.writeUInt32LE(crc, 14); lh.writeUInt32LE(comp.length, 18); lh.writeUInt32LE(inhoud.length, 22);
|
|---|
| 235 | lh.writeUInt16LE(naam.length, 26);
|
|---|
| 236 | const ch = Buffer.alloc(46);
|
|---|
| 237 | ch.writeUInt32LE(0x02014b50, 0); ch.writeUInt16LE(8, 10);
|
|---|
| 238 | ch.writeUInt32LE(crc, 16); ch.writeUInt32LE(comp.length, 20); ch.writeUInt32LE(inhoud.length, 24);
|
|---|
| 239 | ch.writeUInt16LE(naam.length, 28); ch.writeUInt32LE(0, 42);
|
|---|
| 240 | const eocd = Buffer.alloc(22);
|
|---|
| 241 | eocd.writeUInt32LE(0x06054b50, 0); eocd.writeUInt16LE(1, 8); eocd.writeUInt16LE(1, 10);
|
|---|
| 242 | eocd.writeUInt32LE(46 + naam.length, 12); eocd.writeUInt32LE(30 + naam.length + comp.length, 16);
|
|---|
| 243 | const zip = path.join(MEDIA, 'deflate.zip');
|
|---|
| 244 | fs.writeFileSync(zip, Buffer.concat([lh, naam, comp, ch, naam, eocd]));
|
|---|
| 245 | assert.deepEqual(AI.readArchiveZip(fs.readFileSync(zip)).get('manifest.json'), inhoud);
|
|---|
| 246 | });
|
|---|
| 247 |
|
|---|
| 248 | test('een gehoste track overleeft de rondgang, met zijn bestand erbij', () => {
|
|---|
| 249 | leeg();
|
|---|
| 250 | db.prepare('DELETE FROM audio_tracks').run();
|
|---|
| 251 | fs.mkdirSync(path.join(MEDIA, 'audio'), { recursive: true });
|
|---|
| 252 | const WAV = Buffer.from('nep-audio-bytes');
|
|---|
| 253 | fs.writeFileSync(path.join(MEDIA, 'audio', 't1.mp3'), WAV);
|
|---|
| 254 | db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)')
|
|---|
| 255 | .run('m1', 's1', 't1.mp3', 'audio/mpeg', WAV.length, path.join(MEDIA, 'audio', 't1.mp3'));
|
|---|
| 256 | db.prepare(`INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, credit, license)
|
|---|
| 257 | VALUES ('tr1','s1','Kanonnen','Robin',212,'m1','opname 2026','CC BY')`).run();
|
|---|
| 258 | db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, status, published_at)
|
|---|
| 259 | VALUES ('audio','s1','met-audio','u1','Met audio','<p>luister</p>[[track:tr1]]','published','2026-08-03 10:00:00')`).run();
|
|---|
| 260 |
|
|---|
| 261 | const arch = AX.buildArchive('me', { exportedAt: 'X' });
|
|---|
| 262 | db.prepare('DELETE FROM posts').run();
|
|---|
| 263 | db.prepare('DELETE FROM audio_tracks').run();
|
|---|
| 264 | db.prepare('DELETE FROM media').run();
|
|---|
| 265 | fs.rmSync(path.join(MEDIA, 'audio'), { recursive: true, force: true });
|
|---|
| 266 |
|
|---|
| 267 | AI.importArchive(arch.files, { slug: 'me' });
|
|---|
| 268 | const t = db.prepare("SELECT * FROM audio_tracks WHERE id = 'tr1'").get();
|
|---|
| 269 | assert.ok(t, '[[track:tr1]] in de content valt anders op niets terug');
|
|---|
| 270 | assert.equal(t.title, 'Kanonnen');
|
|---|
| 271 | assert.equal(t.license, 'CC BY');
|
|---|
| 272 | const m = db.prepare('SELECT * FROM media WHERE id = ?').get(t.media_id);
|
|---|
| 273 | assert.ok(m && fs.existsSync(m.storage_path), 'en het bestand staat er weer');
|
|---|
| 274 | assert.deepEqual(fs.readFileSync(m.storage_path), WAV);
|
|---|
| 275 | });
|
|---|
| 276 |
|
|---|
| 277 | test('een vroegtijdig gesloten poll blijft gesloten', () => {
|
|---|
| 278 | // Gevonden bij de rondgang op echte beta-data: poll_json.closed viel weg, dus
|
|---|
| 279 | // een poll die je met een einddatum in de TOEKOMST had gesloten stond na een
|
|---|
| 280 | // herstel weer open. AS2 kent hiervoor `closed` op een Question.
|
|---|
| 281 | leeg();
|
|---|
| 282 | db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, status, published_at, poll_json)
|
|---|
| 283 | VALUES ('dicht','s1','dichte-poll','u1','Dicht','<p>x</p>','published','2026-08-04 10:00:00',
|
|---|
| 284 | '{"multiple":false,"options":[{"name":"a"},{"name":"b"}],"endTime":"2030-01-01T00:00:00Z","closed":true}')`).run();
|
|---|
| 285 | const arch = AX.buildArchive('me', { exportedAt: 'X' });
|
|---|
| 286 | assert.equal(JSON.parse(arch.files.get('posts/dicht.json').toString()).closed, true);
|
|---|
| 287 | db.prepare('DELETE FROM posts').run();
|
|---|
| 288 | AI.importArchive(arch.files, { slug: 'me' });
|
|---|
| 289 | const d = JSON.parse(db.prepare("SELECT poll_json FROM posts WHERE id = 'dicht'").get().poll_json);
|
|---|
| 290 | assert.equal(d.closed, true, 'anders staat een gesloten stemming na een herstel weer open');
|
|---|
| 291 | assert.equal(d.endTime, '2030-01-01T00:00:00Z');
|
|---|
| 292 | });
|
|---|
| 293 |
|
|---|
| 294 | test('zonder manifest is het geen archief', () => {
|
|---|
| 295 | const files = new Map(ARCHIEF.files);
|
|---|
| 296 | files.delete('manifest.json');
|
|---|
| 297 | assert.throws(() => AI.importArchive(files, { slug: 'me' }), /geen manifest/);
|
|---|
| 298 | });
|
|---|
| 299 |
|
|---|
| 300 | test.after(() => { try { fs.rmSync(MEDIA, { recursive: true, force: true }); } catch { /* niets */ } });
|
|---|