| 1 | // Een C2S-post kon nooit een titel hebben (shaer-uply): c2sCreatePost schreef
|
|---|
| 2 | // '' in posts.title en in posts_fts en las `name` op het binnenkomende object
|
|---|
| 3 | // nooit. Een client die er een zette zag hem geruisloos verdwijnen -- de API
|
|---|
| 4 | // was daarmee de enige publicatieweg die iets niet kon wat het web wel kan.
|
|---|
| 5 | // Gevonden door R9999, dat alleen over C2S publiceert.
|
|---|
| 6 | import { test } from 'node:test';
|
|---|
| 7 | import assert from 'node:assert/strict';
|
|---|
| 8 |
|
|---|
| 9 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 10 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 11 |
|
|---|
| 12 | const dbMod = await import('../src/config/database.js');
|
|---|
| 13 | const db = dbMod.default;
|
|---|
| 14 | dbMod.initializeDatabase();
|
|---|
| 15 | const AP = (await import('../src/services/ActivityPubService.js')).default;
|
|---|
| 16 |
|
|---|
| 17 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 18 | .run('u1', 'robin', 'u1@t', 'x', 'god');
|
|---|
| 19 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1');
|
|---|
| 20 | const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get('kid');
|
|---|
| 21 | const user = db.prepare('SELECT * FROM users WHERE id = ?').get('u1');
|
|---|
| 22 |
|
|---|
| 23 | function maak(object) {
|
|---|
| 24 | return AP.ingestOutboxActivity(site, user, {
|
|---|
| 25 | type: 'Create',
|
|---|
| 26 | object: {
|
|---|
| 27 | type: 'Note',
|
|---|
| 28 | to: ['https://test.example/ap/users/kid/followers'],
|
|---|
| 29 | cc: ['https://www.w3.org/ns/activitystreams#Public'],
|
|---|
| 30 | ...object,
|
|---|
| 31 | },
|
|---|
| 32 | });
|
|---|
| 33 | }
|
|---|
| 34 |
|
|---|
| 35 | test('de titel van een C2S-post komt in de kolom, de zoekindex en de note', async () => {
|
|---|
| 36 | const r = await maak({ name: 'Mijn eerste getitelde note', content: '<p>de inhoud</p>' });
|
|---|
| 37 | assert.equal(r.status, 201);
|
|---|
| 38 |
|
|---|
| 39 | const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(r.id);
|
|---|
| 40 | assert.equal(post.title, 'Mijn eerste getitelde note', 'de kolom draagt de titel');
|
|---|
| 41 |
|
|---|
| 42 | // Vindbaar OP de titel, niet alleen op de inhoud -- de tweede schrijfplek
|
|---|
| 43 | // uit shaer-uply. Was alleen de kolom gerepareerd, dan faalde deze.
|
|---|
| 44 | const hit = db.prepare("SELECT post_id FROM posts_fts WHERE posts_fts MATCH 'getitelde'").get();
|
|---|
| 45 | assert.ok(hit && hit.post_id === r.id, 'de titel staat in de zoekindex');
|
|---|
| 46 |
|
|---|
| 47 | // En hij federeert zoals een webpost: Mastodon negeert `name` op een Note,
|
|---|
| 48 | // dus buildNote vouwt de titel als vetgedrukte eerste regel in de content.
|
|---|
| 49 | const note = AP.buildNote('https://test.example', site, post);
|
|---|
| 50 | assert.match(note.content, /^<p><strong>Mijn eerste getitelde note<\/strong><\/p>/, 'vetgedrukte eerste regel');
|
|---|
| 51 | });
|
|---|
| 52 |
|
|---|
| 53 | test('geen echte titel blijft een lege titel: zonder name, en met alleen witruimte', async () => {
|
|---|
| 54 | // Een regel met twee kanten (samengevoegd 25-8): wat er niet is, wordt niet
|
|---|
| 55 | // verzonnen -- of het veld nu ontbreekt of alleen witruimte draagt.
|
|---|
| 56 | const zonder = await maak({ content: '<p>gewoon een note</p>' });
|
|---|
| 57 | assert.equal(zonder.status, 201);
|
|---|
| 58 | const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(zonder.id);
|
|---|
| 59 | assert.equal(post.title, '', 'geen name is een lege titel, geen undefined of null');
|
|---|
| 60 | const note = AP.buildNote('https://test.example', site, post);
|
|---|
| 61 | assert.ok(!note.content.startsWith('<p><strong>'), 'geen verzonnen kopregel');
|
|---|
| 62 |
|
|---|
| 63 | const wit = await maak({ name: ' ', content: '<p>z</p>' });
|
|---|
| 64 | const witPost = db.prepare('SELECT title FROM posts WHERE id = ?').get(wit.id);
|
|---|
| 65 | assert.equal(witPost.title, '', 'witruimte trimt weg tot leeg');
|
|---|
| 66 | const witNote = AP.buildNote('https://test.example', site, db.prepare('SELECT * FROM posts WHERE id = ?').get(wit.id));
|
|---|
| 67 | assert.ok(!witNote.content.startsWith('<p><strong>'), 'en er komt geen lege kopregel');
|
|---|
| 68 | });
|
|---|
| 69 |
|
|---|
| 70 | test('de titel is platte tekst: HTML erin wordt tekst, en de grens is 200', async () => {
|
|---|
| 71 | // `name` is per AS2 platte tekst, maar een client kan sturen wat hij wil.
|
|---|
| 72 | // De kolom en de views verwachten tekst; opmaak wordt dus tekst, en een
|
|---|
| 73 | // scriptpoging houdt niets uitvoerbaars over.
|
|---|
| 74 | const r = await maak({ name: 'Dag <b>wereld</b><script>boem()</script>', content: '<p>x</p>' });
|
|---|
| 75 | const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(r.id);
|
|---|
| 76 | assert.ok(!post.title.includes('<'), 'geen tags in de kolom');
|
|---|
| 77 | assert.match(post.title, /^Dag wereld/, 'de tekst zelf blijft');
|
|---|
| 78 |
|
|---|
| 79 | // En de note escapet hem daarna zelf weer voor de vetgedrukte regel, dus
|
|---|
| 80 | // ook daar geen tag-injectie via de titel.
|
|---|
| 81 | const note = AP.buildNote('https://test.example', site, post);
|
|---|
| 82 | assert.ok(!/<script/i.test(note.content), 'niets uitvoerbaars in de note');
|
|---|
| 83 |
|
|---|
| 84 | const lang = await maak({ name: 'x'.repeat(500), content: '<p>y</p>' });
|
|---|
| 85 | const langePost = db.prepare('SELECT title FROM posts WHERE id = ?').get(lang.id);
|
|---|
| 86 | assert.equal(langePost.title.length, 200, 'de huisgrens van 200, zoals content warning en sitetitel');
|
|---|
| 87 | });
|
|---|
| 88 |
|
|---|