// The app's composer posts over C2S. A top-level Note with media used to lose // it silently: c2sCreatePost read only the content, so a photo post arrived // naked while the very same attachments worked fine on replies and DMs. import { test } from 'node:test'; import assert from 'node:assert/strict'; process.env.DATABASE_PATH = ':memory:'; process.env.PUBLIC_BASE_URL = 'https://test.example'; const dbMod = await import('../src/config/database.js'); const db = dbMod.default; dbMod.initializeDatabase(); const AP = (await import('../src/services/ActivityPubService.js')).default; db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)') .run('u1', 'robin', 'u1@t', 'x', 'god'); db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1'); const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get('s1' ? 'kid' : 'kid'); const user = db.prepare('SELECT * FROM users WHERE id = ?').get('u1'); test('a C2S post carries its media: into the web content and out as AS2 attachments', async () => { const r = await AP.ingestOutboxActivity(site, user, { type: 'Create', object: { type: 'Note', content: '

kijk dan

', source: { content: 'kijk dan', mediaType: 'text/plain' }, to: ['https://test.example/ap/users/kid/followers'], cc: ['https://www.w3.org/ns/activitystreams#Public'], attachment: [ { type: 'Image', url: '/media/reply-media/foto.jpg', mediaType: 'image/jpeg', name: 'ons plein' }, { type: 'Audio', url: '/media/reply-media/opname.m4a', mediaType: 'audio/mp4' }, // Not ours: a remote URL must never be laundered into our media. { type: 'Image', url: 'https://evil.test/x.jpg', mediaType: 'image/jpeg' }, ], }, }); assert.equal(r.status, 201, 'the post is created'); const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(r.id); assert.match(post.content, /ons plein/, 'the web shows the photo'); assert.match(post.content, /