import { renderToStaticMarkup } from "react-dom/server"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Clip } from "@/lib/recordings"; const requirePageAccess = vi.fn(); vi.mock("@/lib/access", () => ({ requirePageAccess })); const listClips = vi.fn<() => Promise>(); const allCameraRecords = vi.fn(); vi.mock("@/lib/recordings", async (importOriginal) => ({ ...(await importOriginal()), listClips, })); vi.mock("@/lib/camera-registry", async (importOriginal) => ({ ...(await importOriginal()), allCameraRecords, })); class NotFound extends Error {} vi.mock("next/navigation", () => ({ notFound: () => { throw new NotFound(); }, })); const { default: PlayClipPage, generateMetadata } = await import("./page"); const ID = "22ec0000-8b90-11b5-845d-d03bf404af54"; const CLIP = `cam-${ID}-sub-rec/2026-09-19_17-06-18-101298.mp4`; const clip: Clip = { id: CLIP, cameraId: ID, startedAt: "2026-09-19T22:06:18.000Z", bytes: 2_582_503 }; const record = { id: ID, urn: `urn:uuid:${ID}`, host: "192.168.17.129", port: 80, name: "Porch", lastSeen: "" }; const props = (id: string = CLIP) => ({ params: Promise.resolve({}), searchParams: Promise.resolve({ clip: id }), }); /** No ?clip= at all, which a default parameter would quietly paper over. */ const noClip = { params: Promise.resolve({}), searchParams: Promise.resolve({}) }; const markup = async (id?: string) => renderToStaticMarkup(await PlayClipPage(props(id))); beforeEach(() => { requirePageAccess.mockReset().mockResolvedValue(undefined); listClips.mockReset().mockResolvedValue([clip]); allCameraRecords.mockReset().mockResolvedValue([record]); }); describe("/recordings/play", () => { it("checks access before looking anything up", async () => { requirePageAccess.mockRejectedValue(new Error("redirect /login")); await expect(PlayClipPage(props())).rejects.toThrow("redirect /login"); await expect(generateMetadata(props())).rejects.toThrow("redirect /login"); expect(listClips).not.toHaveBeenCalled(); }); it("plays the clip through our own route, with the camera and time shown", async () => { const html = await markup(); expect(html).toContain(`/api/recordings/clip?clip=${encodeURIComponent(CLIP)}`.replace(/&/g, "&")); expect(html).toContain("Porch"); expect(html).toContain('dateTime="2026-09-19T22:06:18.000Z"'); }); it("titles the window after the camera and the time", async () => { const { title } = await generateMetadata(props()); expect(title).toContain("Porch"); }); it("falls back to the address for an unnamed camera, and says so for an unknown one", async () => { allCameraRecords.mockResolvedValue([{ ...record, name: undefined }]); expect(await markup()).toContain("192.168.17.129:80"); allCameraRecords.mockResolvedValue([]); expect(await markup()).toContain("Unknown camera"); }); it.each([ ["a clip id that isn't ours", "../../etc/passwd"], ["a clip that isn't on disk", `cam-${ID}-main-rec/2026-01-01_00-00-00-000000.mp4`], ["an empty clip id", ""], ])("is not found for %s", async (_label, id) => { await expect(PlayClipPage(props(id))).rejects.toBeInstanceOf(NotFound); }); it("is not found when no clip is named at all", async () => { await expect(PlayClipPage(noClip)).rejects.toBeInstanceOf(NotFound); await expect(generateMetadata(noClip)).rejects.toBeInstanceOf(NotFound); }); });