Projekte anlegen und Clips in Stücken hochladen
Ein Projekt hält die ausgewählten Clips samt Absender und Beschriftung. Videos kommen in Stücken bis 16 MB; die Größe der Teildatei ist der Stand, sodass ein abgebrochener Upload dort weitermacht. Nach dem letzten Stück liest ffprobe Länge, Größe, Drehung und Tonspur aus. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6f583c8f5b
commit
782d306cb1
7 files changed
+549
-2
No files matched your search
@@ -0,0 +1,143 @@
|
||||
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)));
|
||||
});
|
||||
Reference in new issue
Block a user