source: Klonkt/test/music-uitgave.test.js@ ea3e722

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

Een hashtag hoort te staan zoals hij geschreven is (Robins punt, 9-8)

Stond een tag zowel in het tagveld als in het lijf van de post, dan won de vorm
uit het veld. Die gaat door tagParts, want een hashtag mag geen spaties
bevatten en "Doen we Niet" wordt daar #DoenWeNiet van. Nodig voor een tag die
alleen in het veld staat -- maar als iemand in zijn tekst #DoenweNiet heeft
getypt is dat geen benadering meer maar de tag zelf, en die verdween als
duplicaat van de genormaliseerde vorm.

Nu gaat het lijf voor: de geschreven vorm wint, en het tagveld vult aan wat er
niet staat. De slug in de href blijft kleingeschreven -- dat is een adres, geen
naam.

En de uitgave-collectie gebruikt nu diezelfde lijst in plaats van een eigen: ze
miste de tags uit de tekst helemaal, en spelde de rest anders dan dezelfde post
elders deed. buildHashtagList en hashtagTags zijn daarvoor van
ActivityPubService naar ap-core verhuisd, want music/ mag daar niet uit
importeren.

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

  • Property mode set to 100644
File size: 10.1 KB
Line 
1// De post als uitgave (shaer-38y).
2//
3// Drie punten uit de bead: de volgorde van de post behouden, de lijst als
4// geheel overnemen, en de metadata van de post lenen. Het derde is waar het om
5// draait -- een collectie met alleen een naam is wat een Funkwhale-achtige
6// lezer te mager vindt, en de post heeft wel een titel, tekst, hoes en tags.
7import { test } from 'node:test';
8import assert from 'node:assert/strict';
9
10process.env.DATABASE_PATH = ':memory:';
11process.env.PUBLIC_BASE_URL = 'https://test.example';
12
13const dbMod = await import('../src/config/database.js');
14dbMod.initializeDatabase();
15const db = dbMod.default;
16const M = await import('../src/services/music/index.js');
17
18const BASE = 'https://test.example';
19const SITE = 'site-1';
20const site = { id: SITE, slug: 'muziek' };
21
22db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
23 .run('u1', 'robin', 'r@test', 'x', 'god');
24db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
25 .run(SITE, 'muziek', 'Muziek', 'u1');
26
27let n = 0;
28function maakTrack(id, titel) {
29 const mediaId = 'm-' + id;
30 db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,?)')
31 .run(mediaId, SITE, id + '.mp3', '/x/' + id + '.mp3', 'audio/mpeg', 1000 + (n++));
32 db.prepare('INSERT INTO audio_tracks (id, site_id, media_id, title, duration, fedi_open, position) VALUES (?,?,?,?,?,1,?)')
33 .run(id, SITE, mediaId, titel, 100, 0);
34}
35
36function maakPost({ id, slug, titel, content, excerpt = '', cover = '', tags = '' }) {
37 db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, excerpt, content, cover_image_url, tags, status, type, created_at, updated_at, published_at)
38 VALUES (?,?,?,'u1',?,?,?,?,?,'published','playlist',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`)
39 .run(id, SITE, slug, titel, excerpt, content, cover, tags);
40 return db.prepare('SELECT id, slug, title, excerpt, content, cover_image_url, tags FROM posts WHERE id = ?').get(id);
41}
42
43// Drie tracks, bewust in een andere vololgorde ingevoerd dan ze in de post staan.
44maakTrack('t-een', 'Een');
45maakTrack('t-twee', 'Twee');
46maakTrack('t-drie', 'Drie');
47
48const losse = maakPost({
49 id: 'p-los', slug: 'drie-nieuwe', titel: 'Drie nieuwe nummers',
50 content: 'Hier: [[track:t-drie]] dan [[track:t-een]] en tot slot [[track:t-twee]]',
51 excerpt: 'Opgenomen op zolder, in een week.',
52 cover: '/media/hoes.jpg',
53 tags: '["lofi","zolder tapes"]',
54});
55
56test('losse tracks in een post worden EEN collectie', () => {
57 const col = M.buildPostTrackCollection(BASE, site, losse);
58 assert.ok(col, 'er is een uitgave');
59 assert.equal(col.type, 'OrderedCollection');
60 assert.equal(col.id, `${BASE}/ap/users/muziek/posts/p-los/tracks`);
61 assert.equal(col.totalItems, 3);
62});
63
64test('in de volgorde van de POST, niet van de tabel', () => {
65 const col = M.buildPostTrackCollection(BASE, site, losse);
66 assert.deepEqual(col.orderedItems.map((a) => a.name), ['Drie', 'Een', 'Twee'],
67 'zoals iemand ze heeft neergezet is de volgorde waarin ze bedoeld zijn');
68});
69
70test('de post leent zijn titel, tekst, hoes en tags uit', () => {
71 const col = M.buildPostTrackCollection(BASE, site, losse);
72 assert.equal(col.name, 'Drie nieuwe nummers');
73 assert.equal(col.content, 'Opgenomen op zolder, in een week.');
74 assert.equal(col.image.type, 'Image');
75 assert.equal(col.image.url, `${BASE}/media/hoes.jpg`);
76 // Het tagveld is JSON in de database. Een tag van twee woorden wordt
77 // CamelCase, want een hashtag mag geen spaties bevatten.
78 assert.deepEqual(col.tag.map((t) => t.name), ['#lofi', '#ZolderTapes']);
79 assert.equal(col.tag[1].href, `${BASE}/tag/zoldertapes`);
80 assert.equal(col.url, `${BASE}/drie-nieuwe`);
81 assert.equal(col.context, `${BASE}/ap/notes/p-los`);
82});
83
84test('en de tracks erin wijzen terug naar diezelfde post', () => {
85 const col = M.buildPostTrackCollection(BASE, site, losse);
86 for (const a of col.orderedItems) {
87 assert.equal(a.context, `${BASE}/ap/notes/p-los`);
88 assert.ok(a.url.some((u) => u.mediaType === 'text/html' && u.href === `${BASE}/drie-nieuwe`));
89 }
90});
91
92test('de post wijst met een Link-tag naar zijn eigen uitgave', () => {
93 const tags = M.playlistLinkTags(BASE, site, losse.content, losse);
94 const link = tags.find((t) => t.href === `${BASE}/ap/users/muziek/posts/p-los/tracks`);
95 assert.ok(link, 'zonder deze link bestaat de collectie wel maar vindt niemand hem');
96 assert.equal(link.mediaType, 'application/activity+json');
97});
98
99test('een post zonder losse tracks heeft geen eigen uitgave', () => {
100 const geen = maakPost({ id: 'p-leeg', slug: 'niets', titel: 'Niets', content: '<p>gewoon tekst</p>' });
101 assert.equal(M.buildPostTrackCollection(BASE, site, geen), null);
102});
103
104test('een gesloten track telt niet mee, ook niet voor het bestaan', () => {
105 maakTrack('t-dicht', 'Dicht');
106 db.prepare('UPDATE audio_tracks SET fedi_open = 0 WHERE id = ?').run('t-dicht');
107 const p = maakPost({ id: 'p-dicht', slug: 'dicht', titel: 'Dicht', content: '[[track:t-dicht]]' });
108 assert.equal(M.buildPostTrackCollection(BASE, site, p), null,
109 'geen lege collectie maar een collectie die niet bestaat');
110});
111
112// ── De playlist-collectie leent van de post die haar uitbrengt ────────────
113
114db.prepare('INSERT INTO playlists (id, site_id, title, artist, year, kind) VALUES (?,?,?,?,?,?)')
115 .run('nachtlicht', SITE, 'Nachtlicht', 'Robin', 2026, 'album');
116db.prepare('INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES (?,?,?)')
117 .run('nachtlicht', 't-een', 0);
118
119maakPost({
120 id: 'p-plaat', slug: 'de-plaat-is-er', titel: 'De plaat is er',
121 content: 'Eindelijk: [[playlist:nachtlicht]]',
122 excerpt: 'Anderhalf jaar aan gewerkt.',
123 cover: '/media/plaat.jpg', tags: 'album',
124});
125
126test('een playlist-collectie leent van de post die haar uitbrengt', () => {
127 const pl = db.prepare('SELECT id, title, artist, year, cover_url, kind FROM playlists WHERE id = ?').get('nachtlicht');
128 const col = M.buildPlaylistCollection(BASE, site, pl, M.playlistOpenTracks('nachtlicht'));
129 assert.equal(col.name, 'De plaat is er', 'de posttitel wordt de titel');
130 assert.equal(col.alsoKnownAs, 'Nachtlicht', 'de eigen naam gaat niet verloren');
131 assert.equal(col.content, 'Anderhalf jaar aan gewerkt.');
132 assert.equal(col.image.url, `${BASE}/media/plaat.jpg`);
133 assert.equal(col.summary, 'Robin · 2026', 'artiest en jaar blijven de samenvatting');
134 assert.equal(col.context, `${BASE}/ap/notes/p-plaat`);
135});
136
137test('een playlist zonder post leent niets', () => {
138 db.prepare('INSERT INTO playlists (id, site_id, title, kind) VALUES (?,?,?,?)')
139 .run('los', SITE, 'Los', 'playlist');
140 const pl = db.prepare('SELECT id, title, artist, year, cover_url, kind FROM playlists WHERE id = ?').get('los');
141 const col = M.buildPlaylistCollection(BASE, site, pl, []);
142 assert.equal(col.name, 'Los');
143 assert.equal(col.content, undefined);
144 assert.equal(M.uitgavePost(SITE, 'los'), null);
145});
146
147test('twee collecties in een post: dan leent de playlist niets', () => {
148 db.prepare('INSERT INTO playlists (id, site_id, title, kind) VALUES (?,?,?,?)')
149 .run('tweede', SITE, 'Tweede', 'playlist');
150 maakPost({
151 id: 'p-twee', slug: 'twee-lijsten', titel: 'Twee lijsten',
152 content: '[[playlist:tweede]] en [[playlist:nachtlicht]]',
153 });
154 // De post is dan geen drager van EEN identiteit meer -- dezelfde regel als in
155 // de afleiding, hier alleen toegepast op de lening.
156 assert.equal(M.uitgavePost(SITE, 'tweede'), null);
157});
158
159test('zonder excerpt leent hij de tekst uit het lijf van de post', () => {
160 // Live bleek de excerpt vaak leeg; dan is `content` weglaten slechter dan de
161 // tekst zelf pakken. Shortcodes gaan eruit -- die zijn de muziek, niet het
162 // verhaal erover.
163 const p = maakPost({
164 id: 'p-tekst', slug: 'zonder-excerpt', titel: 'Zonder excerpt',
165 content: '<p>Drie schetsen van vorige week.</p>[[track:t-een]]<div>&nbsp;</div>',
166 });
167 const col = M.buildPostTrackCollection(BASE, site, p);
168 assert.equal(col.content, 'Drie schetsen van vorige week.');
169});
170
171test('en een post die alleen uit shortcodes bestaat leent geen lege tekst', () => {
172 const p = maakPost({
173 id: 'p-kaal', slug: 'alleen-muziek', titel: 'Alleen muziek',
174 content: '[[track:t-twee]]<div> </div>',
175 });
176 const col = M.buildPostTrackCollection(BASE, site, p);
177 assert.equal(col.content, undefined, 'een leeg veld is slechter dan geen veld');
178});
179
180test('losse hashtags horen niet in de omschrijving -- die staan al in tag', () => {
181 // Live leverde dit "#DoenweNiet #DoenWeNiet #devs" op als omschrijving van
182 // een post die verder geen tekst heeft. Een description die de tagwolk
183 // herhaalt is ruis.
184 const p = maakPost({
185 id: 'p-tags', slug: 'alleen-tags', titel: 'Alleen tags',
186 content: '[[track:t-een]]<div>#lofi #zolder</div>',
187 });
188 assert.equal(M.buildPostTrackCollection(BASE, site, p).content, undefined);
189});
190
191test('maar tekst MET een hashtag erin blijft gewoon staan', () => {
192 const p = maakPost({
193 id: 'p-mix', slug: 'tekst-en-tags', titel: 'Tekst en tags',
194 content: '<p>Opgenomen in de schuur.</p>[[track:t-twee]]<div>#lofi</div>',
195 });
196 assert.equal(M.buildPostTrackCollection(BASE, site, p).content, 'Opgenomen in de schuur.');
197});
198
199test('een hashtag uit het LIJF staat zoals hij geschreven is', () => {
200 // Robins punt (9-8): #DoenweNiet in de tekst hoort #DoenweNiet te blijven.
201 // Eerder won de vorm uit het tagveld -- tagParts maakt daar #DoenWeNiet van --
202 // en verdween de geschreven vorm als duplicaat.
203 const p = maakPost({
204 id: 'p-schrijf', slug: 'zoals-geschreven', titel: 'Zoals geschreven',
205 content: '<p>Zie <a class="hashtag" href="/tag/doenweniet">#DoenweNiet</a></p>[[track:t-een]]',
206 tags: '["Doen we Niet","lofi"]',
207 });
208 const col = M.buildPostTrackCollection(BASE, site, p);
209 const namen = col.tag.map((t) => t.name);
210 assert.ok(namen.includes('#DoenweNiet'), 'de geschreven vorm wint');
211 assert.ok(!namen.includes('#DoenWeNiet'), 'en de genormaliseerde staat er niet naast');
212 assert.ok(namen.includes('#lofi'), 'tags die alleen in het veld staan blijven');
213 assert.equal(col.tag[0].href, `${BASE}/tag/doenweniet`, 'de slug blijft wel klein: dat is een adres');
214});
Note: See TracBrowser for help on using the repository browser.