source: Klonkt/test/following-csv.test.js

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

De volglijst als bestand uploaden, niet alleen plakken

Robins vraag bij de echte verhuizing: hij downloadt een CSV en moet hem dan met de
hand openen en de inhoud overplakken. Dat is een omweg die niemand hoort te lopen.

Nu een bestandskiezer naast het plakveld. Multer met memoryStorage, want dit is een
lijstje adressen van een paar kilobyte dat na het lezen niets te zoeken heeft op de
server; op schijf schrijven zou alleen rommel achterlaten. Een gekozen bestand wint
van het plakveld: wie een bestand aanwijst bedoelt dat.

De BOM gaat eraf. Excel zet die voor een CSV, en zonder dat eraf te halen plakt hij
aan het eerste adres vast en is de eerste regel stil onbruikbaar.

En twee redirects in deze route wezen nog naar /following. Dat is de oude pagina
waar niets meer naartoe linkt (zie 822d16d), dus je kwam na een import op een
scherm waar je zelf niet meer weg kon. Beide naar /connect.

Changed files:
src/routes/posts.js

  • multer memoryStorage, single('csvfile'), 512kb, 1 bestand
  • bestand wint van plakveld, BOM eraf
  • drie redirects van /following naar /connect

src/views/pages/connect.ejs

  • bestandskiezer met accept=".csv,text/csv,text/plain", formulier op multipart

src/services/i18n.js

  • tl.move_import_file in nl, en en de; het plakveld-label ingekort tot "Of plak de lijst hier"

test/following-csv.test.js

  • test 14: een echt multipart-verzoek met een BOM ervoor, plus de controle dat het plakveld ernaast blijft werken

remarks: 927 groen in UTC en Europe/Amsterdam. De uploadweg is met een echte
multipart-POST tegen een draaiende express+multer getest, niet alleen beredeneerd.
De pagina apart gerenderd: bestandskiezer en multipart-enctype aanwezig, geen
onvertaalde sleutels.

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

  • Property mode set to 100644
File size: 8.1 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)
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);
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
42test('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
50test('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
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);
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
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 // 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
78test('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
83test('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
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\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
102test('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
110test('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
119test('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.
128test('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.
146test('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});
Note: See TracBrowser for help on using the repository browser.