source: Klonkt/test/c2s-compose.test.js@ 97bcf7e

main
Last change on this file since 97bcf7e was 6089c53, checked in by Robin <roboburr@…>, 6 weeks ago

Een C2S-post draagt zijn media, en een foto mag het hele bericht zijn

De composer-uitbreiding van de apps (Robins opdracht, 30-7) liep meteen op een
servergat: c2sCreatePost las alleen de content, dus een top-level post met
attachments kwam naakt aan, terwijl dezelfde attachments op replies en DM's
gewoon werkten.

Nu: de media wordt gevalideerd zoals bij deliverReply (alleen eigen
/media-uploads, image/audio/video, max 4), in de post-HTML gevouwen zodat het
web hem toont en afspeelt, en opgeslagen in posts.c2s_attachments zodat
buildNote hem federeert met zijn ECHTE mediaType: de extensiemap kent geen
audio en noemde een m4a anders een Image. Beelden staan ook inline in de
content; de dedup op URL houdt ze enkel.

En een media-only note is voortaan een post in plaats van een
empty_note-fout: een foto kan het hele bericht zijn.

Changed files:
src/services/ActivityPubService.js

  • c2sCreatePost: attachments valideren, in de HTML vouwen, opslaan
  • buildNote: c2s_attachments mee-federeren, opgeslagen mediaType wint
  • de empty-note-poort laat media-only door

src/config/database.js

  • kolom posts.c2s_attachments

New file:
test/c2s-compose.test.js

  • media in de web-content en als AS2-attachments, met dedup en het juiste type; een vreemde URL komt er niet in; media-only mag

remarks: 336 tests groen. De app-kant (panel-composer met pickers) volgt in
de app-repos.

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

  • Property mode set to 100644
File size: 3.4 KB
Line 
1// The app's composer posts over C2S. A top-level Note with media used to lose
2// it silently: c2sCreatePost read only the content, so a photo post arrived
3// naked while the very same attachments worked fine on replies and DMs.
4import { test } from 'node:test';
5import assert from 'node:assert/strict';
6
7process.env.DATABASE_PATH = ':memory:';
8process.env.PUBLIC_BASE_URL = 'https://test.example';
9
10const dbMod = await import('../src/config/database.js');
11const db = dbMod.default;
12dbMod.initializeDatabase();
13const AP = (await import('../src/services/ActivityPubService.js')).default;
14
15db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
16 .run('u1', 'robin', 'u1@t', 'x', 'god');
17db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1');
18const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get('s1' ? 'kid' : 'kid');
19const user = db.prepare('SELECT * FROM users WHERE id = ?').get('u1');
20
21test('a C2S post carries its media: into the web content and out as AS2 attachments', async () => {
22 const r = await AP.ingestOutboxActivity(site, user, {
23 type: 'Create',
24 object: {
25 type: 'Note',
26 content: '<p>kijk dan</p>',
27 source: { content: 'kijk dan', mediaType: 'text/plain' },
28 to: ['https://test.example/ap/users/kid/followers'],
29 cc: ['https://www.w3.org/ns/activitystreams#Public'],
30 attachment: [
31 { type: 'Image', url: '/media/reply-media/foto.jpg', mediaType: 'image/jpeg', name: 'ons plein' },
32 { type: 'Audio', url: '/media/reply-media/opname.m4a', mediaType: 'audio/mp4' },
33 // Not ours: a remote URL must never be laundered into our media.
34 { type: 'Image', url: 'https://evil.test/x.jpg', mediaType: 'image/jpeg' },
35 ],
36 },
37 });
38 assert.equal(r.status, 201, 'the post is created');
39
40 const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(r.id);
41 assert.match(post.content, /<img src="\/media\/reply-media\/foto\.jpg" alt="ons plein">/, 'the web shows the photo');
42 assert.match(post.content, /<audio controls[^>]+src="\/media\/reply-media\/opname\.m4a">/, 'and plays the recording');
43 assert.ok(!post.content.includes('evil.test'), 'the stranger stays out');
44
45 const note = AP.buildNote('https://test.example', site, post);
46 const att = note.attachment || [];
47 const img = att.find((a) => a.url.endsWith('/media/reply-media/foto.jpg'));
48 const aud = att.find((a) => a.url.endsWith('/media/reply-media/opname.m4a'));
49 assert.ok(img && img.type === 'Image', 'the photo federates as an Image');
50 assert.equal(img.url, 'https://test.example/media/reply-media/foto.jpg', 'absolute, so any server can fetch it');
51 assert.equal(img.name, 'ons plein', 'alt text rides along');
52 assert.ok(aud, 'the recording federates too');
53 assert.equal(aud.type, 'Audio', 'as an Audio, not an Image: the stored mediaType wins over the extension map');
54 assert.equal(att.filter((a) => a.url.endsWith('foto.jpg')).length, 1, 'inline img + stored row dedupe to one');
55});
56
57test('a media-only post is a post, not an empty-note error', async () => {
58 const r = await AP.ingestOutboxActivity(site, user, {
59 type: 'Create',
60 object: {
61 type: 'Note', content: '',
62 to: ['https://test.example/ap/users/kid/followers'],
63 attachment: [{ type: 'Image', url: '/media/reply-media/alleen.jpg', mediaType: 'image/png' }],
64 },
65 });
66 assert.equal(r.status, 201, 'a picture can be the whole message');
67});
Note: See TracBrowser for help on using the repository browser.