test: prove the setup flow against a real database and a real grant

Four claims the unit suite cannot reach, because each is about what is in the
database afterwards: the answers persist and a replay writes nothing new, the
module derivation happens once and leaves a later Settings change alone, a
restart resets the steps and not the modules, and a delegate with a live
accepted grant and a switched session is refused on all three writes — with
neither record acquiring a row, which is the quiet failure a fall-back to the
caller's own record would produce.

The demo guard gains the three paths and, beside them, the case that matters
more: admitting one verb on a path must not open a sibling.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 11:17:00 +02:00
1 parent 3f44b882ac
commit 3f015effec
3 files changed
+459 -6

No files matched your search

@@ -51,6 +51,31 @@ describe("proxy.ts DEMO_MODE mutation allowlist", () => {
expect(res.status).not.toBe(403);
});
it("permits the three setup-flow writes so the demo can finish onboarding", () => {
// v1.39 (C1) — the design spec requires the demo to complete the same flow
// with preset answers, and none of the three creates health data: closed
// enum answers, the step ledger, and the module map derived from them.
for (const [path, method] of [
["/api/onboarding/answers", "PATCH"],
["/api/onboarding/complete", "POST"],
["/api/onboarding/restart", "POST"],
] as const) {
expect(
proxy(makeRequest(path, method)).status,
`${method} ${path} is blocked in the demo`,
).not.toBe(403);
}
});
it("does not drag in a sibling verb on an allowlisted onboarding path", () => {
// The allowlist pins method per path, so admitting the answers PATCH must
// not open a DELETE somebody adds to that route later.
expect(proxy(makeRequest("/api/onboarding/answers", "DELETE")).status).toBe(
403,
);
expect(proxy(makeRequest("/api/onboarding/step", "POST")).status).toBe(403);
});
it("still blocks a health-data mutation (POST /api/measurements)", () => {
const res = proxy(makeRequest("/api/measurements", "POST"));
expect(res.status).toBe(403);
@@ -1,12 +1,21 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";
vi.mock("@/lib/db", () => ({
prisma: {
user: { update: vi.fn() },
auditLog: { create: vi.fn() },
},
}));
vi.mock("@/lib/db", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/db")>();
return {
...actual,
prisma: {
user: { update: vi.fn(), findUnique: vi.fn() },
auditLog: { create: vi.fn() },
// v1.39 (C1) — the needs half of the route looks for a setup row before
// it does anything. These cases are the LEGACY caller, which has none,
// so the lookup answers null and the route falls straight through to the
// acknowledgement it always returned.
onboardingRecord: { findUnique: vi.fn(), update: vi.fn() },
},
};
});
vi.mock("@/lib/auth/session", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/auth/session")>();
@@ -58,6 +67,7 @@ beforeEach(() => {
vi.resetAllMocks();
vi.mocked(prisma.user.update).mockResolvedValue({} as never);
vi.mocked(prisma.auditLog.create).mockResolvedValue({} as never);
vi.mocked(prisma.onboardingRecord.findUnique).mockResolvedValue(null);
});
describe("POST /api/onboarding/complete", () => {