Index: test/ward-pwa.test.js
===================================================================
--- test/ward-pwa.test.js	(revision 5412cbbcf42cb02fb3fb6d67ef433290c95bb38c)
+++ test/ward-pwa.test.js	(revision 2086ea255ae8ea5a474fc805eda10bd71146c947)
@@ -247,2 +247,66 @@
   } finally { server.close(); }
 });
+
+test('de compose-poort geldt ook voor de web-ward', async () => {
+  // mee IS een ward (heeft guardians), dus compose is gated. De web-composer
+  // gaat door DEZELFDE poort als de app (ingestOutboxActivity), dus hij wordt
+  // net zo tegengehouden. Zonder deze gelijkheid zou de browser een sluiproute
+  // om de guardians heen zijn.
+  const { server, port } = await listen(appWith({ user: { id: 'u1' } }));
+  try {
+    const r = await fetch(`http://127.0.0.1:${port}/ward/api/post`, {
+      method: 'POST', headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ text: 'mag dit', visibility: 'friends' }),
+    });
+    assert.equal(r.status, 403);
+    assert.equal((await r.json()).error, 'gated_compose');
+  } finally { server.close(); }
+});
+
+test('een niet-gated account plaatst met media', async () => {
+  // Een account ZONDER guardians is geen ward, dus compose is open. De post
+  // landt echt, met de foto eraan.
+  db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+    .run('u3', 'vrij', 'v@t', 'x', 'user');
+  db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
+    .run('s3', 'vrij', 'Vrij', 'u3');
+
+  const { server, port } = await listen(appWith({ user: { id: 'u3' } }));
+  try {
+    const r = await fetch(`http://127.0.0.1:${port}/ward/api/post`, {
+      method: 'POST', headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        text: 'kijk mijn tekening',
+        visibility: 'friends',
+        media: [{ url: '/media/reply-media/x.jpg', mediaType: 'image/jpeg', name: 'tekening.jpg' }],
+      }),
+    });
+    assert.equal(r.status, 201, JSON.stringify(await r.clone().json().catch(() => ({}))));
+    const row = db.prepare("SELECT content FROM posts WHERE site_id = 's3' ORDER BY created_at DESC LIMIT 1").get();
+    assert.ok(row, 'de post is nergens opgeslagen');
+    // De foto reist als attachment mee, niet als kale tekst.
+    assert.match(row.content, /x\.jpg|<img|video|Image/i);
+  } finally { server.close(); }
+});
+
+test('een lege post wordt geweigerd', async () => {
+  const { server, port } = await listen(appWith({ user: { id: 'u1' } }));
+  try {
+    const r = await fetch(`http://127.0.0.1:${port}/ward/api/post`, {
+      method: 'POST', headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ text: '   ', media: [] }),
+    });
+    assert.equal(r.status, 400);
+    assert.equal((await r.json()).error, 'empty');
+  } finally { server.close(); }
+});
+
+test('een vreemde kan niet uploaden', async () => {
+  // De upload-poort moet net zo dicht zijn als de rest; anders vult iemand
+  // ongevraagd de media-map.
+  const { server, port } = await listen(appWith({}));
+  try {
+    const r = await fetch(`http://127.0.0.1:${port}/ward/api/upload`, { method: 'POST', redirect: 'manual' });
+    assert.notEqual(r.status, 201);
+  } finally { server.close(); }
+});
