diff --git a/web/src/app/api/floor/route.test.ts b/web/src/app/api/floor/route.test.ts new file mode 100644 index 0000000..0fa8945 --- /dev/null +++ b/web/src/app/api/floor/route.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from "vitest"; +import { GET, POST } from "./route"; +import { prisma } from "@/lib/db"; + +function post(action: string) { + return new Request("http://localhost/api/floor", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ action }), + }); +} + +async function flag(key: string) { + return (await prisma.config.findUnique({ where: { key } }))?.value ?? null; +} + +async function queuedRequest(album: string, state = "requested") { + return prisma.request.create({ + data: { artist: "A", album, status: "pending", job: { create: { state: state as never } } }, + include: { job: true }, + }); +} + +describe("floor API", () => { + it("GET reports paused=false by default", async () => { + const res = await GET(); + expect(await res.json()).toEqual({ paused: false }); + }); + + it("pause and resume toggle floor.paused", async () => { + await POST(post("pause")); + expect(await flag("floor.paused")).toBe("true"); + const paused = await (await GET()).json(); + expect(paused).toEqual({ paused: true }); + + await POST(post("resume")); + expect(await flag("floor.paused")).toBe("false"); + }); + + it("stop sets both paused and killRequested", async () => { + const res = await POST(post("stop")); + expect(res.status).toBe(200); + expect(await flag("floor.paused")).toBe("true"); + expect(await flag("floor.killRequested")).toBe("true"); + }); + + it("clear deletes only queued (requested) requests", async () => { + const queued = await queuedRequest("Queued"); + const active = await queuedRequest("Downloading", "downloading"); + + const res = await POST(post("clear")); + const body = await res.json(); + expect(body.cleared).toBe(1); + + expect(await prisma.request.findUnique({ where: { id: queued.id } })).toBeNull(); + expect(await prisma.request.findUnique({ where: { id: active.id } })).not.toBeNull(); + }); + + it("rejects an unknown action", async () => { + const res = await POST(post("nope")); + expect(res.status).toBe(400); + }); +}); diff --git a/web/src/app/api/floor/route.ts b/web/src/app/api/floor/route.ts new file mode 100644 index 0000000..4c801f3 --- /dev/null +++ b/web/src/app/api/floor/route.ts @@ -0,0 +1,49 @@ +import { prisma } from "@/lib/db"; + +async function getFlag(key: string): Promise { + const row = await prisma.config.findUnique({ where: { key } }); + return row?.value === "true"; +} + +async function setFlag(key: string, value: boolean): Promise { + const v = value ? "true" : "false"; + await prisma.config.upsert({ + where: { key }, + create: { key, value: v, secret: false }, + update: { value: v }, + }); +} + +export async function GET() { + return Response.json({ paused: await getFlag("floor.paused") }); +} + +export async function POST(request: Request) { + let body: { action?: string }; + try { + body = (await request.json()) as { action?: string }; + } catch { + return Response.json({ error: "invalid JSON" }, { status: 400 }); + } + + switch (body.action) { + case "pause": + await setFlag("floor.paused", true); + return Response.json({ ok: true, paused: true }); + case "resume": + await setFlag("floor.paused", false); + return Response.json({ ok: true, paused: false }); + case "stop": + await setFlag("floor.paused", true); + await setFlag("floor.killRequested", true); + return Response.json({ ok: true, paused: true, stopped: true }); + case "clear": { + const { count } = await prisma.request.deleteMany({ + where: { job: { is: { state: "requested" } } }, + }); + return Response.json({ ok: true, cleared: count }); + } + default: + return Response.json({ error: "unknown action" }, { status: 400 }); + } +}