import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { devices, ledgerEvents, type Db } from "@parking/db"; import { createTestDb } from "@parking/db/testing"; import type { FastifyInstance } from "fastify"; import { buildServer } from "../server.js"; import { seedUser, login } from "../test-helpers.js"; // POST /api/setup/test-relay pulses a SAVED controller's barrier relay to prove the // wiring — it physically opens the barrier. Because "a physical open with no matching // signed command is the fraud signal" (append-only-event-chain / reconciliation), the // route must SIGN a barrier_open_command (reason setup.relayTest) BEFORE it fires, and it // must be admin-only. These tests use the `stub-access` controller (pulseOpen only logs — // no real hardware) so they exercise the validate → sign → pulse path safely. let db: Db; let close: () => void; let app: FastifyInstance; const CTL = "ctl-stub"; beforeEach(async () => { const t = createTestDb(); db = t.db; close = t.close; db.insert(devices).values({ id: CTL, category: "access", driverId: "stub-access", config: { relays: [{ relay: 1, direction: "entry" }, { relay: 2, direction: "exit" }] }, enabled: true, }).run(); app = await buildServer({ db }); await app.ready(); }); afterEach(async () => { await app.close(); close(); }); async function pulse( body: unknown, auth?: { cookie: string; csrf: string }, ) { return app.inject({ method: "POST", url: "/api/setup/test-relay", headers: auth ? { cookie: auth.cookie, "x-csrf-token": auth.csrf } : {}, payload: body as Record, }); } describe("POST /api/setup/test-relay", () => { it("is admin-only: a non-site:update user is 403", async () => { await seedUser(db, { username: "op", password: "pw", roleId: "operator", permissions: ["shift:read"] }); const auth = await login(app, "op", "pw"); const res = await pulse({ id: CTL, relay: 1 }, auth); expect(res.statusCode).toBe(403); }); it("requires CSRF on the mutation", async () => { await seedUser(db, { username: "admin", password: "pw" }); const { cookie } = await login(app, "admin", "pw"); const res = await app.inject({ method: "POST", url: "/api/setup/test-relay", headers: { cookie }, // no x-csrf-token payload: { id: CTL, relay: 1 }, }); expect(res.statusCode).toBe(403); }); it("signs a barrier_open_command (reason setup.relayTest) BEFORE firing, then reports ok", async () => { await seedUser(db, { username: "admin", password: "pw" }); const auth = await login(app, "admin", "pw"); const res = await pulse({ id: CTL, relay: 2 }, auth); expect(res.statusCode).toBe(200); expect(res.json()).toMatchObject({ ok: true }); // The deliberate open is EXPLAINED in the signed ledger — not an anomaly. const rows = db.select().from(ledgerEvents).all(); const testOpen = rows.find((r) => r.type === "barrier_open_command"); expect(testOpen, "a barrier_open_command must be signed").toBeTruthy(); expect(testOpen!.source).toBe("manual"); // deliberate human action expect(testOpen!.signature.length).toBeGreaterThan(0); const payload = testOpen!.payload as Record; expect(payload.relayTest).toBe(true); expect(payload.reasonCode).toBe("setup.relayTest"); expect(payload.relay).toBe(2); expect(payload.controllerId).toBe(CTL); expect(payload.operator).toBe("admin"); // attributed to the acting admin }); it("rejects a relay the controller does not declare (400, no ledger row)", async () => { await seedUser(db, { username: "admin", password: "pw" }); const auth = await login(app, "admin", "pw"); const res = await pulse({ id: CTL, relay: 9 }, auth); expect(res.statusCode).toBe(400); expect(db.select().from(ledgerEvents).all()).toHaveLength(0); // nothing signed }); it("404s an unknown controller id", async () => { await seedUser(db, { username: "admin", password: "pw" }); const auth = await login(app, "admin", "pw"); const res = await pulse({ id: "nope", relay: 1 }, auth); expect(res.statusCode).toBe(404); }); it("rejects a bad relay value (non-positive-integer)", async () => { await seedUser(db, { username: "admin", password: "pw" }); const auth = await login(app, "admin", "pw"); expect((await pulse({ id: CTL, relay: 0 }, auth)).statusCode).toBe(400); expect((await pulse({ id: CTL, relay: -1 }, auth)).statusCode).toBe(400); }); });