source: Klonkt/test/following-csv.test.js@ 096f099

main
Last change on this file since 096f099 was 096f099, checked in by Robin <roboburr@…>, 4 weeks ago

Wie je volgt gaat mee bij een verhuizing, als CSV

De exporter dekte posts, tracks en antwoorden, maar niet je relaties. Daarmee was
een verhuizing halfslachtig: de Move vertelt je VOLGERS waar je heen ging, maar
niets vertelde JOU wie jij volgde. Die lijst stond alleen in de database die je
achterlaat, en die negen mensen moest je uit je hoofd opnieuw opzoeken.

Kolomvorm van Mastodon, zodat de lijst beide kanten op werkt: hiervandaan naar een
Mastodon, en die van daar naar hier. Een uitwisselformaat dat alleen met zichzelf
praat is er geen.

Account address,Show boosts,Notify on new posts,Languages,Featured

De vijfde kolom is van ons; Mastodon leest de eerste vier en negeert de rest, dus
het blijft daar importeerbaar. Andersom werkt een bestand uit Mastodon hier ook,
en een kale lijst adressen zonder kopregel eveneens.

Over de naam: featured betekent in Klonkt al de collectie VASTGEZETTE POSTS
(toot:featured), en het commentaar bij auto_boost noemt dat al "feature a
followed account" (hun posts in jouw Cirkel). Drie dingen die featured heten is er
twee te veel, dus de kolom heet in de database highlighted: wie je op je eigen
profiel uitlicht. In de CSV blijft de kop Featured, want dat is het woord dat
mensen en Mastodon kennen. FEP-d471 (Endorsements) modelleert dit als een eigen
object voor webs of trust; dat is een zwaarder ding dan hier bedoeld, dus dit
blijft een lokale vlag.

importFollowing staat BEWUST buiten importArchive. Die draait in een transactie en
raakt alleen de database; opnieuw volgen stuurt activiteiten de deur uit en wacht
op het netwerk. Een trage peer zou de transactie openhouden en een rollback neemt
verzonden Follows niet terug. Het is ook een aparte stap omdat een archief inlezen
stil is en negen mensen aanschrijven niet; dat mag geen bijwerking zijn.

Changed files:
src/config/database.js

  • kolom ap_following.highlighted, met de drie betekenissen van "featured" uit elkaar gehouden

src/services/ArchiveExportService.js

  • followingCsv() en parseFollowingCsv(), met een CSV-lezer die geciteerde velden aankan
  • buildArchive schrijft following.csv en telt hem mee

src/services/ArchiveImportService.js

  • importFollowing(), met injecteerbare followFn zodat de test geen netwerk raakt

New file:
test/following-csv.test.js

  • 13 tests: de kopvorm, openstaande verzoeken die NIET meegaan, terugval op de actor-URI, de ronde export-naar-import, een Mastodon-bestand zonder onze kolom, een kale lijst, een geciteerd veld met een komma, jezelf overslaan, en dat een mislukte follow de rest niet stopt
  • plus de koppeling: dat buildArchive de CSV er echt in stopt en in het manifest zet. Die ontbrak eerst, en dat is precies het gat waar dit op zou stranden: losse functies groen, archief zonder volglijst

remarks: gebouwd op branch following-csv, afgetakt van github/main (0824351). Stond
eerst per ongeluk op ward-pwa; die branch is dev-only en mag niet naar main, en is
onaangeroerd gebleven. Suite 921 groen in UTC en Europe/Amsterdam. Nog te doen: een
UI om highlighted te zetten, en de knop die importFollowing aanroept met
followActor als followFn.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 6.3 KB
Line 
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.
10import { test, beforeEach } from 'node:test';
11import assert from 'node:assert/strict';
12
13process.env.DATABASE_PATH = ':memory:';
14
15const dbMod = await import('../src/config/database.js');
16const db = dbMod.default;
17{
18 const stil = console.log;
19 console.log = () => {};
20 try { dbMod.initializeDatabase(); } finally { console.log = stil; }
21}
22
23const { followingCsv, parseFollowingCsv } = await import('../src/services/ArchiveExportService.js');
24const { importFollowing } = await import('../src/services/ArchiveImportService.js');
25
26const volg = db.prepare(`INSERT INTO ap_following (slug, actor_uri, handle, status, auto_boost, highlighted)
27 VALUES (?,?,?,?,?,?)`);
28
29beforeEach(() => {
30 db.prepare('DELETE FROM ap_following').run();
31});
32
33test('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
42test('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
50test('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
55test('een lege lijst levert geen bestand op', () => {
56 assert.equal(followingCsv('me'), null);
57});
58
59test('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
69test('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
77test('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
82test('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
88test('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
101test('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
109test('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
118test('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.
127test('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});
Note: See TracBrowser for help on using the repository browser.