/** * USER_JOURNEYS.md §7 — liking and commenting. * * Like behavior is asserted deterministically via the feed snapshot; the comment * SSE round-trip is asserted by subscribing to the stream as a second user and * waiting for the `new-comment` event to arrive. */ import { test, expect } from '../../fixtures/test'; import { SseListener } from '../../helpers/sse-listener'; import { uploadRaw, JPEG_MAGIC } from '../../helpers/upload-client'; const BASE = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101'; async function seedUpload(jwt: string, db: any, caption?: string): Promise { const body = new Uint8Array(1024); body.set(JPEG_MAGIC, 0); const res = await uploadRaw(jwt, body, { filename: 'a.jpg', contentType: 'image/jpeg', caption }); if (res.status !== 201) throw new Error(`seed upload failed: ${res.status} ${await res.text()}`); const { id } = await res.json(); await db.setUploadCompressionStatus(id, 'done'); return id; } async function like(jwt: string, uploadId: string): Promise { const res = await fetch(`${BASE}/api/v1/upload/${uploadId}/like`, { method: 'POST', headers: { Authorization: `Bearer ${jwt}` }, }); return res.status; } function findRow(feed: any, id: string): any { const list: any[] = feed.uploads ?? feed.items ?? feed; return list.find((u: any) => u.id === id); } test.describe('Feed — like + comment', () => { test('a like counts once per user and toggles off on repeat (no double-count)', async ({ api, guest, db }) => { const author = await guest('Author'); const liker = await guest('Liker'); const uploadId = await seedUpload(author.jwt, db); // Baseline: nobody has liked yet. let row = findRow(await api.getFeed(liker.jwt), uploadId); expect(row.like_count).toBe(0); expect(row.liked_by_me).toBe(false); // First like → counted exactly once. expect(await like(liker.jwt, uploadId)).toBe(204); row = findRow(await api.getFeed(liker.jwt), uploadId); expect(row.like_count).toBe(1); expect(row.liked_by_me).toBe(true); // Liking again is a toggle → back to zero (guards against a regression that // double-counts a repeated like instead of removing it). expect(await like(liker.jwt, uploadId)).toBe(204); row = findRow(await api.getFeed(liker.jwt), uploadId); expect(row.like_count).toBe(0); expect(row.liked_by_me).toBe(false); // A second distinct user's like is counted independently (per-user semantics). expect(await like(liker.jwt, uploadId)).toBe(204); // liker likes again → 1 expect(await like(author.jwt, uploadId)).toBe(204); // author likes too → 2 row = findRow(await api.getFeed(liker.jwt), uploadId); expect(row.like_count).toBe(2); }); test('comment by user A → SSE new-comment delivered to user B', async ({ guest, db }) => { // SSE frames can take a keep-alive tick to flush through the reverse proxy, so // both the stream connect and the delivery may cost up to ~30s each. test.setTimeout(120_000); const a = await guest('CommenterA'); const b = await guest('ListenerB'); // B subscribes to the stream BEFORE A comments, so the broadcast is captured. const sse = new SseListener(); await sse.start(b.jwt); try { const uploadId = await seedUpload(a.jwt, db, 'pic'); const cRes = await fetch(`${BASE}/api/v1/upload/${uploadId}/comments`, { method: 'POST', headers: { Authorization: `Bearer ${a.jwt}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ body: 'hello from A' }), }); expect(cRes.status).toBe(201); // B must receive the new-comment event for this upload. Generous timeout: SSE // frames flush on the keep-alive tick through the compressing reverse proxy. const evt = await sse.waitForEvent( 'new-comment', (e) => e.data?.upload_id === uploadId, 45_000 ); expect(evt.data.comment_count).toBe(1); } finally { sse.stop(); } }); });