| 1 | // De MusicBrainz-koppeling, stap 1 (shaer-mbz).
|
|---|
| 2 | //
|
|---|
| 3 | // Zonder netwerk: globalThis.fetch wordt vervangen en musicbrainz.org staat in
|
|---|
| 4 | // AP_ALLOW_HOSTS, zodat safeFetch geen DNS doet. Anders test dit of de
|
|---|
| 5 | // testmachine internet heeft en of MusicBrainz toevallig up is.
|
|---|
| 6 | //
|
|---|
| 7 | // Wat hier WEL getest wordt is wat wij beloven: hun twee harde regels. Die
|
|---|
| 8 | // leiden bij overtreding tot een BLOKKADE en niet tot een foutmelding, dus
|
|---|
| 9 | // "het werkte toen ik het probeerde" is er geen bewijs voor.
|
|---|
| 10 | import { test } from 'node:test';
|
|---|
| 11 | import assert from 'node:assert/strict';
|
|---|
| 12 |
|
|---|
| 13 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 14 | process.env.PUBLIC_BASE_URL = 'https://ons.test';
|
|---|
| 15 | process.env.MUSICBRAINZ_CONTACT = 'robin@ons.test';
|
|---|
| 16 | process.env.AP_ALLOW_HOSTS = 'musicbrainz.org';
|
|---|
| 17 |
|
|---|
| 18 | const dbMod = await import('../src/config/database.js');
|
|---|
| 19 | dbMod.initializeDatabase();
|
|---|
| 20 | const MB = await import('../src/services/MusicBrainzService.js');
|
|---|
| 21 |
|
|---|
| 22 | const verzoeken = [];
|
|---|
| 23 | const echt = globalThis.fetch;
|
|---|
| 24 | globalThis.fetch = async (url, opts) => {
|
|---|
| 25 | verzoeken.push({ url: String(url), ua: opts?.headers?.['User-Agent'], op: Date.now() });
|
|---|
| 26 | return {
|
|---|
| 27 | ok: true,
|
|---|
| 28 | status: 200,
|
|---|
| 29 | headers: new Map(),
|
|---|
| 30 | json: async () => ({
|
|---|
| 31 | artists: [
|
|---|
| 32 | { id: '8be31978-1884-4773-beae-f73df35b92aa', name: 'Nirvana', score: 100,
|
|---|
| 33 | disambiguation: 'Seattle grunge band', type: 'Group', country: 'US',
|
|---|
| 34 | 'life-span': { begin: '1987', ended: true, end: '1994' } },
|
|---|
| 35 | { id: 'aaaaaaaa-1111-2222-3333-444444444444', name: 'Nirvana', score: 72,
|
|---|
| 36 | disambiguation: '60s UK band', type: 'Group', country: 'GB' },
|
|---|
| 37 | { id: 'bad', name: '' }, // onbruikbaar: moet eruit vallen
|
|---|
| 38 | ],
|
|---|
| 39 | }),
|
|---|
| 40 | };
|
|---|
| 41 | };
|
|---|
| 42 |
|
|---|
| 43 | test('een MBID is een UUID en niets anders', () => {
|
|---|
| 44 | assert.equal(MB.isMbid('8be31978-1884-4773-beae-f73df35b92aa'), true);
|
|---|
| 45 | assert.equal(MB.isMbid('the_ceeesg'), false, 'een handle is geen MBID');
|
|---|
| 46 | assert.equal(MB.isMbid('https://musicbrainz.org/artist/8be31978-1884-4773-beae-f73df35b92aa'), false,
|
|---|
| 47 | 'een URL ook niet -- anders sluipt er een hele link de kolom in');
|
|---|
| 48 | assert.equal(MB.isMbid(''), false);
|
|---|
| 49 | assert.equal(MB.isMbid(null), false);
|
|---|
| 50 | });
|
|---|
| 51 |
|
|---|
| 52 | test('de artiest-URL wordt alleen uit een echte MBID gebouwd', () => {
|
|---|
| 53 | assert.equal(MB.artiestUrl('8BE31978-1884-4773-BEAE-F73DF35B92AA'),
|
|---|
| 54 | 'https://musicbrainz.org/artist/8be31978-1884-4773-beae-f73df35b92aa',
|
|---|
| 55 | 'kleingeschreven, want een MBID is er een en niet twee');
|
|---|
| 56 | assert.equal(MB.artiestUrl('rommel'), null, 'geen URL verzinnen om iets onbekends heen');
|
|---|
| 57 | });
|
|---|
| 58 |
|
|---|
| 59 | test('een lege zoekopdracht raakt het net niet eens', async () => {
|
|---|
| 60 | verzoeken.length = 0;
|
|---|
| 61 | assert.deepEqual(await MB.zoekArtiesten(' '), []);
|
|---|
| 62 | assert.equal(verzoeken.length, 0, 'hun tempo is te kostbaar om aan niets te besteden');
|
|---|
| 63 | });
|
|---|
| 64 |
|
|---|
| 65 | test('de User-Agent noemt Klonkt EN een contact -- hun eis', async () => {
|
|---|
| 66 | // Een generieke of lege User-Agent is precies waarop MusicBrainz blokkeert.
|
|---|
| 67 | verzoeken.length = 0;
|
|---|
| 68 | await MB.zoekArtiesten('iemand');
|
|---|
| 69 | const ua = verzoeken.at(-1)?.ua;
|
|---|
| 70 | assert.ok(ua, 'er is er een gezet');
|
|---|
| 71 | assert.match(ua, /^Klonkt\//, 'met onze naam voorop');
|
|---|
| 72 | assert.match(ua, /robin@ons\.test/, 'en een manier om contact op te nemen');
|
|---|
| 73 | });
|
|---|
| 74 |
|
|---|
| 75 | test('de kandidaten dragen wat een mens nodig heeft om te KIEZEN', async () => {
|
|---|
| 76 | const uit = await MB.zoekArtiesten('Nirvana');
|
|---|
| 77 | assert.equal(uit.length, 2, 'een naamloze treffer is geen kandidaat');
|
|---|
| 78 | assert.equal(uit[0].mbid, '8be31978-1884-4773-beae-f73df35b92aa');
|
|---|
| 79 | // De naam alleen is niet genoeg: er zijn drie bands die Nirvana heten.
|
|---|
| 80 | assert.equal(uit[0].toelichting, 'Seattle grunge band');
|
|---|
| 81 | assert.equal(uit[1].toelichting, '60s UK band');
|
|---|
| 82 | assert.equal(uit[0].jaren, '1987 – 1994');
|
|---|
| 83 | assert.equal(uit[1].jaren, '', 'een band die nog bestaat krijgt geen eindjaar aangepraat');
|
|---|
| 84 | assert.equal(uit[0].url, 'https://musicbrainz.org/artist/8be31978-1884-4773-beae-f73df35b92aa');
|
|---|
| 85 | });
|
|---|
| 86 |
|
|---|
| 87 | test('twee zoekopdrachten liggen minstens een seconde uit elkaar', async () => {
|
|---|
| 88 | // Hun harde regel, en de reden dat dit server-side draait: een verzoek per
|
|---|
| 89 | // seconde geldt per APPLICATIE, niet per bezoeker. Twee tabbladen tegelijk
|
|---|
| 90 | // zouden hem anders samen overtreden.
|
|---|
| 91 | verzoeken.length = 0;
|
|---|
| 92 | await Promise.all([MB.zoekArtiesten('een'), MB.zoekArtiesten('twee')]);
|
|---|
| 93 | assert.equal(verzoeken.length, 2);
|
|---|
| 94 | const gat = verzoeken[1].op - verzoeken[0].op;
|
|---|
| 95 | assert.ok(gat >= 990, `verwacht >= 1000ms tussen twee verzoeken, was ${gat}ms`);
|
|---|
| 96 | });
|
|---|
| 97 |
|
|---|
| 98 | test.after(() => { globalThis.fetch = echt; });
|
|---|
| 99 |
|
|---|
| 100 | // ── Stap 2: de keuze vastleggen ───────────────────────────────────────────
|
|---|
| 101 |
|
|---|
| 102 | test('alleen een echte MBID komt de kolom in', async () => {
|
|---|
| 103 | // De route weigert alles wat geen UUID is. Zonder die zeef sluipt er een
|
|---|
| 104 | // hele URL of een handle in het veld dat straks naar buiten gaat.
|
|---|
| 105 | const db = dbMod.default;
|
|---|
| 106 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 107 | .run('u1', 'u1', 'u1@t', 'x', 'god');
|
|---|
| 108 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'band', 'Band', 'u1');
|
|---|
| 109 | for (const rommel of ['https://musicbrainz.org/artist/8be31978-1884-4773-beae-f73df35b92aa', 'nirvana', '']) {
|
|---|
| 110 | assert.equal(MB.isMbid(rommel), false, `${rommel} hoort geweigerd te worden`);
|
|---|
| 111 | }
|
|---|
| 112 | db.prepare('UPDATE sites SET mb_artist_id = ?, mb_artist_name = ? WHERE id = ?')
|
|---|
| 113 | .run('8be31978-1884-4773-beae-f73df35b92aa', 'Nirvana', 's1');
|
|---|
| 114 | const s = db.prepare("SELECT mb_artist_id, mb_artist_name FROM sites WHERE id = 's1'").get();
|
|---|
| 115 | assert.equal(s.mb_artist_id, '8be31978-1884-4773-beae-f73df35b92aa');
|
|---|
| 116 | assert.equal(s.mb_artist_name, 'Nirvana', 'de naam ernaast, zodat het scherm kan tonen WAT er hangt');
|
|---|
| 117 | });
|
|---|
| 118 |
|
|---|
| 119 | test('ontkoppelen maakt beide velden leeg', () => {
|
|---|
| 120 | const db = dbMod.default;
|
|---|
| 121 | db.prepare('UPDATE sites SET mb_artist_id = NULL, mb_artist_name = NULL WHERE id = ?').run('s1');
|
|---|
| 122 | const s = db.prepare("SELECT mb_artist_id, mb_artist_name FROM sites WHERE id = 's1'").get();
|
|---|
| 123 | assert.equal(s.mb_artist_id, null);
|
|---|
| 124 | assert.equal(s.mb_artist_name, null, 'anders blijft er een naam staan zonder koppeling');
|
|---|
| 125 | });
|
|---|
| 126 |
|
|---|
| 127 | // ── Stap 3: op de draad ───────────────────────────────────────────────────
|
|---|
| 128 |
|
|---|
| 129 | test('de actor draagt sameAs zodra er gekoppeld is -- en anders niets', async () => {
|
|---|
| 130 | const db = dbMod.default;
|
|---|
| 131 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 132 | const BASE = 'https://ons.test';
|
|---|
| 133 | const kaal = db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
|
|---|
| 134 | assert.equal(AP.buildActor(BASE, kaal).sameAs, undefined,
|
|---|
| 135 | 'zonder koppeling staat er niets -- een lege verwijzing is erger dan geen');
|
|---|
| 136 |
|
|---|
| 137 | db.prepare('UPDATE sites SET mb_artist_id = ? WHERE id = ?')
|
|---|
| 138 | .run('8be31978-1884-4773-beae-f73df35b92aa', 's1');
|
|---|
| 139 | const gekoppeld = db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
|
|---|
| 140 | const actor = AP.buildActor(BASE, gekoppeld);
|
|---|
| 141 | assert.equal(actor.sameAs, 'https://musicbrainz.org/artist/8be31978-1884-4773-beae-f73df35b92aa');
|
|---|
| 142 | });
|
|---|
| 143 |
|
|---|
| 144 | test('rommel in de kolom komt NIET op de draad', () => {
|
|---|
| 145 | // Het scherm zeeft al, maar de actor is de laatste deur. Wat hier langskomt
|
|---|
| 146 | // gaat naar iedereen, en een half adres is erger dan geen.
|
|---|
| 147 | const db = dbMod.default;
|
|---|
| 148 | db.prepare('UPDATE sites SET mb_artist_id = ? WHERE id = ?').run('nirvana', 's1');
|
|---|
| 149 | const s = db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
|
|---|
| 150 | assert.equal(MB.artiestUrl(s.mb_artist_id), null);
|
|---|
| 151 | });
|
|---|
| 152 |
|
|---|
| 153 | test('sameAs is een eigen term en NIET alsoKnownAs', async () => {
|
|---|
| 154 | // alsoKnownAs is in AS2 voor vroegere IDENTITEITEN van dezelfde actor, en
|
|---|
| 155 | // FEP-7628 leunt erop bij een verhuizing: het oude adres controleert of het
|
|---|
| 156 | // nieuwe hem daar noemt. Een MBID daar neerzetten zou een verhuizing kunnen
|
|---|
| 157 | // laten mislukken.
|
|---|
| 158 | const core = await import('../src/services/ap-core.js');
|
|---|
| 159 | const term = core.AP_CONTEXT.find((x) => x && typeof x === 'object' && x.sameAs);
|
|---|
| 160 | assert.ok(term, 'de term is gedeclareerd, anders laat een strikte lezer hem vallen');
|
|---|
| 161 | assert.equal(term.sameAs['@id'], 'schema:sameAs');
|
|---|
| 162 | assert.equal(term.sameAs['@type'], '@id', 'het is een URI en geen tekst');
|
|---|
| 163 | });
|
|---|