128 lines
5.6 KiB
JavaScript
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);
|
|
});
|