| 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, highlighted)
|
|---|
| 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, 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,false,,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, 0);
|
|---|
| 44 | volg.run('me', 'https://a.example/ap/users/ja', '@ja@a.example', 'accepted', 0, 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, 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, 0);
|
|---|
| 61 | volg.run('me', 'https://b.example/ap/users/b', '@b@b.example', 'accepted', 0, 1);
|
|---|
| 62 | const terug = parseFollowingCsv(followingCsv('me'));
|
|---|
| 63 | assert.deepEqual(terug, [
|
|---|
| 64 | { address: 'a@a.example', autoBoost: true, highlighted: false },
|
|---|
| 65 | { address: 'b@b.example', autoBoost: false, highlighted: true },
|
|---|
| 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 | assert.deepEqual(uit, [{ address: 'iemand@mastodon.social', autoBoost: true, highlighted: false }]);
|
|---|
| 75 | });
|
|---|
| 76 |
|
|---|
| 77 | test('een kale lijst adressen werkt ook, zonder kopregel', () => {
|
|---|
| 78 | const uit = parseFollowingCsv('@een@a.example\ntwee@b.example\n');
|
|---|
| 79 | assert.deepEqual(uit.map((r) => r.address), ['een@a.example', 'twee@b.example']);
|
|---|
| 80 | });
|
|---|
| 81 |
|
|---|
| 82 | test('een geciteerd veld met een komma erin blijft heel', () => {
|
|---|
| 83 | const uit = parseFollowingCsv('Account address,Show boosts\n"raar,naam@a.example",true\n');
|
|---|
| 84 | assert.equal(uit[0].address, 'raar,naam@a.example');
|
|---|
| 85 | assert.equal(uit[0].autoBoost, true);
|
|---|
| 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,false,,false\n'
|
|---|
| 93 | + 'b@b.example,false,false,,true\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 | assert.equal(r.gevolgd, 2);
|
|---|
| 98 | assert.equal(r.mislukt.length, 0);
|
|---|
| 99 | });
|
|---|
| 100 |
|
|---|
| 101 | test('jezelf volgen wordt overgeslagen', async () => {
|
|---|
| 102 | const r = await importFollowing({ slug: 'me' },
|
|---|
| 103 | 'Account address\nme@eigen.example\nander@a.example\n',
|
|---|
| 104 | { followFn: async () => true });
|
|---|
| 105 | assert.equal(r.overgeslagen, 1);
|
|---|
| 106 | assert.equal(r.gevolgd, 1);
|
|---|
| 107 | });
|
|---|
| 108 |
|
|---|
| 109 | test('een mislukte follow stopt de rest niet, en wordt gemeld', async () => {
|
|---|
| 110 | const r = await importFollowing({ slug: 'me' },
|
|---|
| 111 | 'Account address\neen@a.example\nkapot@b.example\ndrie@c.example\n',
|
|---|
| 112 | { followFn: async (s, adres) => { if (adres.startsWith('kapot')) throw new Error('onbereikbaar'); return true; } });
|
|---|
| 113 |
|
|---|
| 114 | assert.equal(r.gevolgd, 2, 'de derde moet nog geprobeerd zijn na de tweede');
|
|---|
| 115 | assert.deepEqual(r.mislukt, [{ adres: 'kapot@b.example', reden: 'onbereikbaar' }]);
|
|---|
| 116 | });
|
|---|
| 117 |
|
|---|
| 118 | test('zonder followFn gebeurt er niets, en dat zegt hij ook', async () => {
|
|---|
| 119 | const r = await importFollowing({ slug: 'me' }, 'Account address\na@a.example\n', {});
|
|---|
| 120 | assert.equal(r.error, 'no_follow_fn');
|
|---|
| 121 | assert.equal(r.gevolgd, 0);
|
|---|
| 122 | });
|
|---|
| 123 |
|
|---|
| 124 | // De koppeling zelf. De losse functies hierboven kunnen prima werken terwijl het
|
|---|
| 125 | // archief de CSV nooit ziet, en dat is precies het soort stil gat waar een
|
|---|
| 126 | // verhuizing op strandt: alles groen, en je volglijst blijft achter.
|
|---|
| 127 | test('buildArchive stopt following.csv er echt in, met hash in het manifest', async () => {
|
|---|
| 128 | db.prepare('INSERT OR IGNORE INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 129 | .run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 130 | db.prepare('INSERT OR IGNORE INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
|
|---|
| 131 | .run('s1', 'me', 'Mijn site', 'u1');
|
|---|
| 132 | volg.run('me', 'https://a.example/ap/users/jason', '@jason@a.example', 'accepted', 1, 1);
|
|---|
| 133 |
|
|---|
| 134 | const { buildArchive } = await import('../src/services/ArchiveExportService.js');
|
|---|
| 135 | const r = buildArchive('me', { origin: 'https://oud.example' });
|
|---|
| 136 |
|
|---|
| 137 | assert.ok(r.files.has('following.csv'), 'het archief moet de volglijst dragen');
|
|---|
| 138 | assert.equal(r.counts.following, 1);
|
|---|
| 139 | assert.ok(r.manifest.files['following.csv'], 'en hij hoort in het manifest, anders telt hij niet mee bij de controle');
|
|---|
| 140 | assert.match(String(r.files.get('following.csv')), /jason@a\.example,true,false,,true/);
|
|---|
| 141 | });
|
|---|