import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { after, before, test } from 'node:test'; import { createApp } from '../server/app.js'; import { createProjectStore } from '../server/projects.js'; let server; let baseUrl; let cookie; let dataDir; const fakeMedia = { duration: 42, width: 1080, height: 1920, rotation: 0, fps: 30, hasAudio: true }; before(async () => { dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fdc-test-')); const probe = async (file) => { const content = await fs.readFile(file, 'utf8'); if (content.startsWith('kaputt')) throw new Error('Keine Videospur gefunden.'); return fakeMedia; }; const store = createProjectStore({ dataDir, probe }); const app = createApp({ config: { pin: '4711' }, secret: 'geheim', store }); server = app.listen(0); await new Promise((resolve) => server.once('listening', resolve)); baseUrl = `http://127.0.0.1:${server.address().port}`; const res = await fetch(`${baseUrl}/api/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pin: '4711' }), }); cookie = res.headers.get('set-cookie').split(';')[0]; }); after(async () => { server.close(); await fs.rm(dataDir, { recursive: true, force: true }); }); function call(method, url, body, headers = {}) { const isJson = body !== undefined && typeof body === 'object' && !(body instanceof Uint8Array); return fetch(`${baseUrl}/api/${url}`, { method, headers: { cookie, ...(isJson ? { 'Content-Type': 'application/json' } : {}), ...headers }, body: isJson ? JSON.stringify(body) : body, }); } const upload = (id, index, offset, text) => call('PUT', `projects/${id}/clips/${index}?offset=${offset}`, new TextEncoder().encode(text), { 'Content-Type': 'application/octet-stream', }); const clip = (file, size) => ({ file, size, timestamp: '2026-09-21T10:15:30', sender: 'Anna', caption: 'Ordnung', }); async function createProject(clips) { const res = await call('POST', 'projects', { range: { from: '2026-09-21', to: '2026-09-25' }, clips }); assert.equal(res.status, 201); return res.json(); } test('Projekt anlegen und abrufen', async () => { const project = await createProject([clip('a.mp4', 10), clip('b.mov', 20)]); assert.equal(project.clips.length, 2); assert.equal(project.clips[0].status, 'pending'); assert.equal(project.clips[0].received, 0); const res = await call('GET', `projects/${project.id}`); assert.equal((await res.json()).clips[1].file, 'b.mov'); const list = await (await call('GET', 'projects')).json(); assert.ok(list.some((entry) => entry.id === project.id && entry.clipCount === 2)); }); test('Ungültige Clips werden abgelehnt', async () => { for (const clips of [[], [clip('notiz.txt', 10)], [clip('a.mp4', 0)], [{ ...clip('a.mp4', 5), timestamp: 'gestern' }]]) { const res = await call('POST', 'projects', { clips }); assert.equal(res.status, 400); } }); test('Upload in Stücken, Wiederaufnahme und Abschluss mit ffprobe', async () => { const project = await createProject([clip('a.mp4', 10)]); let res = await upload(project.id, 0, 0, 'abcd'); assert.deepEqual(await res.json(), { received: 4, status: 'pending' }); // Falscher Offset: der Server nennt den richtigen Stand res = await upload(project.id, 0, 0, 'abcd'); assert.equal(res.status, 409); assert.equal((await res.json()).received, 4); res = await upload(project.id, 0, 4, 'efghij'); assert.deepEqual(await res.json(), { received: 10, status: 'ready' }); const stored = await (await call('GET', `projects/${project.id}`)).json(); assert.equal(stored.clips[0].status, 'ready'); assert.deepEqual(stored.clips[0].media, fakeMedia); const file = await call('GET', `projects/${project.id}/clips/0/file`); assert.equal(await file.text(), 'abcdefghij'); }); test('Mehr Daten als angekündigt werden abgewiesen, der gültige Anfang bleibt', async () => { const project = await createProject([clip('a.mp4', 5)]); await upload(project.id, 0, 0, 'ab'); const res = await upload(project.id, 0, 2, 'cdefgh'); assert.equal(res.status, 413); const stored = await (await call('GET', `projects/${project.id}`)).json(); assert.equal(stored.clips[0].received, 2); }); test('Datei ohne Videospur wird als Fehler markiert', async () => { const project = await createProject([clip('a.mp4', 6)]); const res = await upload(project.id, 0, 0, 'kaputt'); assert.equal((await res.json()).status, 'error'); const stored = await (await call('GET', `projects/${project.id}`)).json(); assert.equal(stored.clips[0].error, 'Keine Videospur gefunden.'); }); test('Unbekannte Projekte und Clips ergeben 404, auch bei manipulierten IDs', async () => { assert.equal((await call('GET', 'projects/00000000-0000-0000-0000-000000000000')).status, 404); assert.equal((await call('GET', 'projects/..%2F..%2Fetc')).status, 404); const project = await createProject([clip('a.mp4', 5)]); assert.equal((await upload(project.id, 7, 0, 'x')).status, 404); assert.equal((await call('GET', `projects/${project.id}/clips/0/file`)).status, 404); }); test('Projekt löschen entfernt die Dateien', async () => { const project = await createProject([clip('a.mp4', 5)]); await upload(project.id, 0, 0, 'ab'); assert.equal((await call('DELETE', `projects/${project.id}`)).status, 204); assert.equal((await call('GET', `projects/${project.id}`)).status, 404); await assert.rejects(fs.stat(path.join(dataDir, 'projects', project.id))); });