/** * Covers the HTML export's VIDEO path (perf/stability fix P4). The export used to * copy every original — including full-size videos — into a temp dir and then re-read * them into the ZIP (transient 2× disk). It now streams video originals straight from * disk into the archive. The image-only export specs never exercised that branch, so * this drives a real video upload → export → and proves the video entry lands in * Memories.zip (i.e. the streamed-from-original path works and the video isn't dropped). * * The fixture clip is <1s, so ffmpeg extracts no thumbnail frame — but exits 0, so the * compression worker keeps the upload (it isn't auto-cleaned). That's the intended * shape here: the full video is exported even when its thumbnail is absent. */ import { test, expect } from '../../fixtures/test'; import { uploadRaw } from '../../helpers/upload-client'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; const BASE = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101'; test.describe('Export — video streaming (P4)', () => { test('a real video upload is streamed into Memories.zip (not dropped)', async ({ host }) => { test.setTimeout(60_000); const bearer = { Authorization: `Bearer ${host.jwt}` }; // Seed a real video upload owned by the host. const mp4 = readFileSync(join(process.cwd(), 'fixtures', 'media', 'sample.mp4')); const up = await uploadRaw(host.jwt, mp4, { filename: 'clip.mp4', contentType: 'video/mp4' }); expect(up.status).toBe(201); const { id } = await up.json(); // Release the gallery → spawns the real zip + html export jobs. const rel = await fetch(`${BASE}/api/v1/host/gallery/release`, { method: 'POST', headers: bearer }); expect(rel.status).toBe(204); // Wait for the HTML (Memories.zip) job — that's the one whose media staging P4 changed. await expect .poll( async () => { const res = await fetch(`${BASE}/api/v1/export/status`, { headers: bearer }); return (await res.json()).html?.status; }, { timeout: 45_000, intervals: [500] } ) .toBe('done'); // Download Memories.zip via the gated single-use ticket. const ticketRes = await fetch(`${BASE}/api/v1/export/ticket`, { method: 'POST', headers: bearer }); const { ticket } = await ticketRes.json(); const dl = await fetch(`${BASE}/api/v1/export/html?ticket=${encodeURIComponent(ticket)}`); expect(dl.status).toBe(200); const bytes = new Uint8Array(await dl.arrayBuffer()); // Valid ZIP (PK local-file-header magic). expect(Array.from(bytes.subarray(0, 2))).toEqual([0x50, 0x4b]); // ZIP entry names are stored verbatim (uncompressed) in the archive, so the video's // media entry name appears as plaintext bytes. Its presence means the streamed // video entry was written; if the stream had failed, entry.close() would have // errored and the job would never have reached 'done'. const needle = `media/${id}.mp4`; const haystack = new TextDecoder('latin1').decode(bytes); expect(haystack.includes(needle), `Memories.zip must contain ${needle}`).toBe(true); }); });