diff --git a/admin-auth.test.js b/admin-auth.test.js new file mode 100644 index 0000000..2b82dee --- /dev/null +++ b/admin-auth.test.js @@ -0,0 +1,93 @@ +// Integration test for the minimal admin auth gate on /api/admin/* +// (dev-plan.md T6). Spawns server.js on a throwaway port, same isolation +// approach as uploads-route.test.js. +const test = require("node:test"); +const assert = require("node:assert/strict"); +const { spawn } = require("node:child_process"); + +const PORT = 19934 + Math.floor(Math.random() * 1000); +const BASE = `http://localhost:${PORT}`; +const ADMIN_PASSWORD = "test-password-only-for-this-run"; + +function waitForServer(url, timeoutMs = 10000) { + const deadline = Date.now() + timeoutMs; + return (async function poll() { + while (Date.now() < deadline) { + try { + await fetch(url); + return; + } catch { + await new Promise((r) => setTimeout(r, 100)); + } + } + throw new Error("server did not come up in time"); + })(); +} + +function sessionCookie(loginRes) { + const setCookie = loginRes.headers.get("set-cookie") || ""; + return setCookie.split(";")[0]; +} + +let child; +test.before(async () => { + child = spawn(process.execPath, ["server.js"], { + cwd: __dirname, + env: { ...process.env, PORT: String(PORT), H3_CONSOLE_BASE: "", H3_CONSOLE_TOKEN: "", ADMIN_PASSWORD }, + stdio: "pipe", + }); + await waitForServer(BASE); +}); + +test.after(() => child.kill()); + +test("any /api/admin/* route returns 401 without a session cookie", async () => { + const res = await fetch(`${BASE}/api/admin/library`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ prompt: "x", videoUrl: "http://example.com/x.mp4" }), + }); + assert.equal(res.status, 401); +}); + +test("logging in with the wrong password is rejected and sets no cookie", async () => { + const res = await fetch(`${BASE}/api/admin/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password: "definitely-wrong" }), + }); + assert.equal(res.status, 401); + assert.equal(res.headers.get("set-cookie"), null); +}); + +test("logging in with the right password grants a session that unlocks /api/admin/*, and logout revokes it", async () => { + const loginRes = await fetch(`${BASE}/api/admin/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password: ADMIN_PASSWORD }), + }); + assert.equal(loginRes.status, 200); + const cookie = sessionCookie(loginRes); + assert.match(cookie, /^admin_session=.+/); + + const authedRes = await fetch(`${BASE}/api/admin/library`, { + method: "POST", + headers: { "Content-Type": "application/json", Cookie: cookie }, + body: JSON.stringify({ prompt: "authed test entry", videoUrl: "http://example.com/x.mp4" }), + }); + assert.equal(authedRes.status, 200); + const created = await authedRes.json(); + + // clean up the entry this test created in the real library.json + await fetch(`${BASE}/api/admin/library/${created.id}`, { method: "DELETE", headers: { Cookie: cookie } }); + + const logoutRes = await fetch(`${BASE}/api/admin/logout`, { method: "POST", headers: { Cookie: cookie } }); + assert.equal(logoutRes.status, 200); + + const afterLogout = await fetch(`${BASE}/api/admin/library`, { + method: "POST", + headers: { "Content-Type": "application/json", Cookie: cookie }, + body: JSON.stringify({ prompt: "should be rejected", videoUrl: "http://example.com/x.mp4" }), + }); + assert.equal(afterLogout.status, 401); +}); diff --git a/admin-login.html b/admin-login.html new file mode 100644 index 0000000..8114dc2 --- /dev/null +++ b/admin-login.html @@ -0,0 +1,51 @@ + + +
+ +