| 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.
|
|---|
| 7 | import { test } from 'node:test';
|
|---|
| 8 | import assert from 'node:assert/strict';
|
|---|
| 9 |
|
|---|
| 10 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 11 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 12 |
|
|---|
| 13 | const dbMod = await import('../src/config/database.js');
|
|---|
| 14 | dbMod.initializeDatabase();
|
|---|
| 15 | const db = dbMod.default;
|
|---|
| 16 | const M = await import('../src/services/music/index.js');
|
|---|
| 17 |
|
|---|
| 18 | const BASE = 'https://test.example';
|
|---|
| 19 | const SITE = 'site-1';
|
|---|
| 20 | const site = { id: SITE, slug: 'muziek' };
|
|---|
| 21 |
|
|---|
| 22 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 23 | .run('u1', 'robin', 'r@test', 'x', 'god');
|
|---|
| 24 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
|
|---|
| 25 | .run(SITE, 'muziek', 'Muziek', 'u1');
|
|---|
| 26 |
|
|---|
| 27 | let n = 0;
|
|---|
| 28 | function 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 |
|
|---|
| 36 | function 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.
|
|---|
| 44 | maakTrack('t-een', 'Een');
|
|---|
| 45 | maakTrack('t-twee', 'Twee');
|
|---|
| 46 | maakTrack('t-drie', 'Drie');
|
|---|
| 47 |
|
|---|
| 48 | const 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 |
|
|---|
| 56 | test('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 |
|
|---|
| 64 | test('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 |
|
|---|
| 70 | test('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, en een tag van twee woorden wordt
|
|---|
| 77 | // CamelCase -- zelfde regel als elders, want het is nu dezelfde parser.
|
|---|
| 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 |
|
|---|
| 84 | test('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 |
|
|---|
| 92 | test('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 |
|
|---|
| 99 | test('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 |
|
|---|
| 104 | test('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 |
|
|---|
| 114 | db.prepare('INSERT INTO playlists (id, site_id, title, artist, year, kind) VALUES (?,?,?,?,?,?)')
|
|---|
| 115 | .run('nachtlicht', SITE, 'Nachtlicht', 'Robin', 2026, 'album');
|
|---|
| 116 | db.prepare('INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES (?,?,?)')
|
|---|
| 117 | .run('nachtlicht', 't-een', 0);
|
|---|
| 118 |
|
|---|
| 119 | maakPost({
|
|---|
| 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 |
|
|---|
| 126 | test('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 |
|
|---|
| 137 | test('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 |
|
|---|
| 147 | test('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 |
|
|---|
| 159 | test('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> </div>',
|
|---|
| 166 | });
|
|---|
| 167 | const col = M.buildPostTrackCollection(BASE, site, p);
|
|---|
| 168 | assert.equal(col.content, 'Drie schetsen van vorige week.');
|
|---|
| 169 | });
|
|---|
| 170 |
|
|---|
| 171 | test('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 |
|
|---|
| 180 | test('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 |
|
|---|
| 191 | test('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 | });
|
|---|