| [02fb7ce] | 1 | // KARAKTERISERING van "heb ik hierop gereageerd", vóór de opschoning van
|
|---|
| 2 | // shaer-9e9. Deze tests beschrijven wat de code NU doet, niet wat ze zou moeten
|
|---|
| 3 | // doen. Ze staan hier om de aanstaande refactor hoorbaar te maken.
|
|---|
| 4 | //
|
|---|
| 5 | // Waarom dat nodig is: dit gebied faalt stil. Elke mark*-aanroep zit in een
|
|---|
| 6 | // try/catch die niets doet, en een UPDATE die geen rij raakt is geen fout. Zo
|
|---|
| 7 | // kon de shaer:liked-bug (Klonkt 04aca12) maanden bestaan. Een refactor kan hier
|
|---|
| 8 | // dus slagen, groen testen en tóch state kwijtraken -- tenzij het huidige gedrag
|
|---|
| 9 | // eerst is vastgeschreven.
|
|---|
| 10 | //
|
|---|
| 11 | // LET OP bij het opruimen: de tests onder "de twee bronnen lopen uiteen" horen
|
|---|
| 12 | // te GAAN FALEN zodra fase 1 (één setReaction) klaar is. Dat is geen regressie
|
|---|
| 13 | // maar het doel; werk ze dan bewust bij in plaats van ze te laten verdwijnen.
|
|---|
| 14 | //
|
|---|
| 15 | // Wat hier NIET in kan: de routes zelf (die vragen HTTP + sessie) en het
|
|---|
| 16 | // C2S-pad voor Like/Announce (dat doet netwerk; zie de kop van
|
|---|
| 17 | // c2s-outbox.test.js). Dit dekt de servicelaag waar alle drie de ingangen op
|
|---|
| 18 | // uitkomen.
|
|---|
| 19 | //
|
|---|
| 20 | // Run: npm test
|
|---|
| 21 |
|
|---|
| 22 | import { test } from 'node:test';
|
|---|
| 23 | import assert from 'node:assert/strict';
|
|---|
| 24 |
|
|---|
| 25 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 26 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 27 |
|
|---|
| 28 | const dbMod = await import('../src/config/database.js');
|
|---|
| 29 | const db = dbMod.default;
|
|---|
| 30 | dbMod.initializeDatabase();
|
|---|
| 31 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 32 |
|
|---|
| 33 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 34 | .run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 35 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
|
|---|
| 36 |
|
|---|
| 37 | const uri = (n) => `https://r.test/ap/notes/${n}`;
|
|---|
| 38 |
|
|---|
| 39 | /** Een post van iemand anders in JOUW tijdlijn-cache. */
|
|---|
| 40 | function seedTimeline(id, author = 'https://r.test/users/anna') {
|
|---|
| 41 | db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, created_at)
|
|---|
| 42 | VALUES (?,?,?,?,?,?)`).run(id, 'me', author, 'Anna', '<p>x</p>', '2026-08-06 09:00:00');
|
|---|
| 43 | }
|
|---|
| 44 |
|
|---|
| 45 | // ── Ingang 1: de tijdlijn-route (posts.js ~1279-1304) ────────────────────
|
|---|
| 46 | // Schrijft de KOLOM en leest de kolom. De tussentabel blijft ongemoeid.
|
|---|
| 47 |
|
|---|
| 48 | test('tijdlijn-route: markLiked zet de kolom en getTimelineReaction leest hem', () => {
|
|---|
| 49 | const u = uri('t1'); seedTimeline(u);
|
|---|
| 50 | assert.deepEqual(AP.getTimelineReaction('me', u), { liked: false, boosted: false });
|
|---|
| 51 | AP.markLiked('me', u);
|
|---|
| 52 | assert.deepEqual(AP.getTimelineReaction('me', u), { liked: true, boosted: false });
|
|---|
| 53 | AP.unmarkLiked('me', u);
|
|---|
| 54 | assert.equal(AP.getTimelineReaction('me', u).liked, false);
|
|---|
| 55 | });
|
|---|
| 56 |
|
|---|
| 57 | test('tijdlijn-route: markBoosted zet de kolom en de post verschijnt in de Cirkel', () => {
|
|---|
| 58 | const u = uri('t2'); seedTimeline(u);
|
|---|
| 59 | assert.equal(AP.boostedCount('me'), 0);
|
|---|
| 60 | AP.markBoosted('me', u);
|
|---|
| 61 | assert.equal(AP.getTimelineReaction('me', u).boosted, true);
|
|---|
| 62 | assert.equal(AP.boostedCount('me'), 1);
|
|---|
| 63 | // De Cirkel is wat een gebruiker ziet: geboost = zichtbaar daar.
|
|---|
| 64 | assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'geboost hoort in de Cirkel te staan');
|
|---|
| 65 | AP.unmarkBoosted('me', u);
|
|---|
| 66 | assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false);
|
|---|
| 67 | });
|
|---|
| 68 |
|
|---|
| 69 | test('tijdlijn-route: een vlag op een post die NIET in je tijdlijn staat gaat stil verloren', () => {
|
|---|
| 70 | // Dit is het stille falen waar de refactor voor moet oppassen: geen fout,
|
|---|
| 71 | // geen rij, geen spoor. Een UPDATE die niets raakt is voor SQLite in orde.
|
|---|
| 72 | const u = uri('bestaat-niet');
|
|---|
| 73 | AP.markLiked('me', u);
|
|---|
| 74 | assert.deepEqual(AP.getTimelineReaction('me', u), { liked: false, boosted: false });
|
|---|
| 75 | });
|
|---|
| 76 |
|
|---|
| 77 | // ── Ingang 2: de interact-route (posts.js ~841-869) ──────────────────────
|
|---|
| 78 | // Schrijft de TUSSENTABEL en leest de tussentabel. De kolom blijft ongemoeid.
|
|---|
| 79 |
|
|---|
| 80 | test('interact-route: setMyReaction schrijft de tussentabel, ook zonder tijdlijnrij', () => {
|
|---|
| 81 | const u = uri('i1'); // bewust NIET in ap_timeline
|
|---|
| 82 | assert.deepEqual(AP.getMyReactions('me', u), { liked: false, boosted: false });
|
|---|
| 83 | AP.setMyReaction('me', u, 'like', true);
|
|---|
| 84 | assert.deepEqual(AP.getMyReactions('me', u), { liked: true, boosted: false });
|
|---|
| 85 | AP.setMyReaction('me', u, 'boost', true);
|
|---|
| 86 | assert.deepEqual(AP.getMyReactions('me', u), { liked: true, boosted: true });
|
|---|
| 87 | AP.setMyReaction('me', u, 'like', false);
|
|---|
| 88 | assert.deepEqual(AP.getMyReactions('me', u), { liked: false, boosted: true });
|
|---|
| 89 | });
|
|---|
| 90 |
|
|---|
| 91 | test('interact-route: tweemaal dezelfde reactie levert geen dubbele rij op', () => {
|
|---|
| 92 | const u = uri('i2');
|
|---|
| 93 | AP.setMyReaction('me', u, 'like', true);
|
|---|
| 94 | AP.setMyReaction('me', u, 'like', true);
|
|---|
| 95 | const n = db.prepare('SELECT COUNT(*) AS n FROM ap_my_reactions WHERE site_slug=? AND target_uri=?').get('me', u).n;
|
|---|
| 96 | assert.equal(n, 1, 'de UNIQUE hoort het dubbel opslaan te voorkomen');
|
|---|
| 97 | });
|
|---|
| 98 |
|
|---|
| 99 | // ── De valkuil met naam: upsertBoostedNote is een INSERT ─────────────────
|
|---|
| 100 |
|
|---|
| 101 | test('upsertBoostedNote trekt een post die je NIET volgt je tijdlijn in', () => {
|
|---|
| 102 | // Dit is geen vlag zetten. Zonder deze insert bestaat de rij niet, kan de
|
|---|
| 103 | // vlag nergens landen, en verschijnt de boost nergens. Wie dit in de refactor
|
|---|
| 104 | // vervangt door "gewoon markBoosted" boost iets wat daarna onvindbaar is.
|
|---|
| 105 | const u = uri('b1');
|
|---|
| 106 | assert.equal(db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE id=?').get(u).n, 0);
|
|---|
| 107 | AP.upsertBoostedNote('me', {
|
|---|
| 108 | object_uri: u, actor_uri: 'https://r.test/users/bo', actor_name: 'Bo',
|
|---|
| 109 | content: '<p>geboost</p>', media: '[]',
|
|---|
| 110 | });
|
|---|
| 111 | assert.equal(db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE id=?').get(u).n, 1, 'de rij hoort aangemaakt te worden');
|
|---|
| 112 | assert.equal(AP.getTimelineReaction('me', u).boosted, true, 'en meteen als geboost gemarkeerd');
|
|---|
| 113 | assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'en dus zichtbaar in de Cirkel');
|
|---|
| 114 | });
|
|---|
| 115 |
|
|---|
| 116 | // ── De twee bronnen lopen uiteen ─────────────────────────────────────────
|
|---|
| 117 | // DEZE TESTS HOREN TE FALEN NA FASE 1. Ze leggen de huidige splitsing vast,
|
|---|
| 118 | // zodat het samenvoegen een zichtbare gebeurtenis is en geen stille.
|
|---|
| 119 |
|
|---|
| 120 | test('HUIDIG GEDRAG: de tijdlijn-route vult de tussentabel niet', () => {
|
|---|
| 121 | const u = uri('d1'); seedTimeline(u);
|
|---|
| 122 | AP.markLiked('me', u);
|
|---|
| 123 | assert.equal(AP.getTimelineReaction('me', u).liked, true, 'de kolom staat aan');
|
|---|
| 124 | assert.equal(AP.getMyReactions('me', u).liked, false,
|
|---|
| 125 | 'NA FASE 1 hoort dit true te zijn: pas deze test dan bewust aan');
|
|---|
| 126 | });
|
|---|
| 127 |
|
|---|
| 128 | test('HUIDIG GEDRAG: de interact-route vult de kolom niet', () => {
|
|---|
| 129 | const u = uri('d2'); seedTimeline(u);
|
|---|
| 130 | AP.setMyReaction('me', u, 'like', true);
|
|---|
| 131 | assert.equal(AP.getMyReactions('me', u).liked, true, 'de tussentabel staat aan');
|
|---|
| 132 | assert.equal(AP.getTimelineReaction('me', u).liked, false,
|
|---|
| 133 | 'NA FASE 1 hoort dit true te zijn: pas deze test dan bewust aan');
|
|---|
| 134 | });
|
|---|
| 135 |
|
|---|
| 136 | test('HUIDIG GEDRAG: alleen het C2S-pad schrijft allebei', () => {
|
|---|
| 137 | // Wat ingestOutboxActivity sinds Klonkt 04aca12 doet, hier nagebootst zonder
|
|---|
| 138 | // het netwerk: setMyReaction + markLiked. Dit is het enige pad dat de twee
|
|---|
| 139 | // bronnen gelijk houdt, en precies daarom werkt un-liken vanuit een app wel.
|
|---|
| 140 | const u = uri('d3'); seedTimeline(u);
|
|---|
| 141 | AP.setMyReaction('me', u, 'like', true);
|
|---|
| 142 | AP.markLiked('me', u);
|
|---|
| 143 | assert.equal(AP.getMyReactions('me', u).liked, true);
|
|---|
| 144 | assert.equal(AP.getTimelineReaction('me', u).liked, true);
|
|---|
| 145 | });
|
|---|
| 146 |
|
|---|
| 147 | // ── Wat de invariant-test van fase 1 straks moet bewaken ─────────────────
|
|---|
| 148 |
|
|---|
| 149 | test('meetbaar: hoeveel rijen hebben een vlag zonder tegenhanger', () => {
|
|---|
| 150 | // Dezelfde query als fase 0 in shaer-9e9, hier als test zodat de refactor
|
|---|
| 151 | // hem kan gebruiken: na fase 1 hoort dit getal 0 te zijn en te blijven.
|
|---|
| 152 | const scheef = db.prepare(`
|
|---|
| 153 | SELECT COUNT(*) AS n FROM ap_timeline t
|
|---|
| 154 | WHERE (t.liked = 1 OR t.boosted = 1)
|
|---|
| 155 | AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r
|
|---|
| 156 | WHERE r.site_slug = t.slug AND r.target_uri = t.id)`).get().n;
|
|---|
| 157 | assert.ok(scheef > 0, 'vandaag lopen ze uiteen; na fase 1 hoort deze assert omgedraaid te worden naar === 0');
|
|---|
| 158 | });
|
|---|