| 1 | // Zachte groepen in Berichten (shaer-mbji): een gesprek is de SET deelnemers,
|
|---|
| 2 | // niet één tegenpartij.
|
|---|
| 3 | import { test } from 'node:test';
|
|---|
| 4 | import assert from 'node:assert/strict';
|
|---|
| 5 |
|
|---|
| 6 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 7 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 8 |
|
|---|
| 9 | const dbMod = await import('../src/config/database.js');
|
|---|
| 10 | const db = dbMod.default;
|
|---|
| 11 | dbMod.initializeDatabase();
|
|---|
| 12 | const K = await import('../src/services/conversation-key.js');
|
|---|
| 13 | const T = await import('../src/services/ap-timeline.js');
|
|---|
| 14 |
|
|---|
| 15 | const ME = 'https://test.example/ap/users/me';
|
|---|
| 16 | const ANNA = 'https://a.test/users/anna';
|
|---|
| 17 | const BEN = 'https://b.test/users/ben';
|
|---|
| 18 | const CARLA = 'https://c.test/users/carla';
|
|---|
| 19 | const PUB = 'https://www.w3.org/ns/activitystreams#Public';
|
|---|
| 20 |
|
|---|
| 21 | // ── De sleutel ────────────────────────────────────────────────────
|
|---|
| 22 |
|
|---|
| 23 | test('één ander is zijn kale URI: bestaande gesprekken veranderen niet', () => {
|
|---|
| 24 | assert.equal(K.conversationKey([ANNA]), ANNA);
|
|---|
| 25 | });
|
|---|
| 26 |
|
|---|
| 27 | test('een groep is gesorteerd, dus {Anna, Ben} en {Ben, Anna} zijn één gesprek', () => {
|
|---|
| 28 | assert.equal(K.conversationKey([BEN, ANNA]), K.conversationKey([ANNA, BEN]));
|
|---|
| 29 | assert.equal(K.conversationKey([ANNA, BEN]), `${ANNA} ${BEN}`);
|
|---|
| 30 | assert.equal(K.conversationKey([ANNA, ANNA, BEN]), `${ANNA} ${BEN}`, 'dubbelen tellen een keer');
|
|---|
| 31 | });
|
|---|
| 32 |
|
|---|
| 33 | test('een publiek of een volgerslijst is geen deelnemer', () => {
|
|---|
| 34 | assert.equal(K.conversationKey([ANNA, PUB, `${ANNA}/followers`]), ANNA);
|
|---|
| 35 | });
|
|---|
| 36 |
|
|---|
| 37 | test('de sleutel valt terug in zijn deelnemers', () => {
|
|---|
| 38 | assert.deepEqual(K.participantsOfKey(`${ANNA} ${BEN}`), [ANNA, BEN]);
|
|---|
| 39 | assert.deepEqual(K.participantsOfKey(ANNA), [ANNA]);
|
|---|
| 40 | assert.equal(K.isGroupKey(`${ANNA} ${BEN}`), true);
|
|---|
| 41 | assert.equal(K.isGroupKey(ANNA), false);
|
|---|
| 42 | });
|
|---|
| 43 |
|
|---|
| 44 | test('de deelnemers van een binnenkomer: afzender plus geadresseerden, zonder mij', () => {
|
|---|
| 45 | const note = { to: [ME, BEN], cc: [PUB] };
|
|---|
| 46 | assert.deepEqual(K.participantsOf(note, ANNA, ME), [ANNA, BEN].sort());
|
|---|
| 47 | // Een gewoon één-op-één-bericht: alleen de afzender blijft over.
|
|---|
| 48 | assert.deepEqual(K.participantsOf({ to: [ME] }, ANNA, ME), [ANNA]);
|
|---|
| 49 | // AS2 staat ook objecten toe in to/cc.
|
|---|
| 50 | assert.deepEqual(K.participantsOf({ to: [{ id: ME }, { id: BEN }] }, ANNA, ME), [ANNA, BEN].sort());
|
|---|
| 51 | });
|
|---|
| 52 |
|
|---|
| 53 | test('context (FEP-7888) is een string of een object met een id', () => {
|
|---|
| 54 | assert.equal(K.contextOf({ context: 'https://a.test/ctx/1' }), 'https://a.test/ctx/1');
|
|---|
| 55 | assert.equal(K.contextOf({ context: { id: 'https://a.test/ctx/2' } }), 'https://a.test/ctx/2');
|
|---|
| 56 | assert.equal(K.contextOf({ context: 'javascript:alert(1)' }), null, 'alleen http(s)');
|
|---|
| 57 | assert.equal(K.contextOf({}), null);
|
|---|
| 58 | });
|
|---|
| 59 |
|
|---|
| 60 | // ── De lezing ─────────────────────────────────────────────────────
|
|---|
| 61 |
|
|---|
| 62 | let n = 0;
|
|---|
| 63 | function binnen(van, conv, stamp) {
|
|---|
| 64 | n += 1;
|
|---|
| 65 | db.prepare(`INSERT INTO ap_mentions (slug, object_uri, actor_uri, content, published, conv, participants)
|
|---|
| 66 | VALUES ('me', ?, ?, '<p>x</p>', ?, ?, ?)`)
|
|---|
| 67 | .run(`${van}/notes/${n}`, van, stamp, conv, conv ? JSON.stringify(K.participantsOfKey(conv)) : null);
|
|---|
| 68 | return `${van}/notes/${n}`;
|
|---|
| 69 | }
|
|---|
| 70 | function uit(ontvangers, { conv = K.conversationKey(ontvangers), stamp }) {
|
|---|
| 71 | n += 1;
|
|---|
| 72 | const id = `out-${n}`;
|
|---|
| 73 | db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, content, visibility, to_actor, to_actors, conv, created_at)
|
|---|
| 74 | VALUES (?, 'me', '', '<p>x</p>', 'direct', ?, ?, ?, ?)`)
|
|---|
| 75 | .run(id, ontvangers[0], JSON.stringify(ontvangers), conv, stamp);
|
|---|
| 76 | return id;
|
|---|
| 77 | }
|
|---|
| 78 |
|
|---|
| 79 | test('een bericht aan Anna en Ben is één gesprek, niet twee', () => {
|
|---|
| 80 | uit([ANNA, BEN], { stamp: '2026-10-01 10:00:00' });
|
|---|
| 81 | binnen(ANNA, K.conversationKey([ANNA, BEN]), '2026-10-01T10:05:00Z');
|
|---|
| 82 | binnen(ANNA, ANNA, '2026-10-01T09:00:00Z'); // het één-op-één-gesprek bestaat ernaast
|
|---|
| 83 |
|
|---|
| 84 | const koppen = T.conversationHeads('me').map((h) => h.other);
|
|---|
| 85 | const groep = K.conversationKey([ANNA, BEN]);
|
|---|
| 86 | assert.ok(koppen.includes(groep), 'de groep heeft een kop');
|
|---|
| 87 | assert.ok(koppen.includes(ANNA), 'het gesprek met alleen Anna blijft bestaan');
|
|---|
| 88 | assert.ok(!koppen.includes(BEN), 'Ben heeft geen los gesprek gekregen van een groepsbericht');
|
|---|
| 89 |
|
|---|
| 90 | const draad = T.conversationHistory('me', groep).rows;
|
|---|
| 91 | assert.equal(draad.length, 2, 'mijn bericht en Anna\'s antwoord staan in dezelfde draad');
|
|---|
| 92 | assert.deepEqual(new Set(draad.map((r) => r.direction)), new Set(['in', 'out']));
|
|---|
| 93 | });
|
|---|
| 94 |
|
|---|
| 95 | test('oude uitgaande rijen zonder sleutel lezen zoals ze altijd lazen', () => {
|
|---|
| 96 | // Geen conv: per ontvanger, zoals voor deze wijziging. Geen migratie die de
|
|---|
| 97 | // geschiedenis herschikt.
|
|---|
| 98 | uit([CARLA, BEN], { conv: null, stamp: '2026-09-01 10:00:00' });
|
|---|
| 99 | const koppen = T.conversationHeads('me').map((h) => h.other);
|
|---|
| 100 | assert.ok(koppen.includes(CARLA));
|
|---|
| 101 | assert.ok(koppen.includes(BEN));
|
|---|
| 102 | assert.ok(!koppen.includes(K.conversationKey([BEN, CARLA])), 'een oude rij wordt niet alsnog een groep');
|
|---|
| 103 | });
|
|---|
| 104 |
|
|---|
| 105 | test('ongelezen en gelezen gaan per gesprek, ook voor een groep', () => {
|
|---|
| 106 | const groep = K.conversationKey([ANNA, CARLA]);
|
|---|
| 107 | const eerste = binnen(ANNA, groep, '2026-10-02T10:00:00Z');
|
|---|
| 108 | binnen(CARLA, groep, '2026-10-02T10:01:00Z');
|
|---|
| 109 |
|
|---|
| 110 | assert.equal(T.unreadPerConversation('me').get(groep).n, 2);
|
|---|
| 111 | T.markRead('me', eerste);
|
|---|
| 112 | assert.equal(T.unreadPerConversation('me').get(groep).n, 1, 'gelezen tot en met het eerste');
|
|---|
| 113 | // Het één-op-één-gesprek met Anna merkt daar niets van.
|
|---|
| 114 | const losAnna = T.unreadPerConversation('me').get(ANNA);
|
|---|
| 115 | assert.ok(!losAnna || losAnna.n >= 1);
|
|---|
| 116 | });
|
|---|