Changeset 2086ea2 in Klonkt for test


Ignore:
Timestamp:
08/12/2026 09:16:09 PM (3 months ago)
Author:
Robin <roboburr@…>
Children:
39dab6b
Parents:
5412cbb
Message:

Ward-PWA: de composer met media

Schrijven met foto, geluid of video, in de browser.

DEZELFDE POORT ALS DE APP, en dat is de kern. De composer bouwt precies
de AS2 Create die Shaer over C2S stuurt en geeft die aan
ingestOutboxActivity: die doet de compose-poort (par. 5), de
attachment-validatie en de zichtbaarheid. GEEN eigen posting-pad, want
dan zou de web-ward een sluiproute om de guardians heen zijn. Een test
bewaakt precies dat: een gated ward wordt ook via de browser
tegengehouden.

Media gaan eerst apart naar /ward/api/upload, in DEZELFDE bak als de
app (reply-media), zodat een foto van een app-ward en een browser-ward
niet van twee plekken federeert. De server maakt de rest af zoals altijd:
posterframe voor video, waveform voor audio. De post draagt daarna alleen
de url, dus de payload blijft klein.

Sessie-geauthenticeerd, niet met een bearer: /ward draait op je eigen
ingelogde Klonkt, dus de browser heeft geen token nodig zoals de app.

New file: (geen)
Changed files:
src/routes/ward.js

  • /api/upload: sessie-geauthenticeerd, zelfde filters/map als C2S
  • /api/post: bouwt de AS2 Create, door ingestOutboxActivity

src/views/pages/ward.ejs

  • schrijfknop, composer-dialoog met tekst, media en zichtbaarheid

src/assets/js/ward.js

  • upload-per-bestand, media-chips, plaatsen; opruiming van een dubbel geraakte doc-comment

src/assets/css/ward.css

  • composer, media-chips, tools

src/services/i18n.js

  • twaalf sleutels in nl, en, de

test/ward-pwa.test.js

  • de gate geldt ook in de browser; een vrij account plaatst met media; leeg wordt geweigerd; een vreemde kan niet uploaden

remarks: 903 tests groen. Opnemen van audio/video in de browser zelf
(MediaRecorder) is nog niet gebouwd; nu kies je een bestand. Bead volgt.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • test/ward-pwa.test.js

    r5412cbb r2086ea2  
    247247  } finally { server.close(); }
    248248});
     249
     250test('de compose-poort geldt ook voor de web-ward', async () => {
     251  // mee IS een ward (heeft guardians), dus compose is gated. De web-composer
     252  // gaat door DEZELFDE poort als de app (ingestOutboxActivity), dus hij wordt
     253  // net zo tegengehouden. Zonder deze gelijkheid zou de browser een sluiproute
     254  // om de guardians heen zijn.
     255  const { server, port } = await listen(appWith({ user: { id: 'u1' } }));
     256  try {
     257    const r = await fetch(`http://127.0.0.1:${port}/ward/api/post`, {
     258      method: 'POST', headers: { 'Content-Type': 'application/json' },
     259      body: JSON.stringify({ text: 'mag dit', visibility: 'friends' }),
     260    });
     261    assert.equal(r.status, 403);
     262    assert.equal((await r.json()).error, 'gated_compose');
     263  } finally { server.close(); }
     264});
     265
     266test('een niet-gated account plaatst met media', async () => {
     267  // Een account ZONDER guardians is geen ward, dus compose is open. De post
     268  // landt echt, met de foto eraan.
     269  db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
     270    .run('u3', 'vrij', 'v@t', 'x', 'user');
     271  db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
     272    .run('s3', 'vrij', 'Vrij', 'u3');
     273
     274  const { server, port } = await listen(appWith({ user: { id: 'u3' } }));
     275  try {
     276    const r = await fetch(`http://127.0.0.1:${port}/ward/api/post`, {
     277      method: 'POST', headers: { 'Content-Type': 'application/json' },
     278      body: JSON.stringify({
     279        text: 'kijk mijn tekening',
     280        visibility: 'friends',
     281        media: [{ url: '/media/reply-media/x.jpg', mediaType: 'image/jpeg', name: 'tekening.jpg' }],
     282      }),
     283    });
     284    assert.equal(r.status, 201, JSON.stringify(await r.clone().json().catch(() => ({}))));
     285    const row = db.prepare("SELECT content FROM posts WHERE site_id = 's3' ORDER BY created_at DESC LIMIT 1").get();
     286    assert.ok(row, 'de post is nergens opgeslagen');
     287    // De foto reist als attachment mee, niet als kale tekst.
     288    assert.match(row.content, /x\.jpg|<img|video|Image/i);
     289  } finally { server.close(); }
     290});
     291
     292test('een lege post wordt geweigerd', async () => {
     293  const { server, port } = await listen(appWith({ user: { id: 'u1' } }));
     294  try {
     295    const r = await fetch(`http://127.0.0.1:${port}/ward/api/post`, {
     296      method: 'POST', headers: { 'Content-Type': 'application/json' },
     297      body: JSON.stringify({ text: '   ', media: [] }),
     298    });
     299    assert.equal(r.status, 400);
     300    assert.equal((await r.json()).error, 'empty');
     301  } finally { server.close(); }
     302});
     303
     304test('een vreemde kan niet uploaden', async () => {
     305  // De upload-poort moet net zo dicht zijn als de rest; anders vult iemand
     306  // ongevraagd de media-map.
     307  const { server, port } = await listen(appWith({}));
     308  try {
     309    const r = await fetch(`http://127.0.0.1:${port}/ward/api/upload`, { method: 'POST', redirect: 'manual' });
     310    assert.notEqual(r.status, 201);
     311  } finally { server.close(); }
     312});
Note: See TracChangeset for help on using the changeset viewer.