| 1 | // ActivityPub C2S — ingestOutboxActivity dispatch. Covers the deterministic,
|
|---|
| 2 | // no-network paths: top-level Note creation (real DB), bare-object wrapping, and
|
|---|
| 3 | // input validation. Network verbs (Like/Announce/Follow/Undo, replies) are
|
|---|
| 4 | // verified live against a running server; safeFetch's SSRF pre-flight makes them
|
|---|
| 5 | // non-deterministic to unit-test.
|
|---|
| 6 | //
|
|---|
| 7 | // Run: npm test
|
|---|
| 8 |
|
|---|
| 9 | import { test } from 'node:test';
|
|---|
| 10 | import assert from 'node:assert/strict';
|
|---|
| 11 |
|
|---|
| 12 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 13 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 14 |
|
|---|
| 15 | const dbMod = await import('../src/config/database.js');
|
|---|
| 16 | const db = dbMod.default;
|
|---|
| 17 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 18 | dbMod.initializeDatabase();
|
|---|
| 19 |
|
|---|
| 20 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 21 | .run('u1', 'robin', 'r@test', 'x', 'god');
|
|---|
| 22 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
|
|---|
| 23 | const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get('me');
|
|---|
| 24 | const user = db.prepare('SELECT * FROM users WHERE id = ?').get('u1');
|
|---|
| 25 |
|
|---|
| 26 | test('Create(Note) top-level → a published post with sanitized content', async () => {
|
|---|
| 27 | const out = await AP.ingestOutboxActivity(site, user, {
|
|---|
| 28 | type: 'Create',
|
|---|
| 29 | object: { type: 'Note', content: '<p>Hallo fediverse <script>alert(1)</script></p>' },
|
|---|
| 30 | });
|
|---|
| 31 | assert.equal(out.status, 201);
|
|---|
| 32 | assert.ok(out.id);
|
|---|
| 33 | const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(out.id);
|
|---|
| 34 | assert.equal(post.status, 'published');
|
|---|
| 35 | assert.equal(post.site_id, 's1');
|
|---|
| 36 | assert.match(post.content, /Hallo fediverse/);
|
|---|
| 37 | assert.doesNotMatch(post.content, /<script>/i); // sanitized
|
|---|
| 38 | });
|
|---|
| 39 |
|
|---|
| 40 | test('a bare Note (no Create wrapper) is wrapped and posted', async () => {
|
|---|
| 41 | const out = await AP.ingestOutboxActivity(site, user, { type: 'Note', content: '<p>bare note</p>' });
|
|---|
| 42 | assert.equal(out.status, 201);
|
|---|
| 43 | const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(out.id);
|
|---|
| 44 | assert.match(post.content, /bare note/);
|
|---|
| 45 | });
|
|---|
| 46 |
|
|---|
| 47 | test('empty note → 400', async () => {
|
|---|
| 48 | const out = await AP.ingestOutboxActivity(site, user, { type: 'Create', object: { type: 'Note', content: '' } });
|
|---|
| 49 | assert.equal(out.status, 400);
|
|---|
| 50 | assert.equal(out.error, 'empty_note');
|
|---|
| 51 | });
|
|---|
| 52 |
|
|---|
| 53 | test('unsupported activity type → 400 with detail', async () => {
|
|---|
| 54 | const out = await AP.ingestOutboxActivity(site, user, { type: 'Arrive', object: 'x' });
|
|---|
| 55 | assert.equal(out.status, 400);
|
|---|
| 56 | assert.equal(out.error, 'unsupported_type');
|
|---|
| 57 | assert.equal(out.detail, 'Arrive');
|
|---|
| 58 | });
|
|---|
| 59 |
|
|---|
| 60 | test('Like/Announce/Follow without an object → 400', async () => {
|
|---|
| 61 | for (const type of ['Like', 'Announce', 'Follow']) {
|
|---|
| 62 | const out = await AP.ingestOutboxActivity(site, user, { type, object: null });
|
|---|
| 63 | assert.equal(out.status, 400, type);
|
|---|
| 64 | assert.equal(out.error, 'missing_object', type);
|
|---|
| 65 | }
|
|---|
| 66 | });
|
|---|
| 67 |
|
|---|
| 68 | test('Undo of an unknown inner type → 400', async () => {
|
|---|
| 69 | // Block used to be the unsupported example; it is real now (c2s-block.test).
|
|---|
| 70 | const out = await AP.ingestOutboxActivity(site, user, { type: 'Undo', object: { type: 'Move', object: 'x' } });
|
|---|
| 71 | assert.equal(out.status, 400);
|
|---|
| 72 | assert.equal(out.error, 'unsupported_undo');
|
|---|
| 73 | });
|
|---|
| 74 |
|
|---|
| 75 | test('garbage input → 400, never throws', async () => {
|
|---|
| 76 | assert.equal((await AP.ingestOutboxActivity(site, user, null)).status, 400);
|
|---|
| 77 | assert.equal((await AP.ingestOutboxActivity(site, user, 'nope')).status, 400);
|
|---|
| 78 | assert.equal((await AP.ingestOutboxActivity(site, user, { type: 'Create' })).error, 'missing_object');
|
|---|
| 79 | });
|
|---|