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>
94 lines
3.1 KiB
TypeScript
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);
|
|
});
|
|
});
|