cameras/src/lib/camera-route.test.ts
Michael Mainguy 71d02195ce Add Vitest test suite with coverage reporting
Sets up Vitest (jsdom, Testing Library, v8 coverage) per the Next 16
testing guide, using Vite's native tsconfig path resolution instead of
vite-tsconfig-paths. A server-only stub and a temp-data helper let
server modules run in isolation. @types/node moves to ^24 to match the
Node 24 runtime (a vitest 5 peer requirement).

First 68 tests cover the discover route (including iss-dbwgww8: no raw
errors in responses), the credential store, the camera registry,
camera-route helpers and the refresh-rate schema. Line coverage is
31.2%, toward the 95% goal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 08:49:50 -05:00

94 lines
3.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { CameraRecord } from "./camera-registry";
const getCameraRecord = vi.fn<(id: string) => Promise<CameraRecord | null>>();
vi.mock("./camera-registry", async (importOriginal) => ({
...(await importOriginal<typeof import("./camera-registry")>()),
getCameraRecord,
}));
const { cameraTarget, cameraErrorResponse } = await import("./camera-route");
const { CameraAuthError, CameraInactiveError } = await import("./camera");
const { CredentialStoreError } = await import("./credential-store");
const ID = "11111111-2222-3333-4444-555555555555";
const record = (host: string): CameraRecord => ({
id: ID,
urn: `urn:uuid:${ID}`,
host,
port: 8080,
lastSeen: "2026-09-19T00:00:00.000Z",
});
async function expectError(result: unknown, status: number, error: string) {
expect(result).toBeInstanceOf(Response);
const res = result as Response;
expect(res.status).toBe(status);
expect(await res.json()).toEqual({ error });
}
describe("cameraTarget", () => {
beforeEach(() => {
getCameraRecord.mockReset();
});
it("resolves a known camera to its registry address", async () => {
getCameraRecord.mockResolvedValue(record("192.168.1.10"));
expect(await cameraTarget(Promise.resolve({ id: ID }))).toEqual({
id: ID,
host: "192.168.1.10",
port: 8080,
});
});
it("rejects a malformed ID without touching the registry", async () => {
await expectError(await cameraTarget(Promise.resolve({ id: "../etc" })), 400, "Invalid camera ID");
expect(getCameraRecord).not.toHaveBeenCalled();
});
it("returns 404 for an unknown camera", async () => {
getCameraRecord.mockResolvedValue(null);
await expectError(
await cameraTarget(Promise.resolve({ id: ID })),
404,
"Unknown camera; scan the network again",
);
});
it.each(["8.8.8.8", "127.0.0.1", "camera.example.com"])(
"refuses a registry entry pointing at non-private host %s",
async (host) => {
getCameraRecord.mockResolvedValue(record(host));
await expectError(
await cameraTarget(Promise.resolve({ id: ID })),
400,
"Camera address is not a private IPv4 address",
);
},
);
});
describe("cameraErrorResponse", () => {
it.each([
["inactive camera", new CameraInactiveError("not activated"), 409, "inactive"],
["rejected login", new CameraAuthError("401"), 401, "auth"],
["credential store problem", new CredentialStoreError("no key"), 500, "store"],
])("maps a %s to status %i with code %s", async (_label, err, status, code) => {
const res = cameraErrorResponse(err);
expect(res.status).toBe(status);
expect(await res.json()).toMatchObject({ code });
});
it("maps any other failure to 502 without a code", async () => {
const res = cameraErrorResponse(new Error("socket hang up"));
expect(res.status).toBe(502);
expect(await res.json()).not.toHaveProperty("code");
});
it("accepts non-Error values", async () => {
const res = cameraErrorResponse("boom");
expect(res.status).toBe(502);
});
});