import { mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // Access control is tested in src/app/api/access.test.ts; here requests are allowed. vi.mock("@/lib/access", () => ({ apiAccessDenied: async () => null })); const { GET, DELETE } = await import("./route"); const ID = "11111111-2222-3333-4444-555555555555"; const NAME = "2026-09-19_17-06-18-101298.mp4"; const CLIP = `cam-${ID}-sub-rec/${NAME}`; const BODY = "0123456789"; let dir: string; beforeEach(async () => { dir = await mkdtemp(path.join(os.tmpdir(), "clip-route-")); vi.stubEnv("RECORDINGS_DIR", dir); const folder = path.join(dir, `cam-${ID}-sub-rec`); await (await import("node:fs/promises")).mkdir(folder, { recursive: true }); await writeFile(path.join(folder, NAME), BODY); }); afterEach(() => rm(dir, { recursive: true, force: true })); const url = (clip = CLIP) => `http://localhost/api/recordings/clip?clip=${encodeURIComponent(clip)}`; const get = (clip?: string, headers?: HeadersInit) => GET(new Request(url(clip), { headers })); const text = async (res: Response) => Buffer.from(await res.arrayBuffer()).toString(); describe("GET /api/recordings/clip", () => { it("serves the whole clip as seekable video", async () => { const res = await get(); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("video/mp4"); expect(res.headers.get("Accept-Ranges")).toBe("bytes"); expect(res.headers.get("Content-Length")).toBe(String(BODY.length)); expect(res.headers.get("Cache-Control")).toBe("no-store"); expect(await text(res)).toBe(BODY); }); it("serves a byte range so the player can seek", async () => { const res = await get(CLIP, { range: "bytes=2-5" }); expect(res.status).toBe(206); expect(res.headers.get("Content-Range")).toBe(`bytes 2-5/${BODY.length}`); expect(res.headers.get("Content-Length")).toBe("4"); expect(await text(res)).toBe("2345"); }); it("serves an open-ended range and a suffix range", async () => { expect(await text(await get(CLIP, { range: "bytes=7-" }))).toBe("789"); expect(await text(await get(CLIP, { range: "bytes=-3" }))).toBe("789"); }); it("refuses a range past the end", async () => { const res = await get(CLIP, { range: "bytes=50-60" }); expect(res.status).toBe(416); expect(res.headers.get("Content-Range")).toBe(`bytes */${BODY.length}`); }); it.each([ ["a traversal", "../../etc/passwd"], ["a folder that isn't ours", `secrets/${NAME}`], ["a name that isn't a clip", `cam-${ID}-sub-rec/passwd`], ["a clip that doesn't exist", `cam-${ID}-main-rec/${NAME}`], ["nothing", ""], ])("is not found for %s", async (_l, clip) => { expect((await get(clip)).status).toBe(404); }); }); describe("DELETE /api/recordings/clip", () => { it("deletes the clip", async () => { expect((await DELETE(new Request(url(), { method: "DELETE" }))).status).toBe(204); expect((await get()).status).toBe(404); }); it.each([["../../etc/passwd"], [""], [`cam-${ID}-sub-rec/2026-01-01_00-00-00-000000.mp4`]])( "is not found for %j", async (clip) => { expect((await DELETE(new Request(url(clip), { method: "DELETE" }))).status).toBe(404); }, ); });