pixel8/tests/phase2.test.js

128 lines
5.6 KiB
JavaScript

"use strict";
const { test, before, after } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { execFileSync } = require("node:child_process");
const { loadConfig } = require("../src/config");
const { Engine } = require("../src/engine");
const { openStore } = require("../src/store");
const { createServer } = require("../src/server");
const { solidSource } = require("../src/sources");
const { QueueController } = require("../src/queue");
let server, base, store, engine, queue, tmpDir, mediaDir;
const HEADER = { authorization: "Bearer secret" };
function makePng(tmp, color = "red") {
const p = path.join(tmp, `in-${color}.png`);
execFileSync("ffmpeg", ["-y", "-loglevel", "error", "-f", "lavfi", "-i", `color=c=${color}:s=32x32`, "-frames:v", "1", p], { timeout: 20000 });
return fs.readFileSync(p);
}
before(async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pixel8-p2-"));
mediaDir = path.join(tmpDir, "media");
const config = loadConfig({ PORT: "0", HOST: "127.0.0.1", DB_PATH: path.join(tmpDir, "t.db"), MEDIA_DIR: mediaDir, ADMIN_TOKEN: "secret", MAX_FRAMES: "30" });
engine = new Engine({ fps: 30 });
engine.setSource(solidSource([0, 0, 0]), "test:black");
store = openStore(config.dbPath);
queue = new QueueController({ store, engine, defaultItemMs: 1000, fps: 10 });
({ server } = createServer(config, { engine, store, queue }));
await new Promise((r) => server.listen(0, "127.0.0.1", r));
base = `http://127.0.0.1:${server.address().port}`;
});
after(async () => {
queue.stop();
if (server.closeAllConnections) server.closeAllConnections();
await new Promise((r) => server.close(r));
store.close();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test("upload PNG -> pending artwork with 8x8 preview + cached frames", async () => {
let png;
try { png = makePng(tmpDir, "red"); } catch { return; } // ffmpeg unavailable: skip
const fd = new FormData();
fd.set("title", "Red Square");
fd.set("artist", "Tester");
fd.set("license", "CC-BY-4.0");
fd.set("fit", "cover");
fd.set("file", new Blob([png], { type: "image/png" }), "red.png");
const r = await fetch(base + "/api/uploads", { method: "POST", body: fd });
assert.equal(r.status, 201);
const j = await r.json();
assert.equal(j.status, "pending");
assert.equal(j.preview.pixels.length, 64);
assert.equal(j.preview.frameCount, 1);
const px = j.preview.pixels[0];
assert.ok(px[0] > 200, `expected dominant red, got ${px}`);
assert.ok(px[1] < 60 && px[2] < 60, `expected low green/blue, got ${px}`);
assert.ok(fs.existsSync(path.join(mediaDir, "artworks", j.id, "source.png")));
});
test("SVG upload is rejected (future work, R-xxx)", async () => {
const fd = new FormData();
fd.set("title", "svg");
fd.set("file", new Blob(['<svg xmlns="http://www.w3.org/2000/svg"/>'], { type: "image/svg+xml" }), "x.svg");
const r = await fetch(base + "/api/uploads", { method: "POST", body: fd });
assert.equal(r.status, 415);
const j = await r.json();
assert.equal(j.code, "unsupported_format");
});
test("content sniffing rejects fake images", async () => {
const fd = new FormData();
fd.set("title", "fake");
fd.set("file", new Blob([Buffer.from("this is not a png")], { type: "image/png" }), "fake.png");
const r = await fetch(base + "/api/uploads", { method: "POST", body: fd });
assert.equal(r.status, 415);
const j = await r.json();
assert.equal(j.code, "invalid_content");
});
test("pending preview requires admin; approved flows through queue", async () => {
let png;
try { png = makePng(tmpDir, "blue"); } catch { return; }
const fd = new FormData();
fd.set("title", "Queue Me");
fd.set("artist", "Queue Tester");
fd.set("file", new Blob([png], { type: "image/png" }), "blue.png");
const up = await (await fetch(base + "/api/uploads", { method: "POST", body: fd })).json();
// pending preview gated
assert.equal((await fetch(`${base}/api/artworks/${up.id}/preview`)).status, 401);
assert.equal((await fetch(`${base}/api/artworks/${up.id}/preview`, { headers: HEADER })).status, 200);
// reject first: not queueable
const bad = await fetch(base + "/api/admin/queue", { method: "POST", headers: { ...HEADER, "content-type": "application/json" }, body: JSON.stringify({ artwork_id: up.id, duration_ms: 1000 }) });
assert.equal(bad.status, 400);
// approve
const ok = await fetch(`${base}/api/admin/artworks/${up.id}`, { method: "PATCH", headers: { ...HEADER, "content-type": "application/json" }, body: JSON.stringify({ approval_status: "approved" }) });
assert.equal(ok.status, 200);
// enqueue
const enq = await fetch(base + "/api/admin/queue", { method: "POST", headers: { ...HEADER, "content-type": "application/json" }, body: JSON.stringify({ artwork_id: up.id, duration_ms: 1000 }) });
assert.equal(enq.status, 201);
// play from queue
queue.tick();
assert.equal(engine.currentMeta && engine.currentMeta.artworkId, up.id);
// public queue endpoint shows it
const pub = await (await fetch(base + "/api/queue")).json();
assert.ok(pub.items.some((i) => i.artwork_id === up.id));
});
test("upload deduplicates by content hash (R-019)", async () => {
let png;
try { png = makePng(tmpDir, "green"); } catch { return; }
const mk = () => {
const fd = new FormData();
fd.set("title", "Green");
fd.set("file", new Blob([png], { type: "image/png" }), "green.png");
return fetch(base + "/api/uploads", { method: "POST", body: fd }).then((r) => r.json());
};
const a = await mk();
const b = await mk();
assert.equal(b.deduplicated, true);
assert.equal(a.id, b.id);
});