| 1 | // Wie je volgt gaat mee bij een verhuizing.
|
|---|
| 2 | //
|
|---|
| 3 | // Dit ontbrak: de exporter dekte posts, tracks en antwoorden, maar niet je
|
|---|
| 4 | // relaties. De Move vertelt je VOLGERS waar je heen ging; niets vertelde JOU wie
|
|---|
| 5 | // jij volgde. Die lijst stond alleen in de database die je achterlaat.
|
|---|
| 6 | //
|
|---|
| 7 | // Kolomvorm van Mastodon, met een vijfde kolom van ons erachter, zodat de lijst
|
|---|
| 8 | // beide kanten op werkt. Een uitwisselformaat dat alleen met zichzelf praat is
|
|---|
| 9 | // geen uitwisselformaat.
|
|---|
| 10 | import { test, beforeEach } from 'node:test';
|
|---|
| 11 | import assert from 'node:assert/strict';
|
|---|
| 12 |
|
|---|
| 13 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 14 |
|
|---|
| 15 | const dbMod = await import('../src/config/database.js');
|
|---|
| 16 | const db = dbMod.default;
|
|---|
| 17 | {
|
|---|
| 18 | const stil = console.log;
|
|---|
| 19 | console.log = () => {};
|
|---|
| 20 | try { dbMod.initializeDatabase(); } finally { console.log = stil; }
|
|---|
| 21 | }
|
|---|
| 22 |
|
|---|
| 23 | const { followingCsv, parseFollowingCsv } = await import('../src/services/ArchiveExportService.js');
|
|---|
| 24 | const { importFollowing } = await import('../src/services/ArchiveImportService.js');
|
|---|
| 25 |
|
|---|
| 26 | const volg = db.prepare(`INSERT INTO ap_following (slug, actor_uri, handle, status, auto_boost)
|
|---|
| 27 | VALUES (?,?,?,?,?)`);
|
|---|
| 28 |
|
|---|
| 29 | beforeEach(() => {
|
|---|
| 30 | db.prepare('DELETE FROM ap_following').run();
|
|---|
| 31 | });
|
|---|
| 32 |
|
|---|
| 33 | test('de CSV heeft de Mastodon-koppen, met Featured erachter', () => {
|
|---|
| 34 | volg.run('me', 'https://a.example/ap/users/jason', '@jason@a.example', 'accepted', 1);
|
|---|
| 35 | const csv = followingCsv('me');
|
|---|
| 36 | const [kop, eerste] = csv.trim().split('\n');
|
|---|
| 37 | assert.equal(kop, 'Account address,Show boosts,Notify on new posts,Languages,Featured');
|
|---|
| 38 | // Zonder de leidende @, want zo schrijft Mastodon het adres.
|
|---|
| 39 | assert.equal(eerste, 'jason@a.example,,,,true');
|
|---|
| 40 | });
|
|---|
| 41 |
|
|---|
| 42 | test('een openstaand verzoek gaat NIET mee', () => {
|
|---|
| 43 | volg.run('me', 'https://a.example/ap/users/wacht', '@wacht@a.example', 'pending', 0);
|
|---|
| 44 | volg.run('me', 'https://a.example/ap/users/ja', '@ja@a.example', 'accepted', 0);
|
|---|
| 45 | const regels = followingCsv('me').trim().split('\n').slice(1);
|
|---|
| 46 | assert.equal(regels.length, 1, 'een verzoek dat iemand bewust liet liggen mag niet opnieuw verstuurd worden');
|
|---|
| 47 | assert.match(regels[0], /^ja@a\.example/);
|
|---|
| 48 | });
|
|---|
| 49 |
|
|---|
| 50 | test('zonder handle valt hij terug op de actor-URI', () => {
|
|---|
| 51 | volg.run('me', 'https://a.example/ap/users/naamloos', null, 'accepted', 0);
|
|---|
| 52 | assert.match(followingCsv('me'), /https:\/\/a\.example\/ap\/users\/naamloos/);
|
|---|
| 53 | });
|
|---|
| 54 |
|
|---|
| 55 | test('een lege lijst levert geen bestand op', () => {
|
|---|
| 56 | assert.equal(followingCsv('me'), null);
|
|---|
| 57 | });
|
|---|
| 58 |
|
|---|
| 59 | test('een export leest zichzelf terug', () => {
|
|---|
| 60 | volg.run('me', 'https://a.example/ap/users/a', '@a@a.example', 'accepted', 1);
|
|---|
| 61 | volg.run('me', 'https://b.example/ap/users/b', '@b@b.example', 'accepted', 0);
|
|---|
| 62 | const terug = parseFollowingCsv(followingCsv('me'));
|
|---|
| 63 | assert.deepEqual(terug, [
|
|---|
| 64 | { address: 'a@a.example', featured: true },
|
|---|
| 65 | { address: 'b@b.example', featured: false },
|
|---|
| 66 | ]);
|
|---|
| 67 | });
|
|---|
| 68 |
|
|---|
| 69 | test('een bestand uit Mastodon werkt, ook zonder onze vijfde kolom', () => {
|
|---|
| 70 | const uit = parseFollowingCsv(
|
|---|
| 71 | 'Account address,Show boosts,Notify on new posts,Languages\n'
|
|---|
| 72 | + 'iemand@mastodon.social,true,false,\n',
|
|---|
| 73 | );
|
|---|
| 74 | // Hun "Show boosts" staat op true, en dat mag hier NIET als uitgelicht landen.
|
|---|
| 75 | assert.deepEqual(uit, [{ address: 'iemand@mastodon.social', featured: false }]);
|
|---|
| 76 | });
|
|---|
| 77 |
|
|---|
| 78 | test('een kale lijst adressen werkt ook, zonder kopregel', () => {
|
|---|
| 79 | const uit = parseFollowingCsv('@een@a.example\ntwee@b.example\n');
|
|---|
| 80 | assert.deepEqual(uit.map((r) => r.address), ['een@a.example', 'twee@b.example']);
|
|---|
| 81 | });
|
|---|
| 82 |
|
|---|
| 83 | test('een geciteerd veld met een komma erin blijft heel', () => {
|
|---|
| 84 | const uit = parseFollowingCsv('Account address,Show boosts\n"raar,naam@a.example",true\n');
|
|---|
| 85 | assert.equal(uit[0].address, 'raar,naam@a.example');
|
|---|
| 86 | });
|
|---|
| 87 |
|
|---|
| 88 | test('importFollowing volgt elke regel, met de boost-stand mee', async () => {
|
|---|
| 89 | const gezien = [];
|
|---|
| 90 | const r = await importFollowing({ slug: 'me' },
|
|---|
| 91 | 'Account address,Show boosts,Notify on new posts,Languages,Featured\n'
|
|---|
| 92 | + 'a@a.example,,,,true\n'
|
|---|
| 93 | + 'b@b.example,,,,false\n',
|
|---|
| 94 | { followFn: async (site, adres, boost) => { gezien.push([adres, boost]); return true; } });
|
|---|
| 95 |
|
|---|
| 96 | assert.deepEqual(gezien, [['a@a.example', true], ['b@b.example', false]],
|
|---|
| 97 | 'de uitgelicht-stand moet als derde argument mee de Follow in');
|
|---|
| 98 | assert.equal(r.gevolgd, 2);
|
|---|
| 99 | assert.equal(r.mislukt.length, 0);
|
|---|
| 100 | });
|
|---|
| 101 |
|
|---|
| 102 | test('jezelf volgen wordt overgeslagen', async () => {
|
|---|
| 103 | const r = await importFollowing({ slug: 'me' },
|
|---|
| 104 | 'Account address\nme@eigen.example\nander@a.example\n',
|
|---|
| 105 | { followFn: async () => true });
|
|---|
| 106 | assert.equal(r.overgeslagen, 1);
|
|---|
| 107 | assert.equal(r.gevolgd, 1);
|
|---|
| 108 | });
|
|---|
| 109 |
|
|---|
| 110 | test('een mislukte follow stopt de rest niet, en wordt gemeld', async () => {
|
|---|
| 111 | const r = await importFollowing({ slug: 'me' },
|
|---|
| 112 | 'Account address\neen@a.example\nkapot@b.example\ndrie@c.example\n',
|
|---|
| 113 | { followFn: async (s, adres) => { if (adres.startsWith('kapot')) throw new Error('onbereikbaar'); return true; } });
|
|---|
| 114 |
|
|---|
| 115 | assert.equal(r.gevolgd, 2, 'de derde moet nog geprobeerd zijn na de tweede');
|
|---|
| 116 | assert.deepEqual(r.mislukt, [{ adres: 'kapot@b.example', reden: 'onbereikbaar' }]);
|
|---|
| 117 | });
|
|---|
| 118 |
|
|---|
| 119 | test('zonder followFn gebeurt er niets, en dat zegt hij ook', async () => {
|
|---|
| 120 | const r = await importFollowing({ slug: 'me' }, 'Account address\na@a.example\n', {});
|
|---|
| 121 | assert.equal(r.error, 'no_follow_fn');
|
|---|
| 122 | assert.equal(r.gevolgd, 0);
|
|---|
| 123 | });
|
|---|
| 124 |
|
|---|
| 125 | // De koppeling zelf. De losse functies hierboven kunnen prima werken terwijl het
|
|---|
| 126 | // archief de CSV nooit ziet, en dat is precies het soort stil gat waar een
|
|---|
| 127 | // verhuizing op strandt: alles groen, en je volglijst blijft achter.
|
|---|
| 128 | test('buildArchive stopt following.csv er echt in, met hash in het manifest', async () => {
|
|---|
| 129 | db.prepare('INSERT OR IGNORE INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 130 | .run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 131 | db.prepare('INSERT OR IGNORE INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
|
|---|
| 132 | .run('s1', 'me', 'Mijn site', 'u1');
|
|---|
| 133 | volg.run('me', 'https://a.example/ap/users/jason', '@jason@a.example', 'accepted', 1);
|
|---|
| 134 |
|
|---|
| 135 | const { buildArchive } = await import('../src/services/ArchiveExportService.js');
|
|---|
| 136 | const r = buildArchive('me', { origin: 'https://oud.example' });
|
|---|
| 137 |
|
|---|
| 138 | assert.ok(r.files.has('following.csv'), 'het archief moet de volglijst dragen');
|
|---|
| 139 | assert.equal(r.counts.following, 1);
|
|---|
| 140 | assert.ok(r.manifest.files['following.csv'], 'en hij hoort in het manifest, anders telt hij niet mee bij de controle');
|
|---|
| 141 | assert.match(String(r.files.get('following.csv')), /jason@a\.example,,,,true/);
|
|---|
| 142 | });
|
|---|
| 143 |
|
|---|
| 144 | // De uploadweg. De parser kan prima werken terwijl het formulier niets doorgeeft,
|
|---|
| 145 | // en dat merk je pas als je met een echt bestand voor de knop staat.
|
|---|
| 146 | test('een geupload bestand wordt gelezen, ook met de BOM die Excel ervoor zet', async () => {
|
|---|
| 147 | const multer = (await import('multer')).default;
|
|---|
| 148 | const express = (await import('express')).default;
|
|---|
| 149 |
|
|---|
| 150 | const up = multer({ storage: multer.memoryStorage(), limits: { fileSize: 512 * 1024, files: 1 } }).single('csvfile');
|
|---|
| 151 | const app = express();
|
|---|
| 152 | app.post('/t', up, (req, res) => {
|
|---|
| 153 | const csv = (req.file && req.file.buffer)
|
|---|
| 154 | ? req.file.buffer.toString('utf8').replace(/^/, '')
|
|---|
| 155 | : ((req.body && req.body.csv) || '');
|
|---|
| 156 | res.json({ bron: req.file ? 'bestand' : 'plakveld', rijen: parseFollowingCsv(csv) });
|
|---|
| 157 | });
|
|---|
| 158 |
|
|---|
| 159 | const srv = app.listen(0);
|
|---|
| 160 | await new Promise((r) => srv.once('listening', r));
|
|---|
| 161 | const url = `http://127.0.0.1:${srv.address().port}/t`;
|
|---|
| 162 | const csv = 'Account address,Show boosts,Notify on new posts,Languages,Featured\n'
|
|---|
| 163 | + 'jason@a.example,,,,true\n';
|
|---|
| 164 | try {
|
|---|
| 165 | const fd = new FormData();
|
|---|
| 166 | fd.append('csvfile', new Blob([`${csv}`], { type: 'text/csv' }), 'following.csv');
|
|---|
| 167 | const a = await (await fetch(url, { method: 'POST', body: fd })).json();
|
|---|
| 168 | assert.equal(a.bron, 'bestand');
|
|---|
| 169 | assert.deepEqual(a.rijen, [{ address: 'jason@a.example', featured: true }],
|
|---|
| 170 | 'de BOM mag niet in het eerste adres blijven plakken');
|
|---|
| 171 |
|
|---|
| 172 | // Het plakveld moet blijven werken naast de upload.
|
|---|
| 173 | const fd2 = new FormData();
|
|---|
| 174 | fd2.append('csv', csv);
|
|---|
| 175 | const b = await (await fetch(url, { method: 'POST', body: fd2 })).json();
|
|---|
| 176 | assert.equal(b.bron, 'plakveld');
|
|---|
| 177 | assert.deepEqual(b.rijen, [{ address: 'jason@a.example', featured: true }]);
|
|---|
| 178 | } finally {
|
|---|
| 179 | srv.close();
|
|---|
| 180 | }
|
|---|
| 181 | });
|
|---|