mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
release: v1.32.0 — hardening and scalability (#580)
* release: prepare v1.32.0 * test: stabilize water capture re-entry
This commit is contained in:
1 parent
b01822be48
commit
62eb431d66
372 files changed
+25178
-8561
No files matched your search
@@ -0,0 +1,74 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
prisma: { user: { update: vi.fn() } },
|
||||
}));
|
||||
vi.mock("@/lib/auth/session", () => ({
|
||||
getSession: vi.fn(),
|
||||
setOnboardingPendingCookie: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() }));
|
||||
vi.mock("@/lib/db-compat", () => ({
|
||||
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
vi.mock("next/headers", () => ({
|
||||
headers: vi.fn(async () => ({ get: () => null })),
|
||||
cookies: vi.fn(async () => ({
|
||||
get: () => undefined,
|
||||
set: () => {},
|
||||
delete: () => {},
|
||||
})),
|
||||
}));
|
||||
|
||||
import { POST as disclaimerPost } from "../disclaimer/route";
|
||||
import { POST as tourPost } from "../tour/route";
|
||||
import { POST as completePost } from "../complete/route";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
|
||||
const SESSION = {
|
||||
session: { id: "s1", expiresAt: new Date(Date.now() + 60_000) },
|
||||
user: {
|
||||
id: "u1",
|
||||
username: "user",
|
||||
role: "USER" as const,
|
||||
onboardingTourCompleted: false,
|
||||
},
|
||||
};
|
||||
|
||||
function req(path: string, body: unknown) {
|
||||
return new NextRequest(`http://localhost/api/onboarding/${path}`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION as never);
|
||||
});
|
||||
|
||||
describe("onboarding validation error contract", () => {
|
||||
it("pins disclaimer validation", async () => {
|
||||
const res = await disclaimerPost(req("disclaimer", {}));
|
||||
expect(res.status).toBe(422);
|
||||
expect((await res.json()).meta?.errorCode).toBe(
|
||||
"onboarding.disclaimer.invalid",
|
||||
);
|
||||
});
|
||||
|
||||
it("pins tour validation", async () => {
|
||||
const res = await tourPost(req("tour", {}));
|
||||
expect(res.status).toBe(422);
|
||||
expect((await res.json()).meta?.errorCode).toBe("onboarding.tour.invalid");
|
||||
});
|
||||
|
||||
it("pins legacy completion validation", async () => {
|
||||
const res = await completePost(req("complete", { heightCm: 20 }));
|
||||
expect(res.status).toBe(422);
|
||||
expect((await res.json()).meta?.errorCode).toBe(
|
||||
"onboarding.complete.invalid",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -28,7 +28,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
if (jsonError) return jsonError;
|
||||
const result = z.safeParse(onboardingSchema, body);
|
||||
if (!result.success) {
|
||||
return apiError("Invalid input", 422);
|
||||
return apiError("Invalid input", 422, {
|
||||
errorCode: "onboarding.complete.invalid",
|
||||
});
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = {
|
||||
|
||||
@@ -36,7 +36,9 @@ export const POST = apiHandler(async (request) => {
|
||||
|
||||
const parsed = z.safeParse(bodySchema, rawBody);
|
||||
if (!parsed.success) {
|
||||
return apiError("Invalid input", 422);
|
||||
return apiError("Invalid input", 422, {
|
||||
errorCode: "onboarding.disclaimer.invalid",
|
||||
});
|
||||
}
|
||||
|
||||
// The server pins the canonical version it stamps — the body version is
|
||||
|
||||
@@ -73,7 +73,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
action: { name: "onboarding.step" },
|
||||
meta: { outcome: "rate_limited" },
|
||||
});
|
||||
return apiError("Too many onboarding writes, try again later", 429);
|
||||
return apiError("Too many onboarding writes, try again later", 429, {
|
||||
errorCode: "onboarding.step.rateLimited",
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson<unknown>(request, {
|
||||
@@ -87,7 +89,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
action: { name: "onboarding.step" },
|
||||
meta: { outcome: "validation_failed" },
|
||||
});
|
||||
return apiError("Invalid step payload", 422);
|
||||
return apiError("Invalid step payload", 422, {
|
||||
errorCode: "onboarding.step.invalid",
|
||||
});
|
||||
}
|
||||
const { step, goals } = parsed.data;
|
||||
|
||||
@@ -102,7 +106,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
},
|
||||
});
|
||||
if (!fresh) {
|
||||
return apiError("User not found", 404);
|
||||
return apiError("User not found", 404, {
|
||||
errorCode: "onboarding.user.notFound",
|
||||
});
|
||||
}
|
||||
|
||||
if (fresh.onboardingCompletedAt) {
|
||||
@@ -110,7 +116,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
action: { name: "onboarding.step" },
|
||||
meta: { outcome: "already_completed" },
|
||||
});
|
||||
return apiError("Onboarding already completed", 409);
|
||||
return apiError("Onboarding already completed", 409, {
|
||||
errorCode: "onboarding.step.completed",
|
||||
});
|
||||
}
|
||||
|
||||
const current = fresh.onboardingStep;
|
||||
@@ -119,7 +127,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
action: { name: "onboarding.step" },
|
||||
meta: { outcome: "out_of_order", current, requested: step },
|
||||
});
|
||||
return apiError(`Step out of order — current step is ${current}`, 409);
|
||||
return apiError(`Step out of order — current step is ${current}`, 409, {
|
||||
errorCode: "onboarding.step.outOfOrder",
|
||||
});
|
||||
}
|
||||
|
||||
const completing = step === 4;
|
||||
@@ -159,6 +169,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
return apiError(
|
||||
"Onboarding step changed concurrently, refresh and retry",
|
||||
409,
|
||||
{ errorCode: "onboarding.step.concurrent" },
|
||||
);
|
||||
}
|
||||
const updated = await prisma.user.findUniqueOrThrow({
|
||||
|
||||
@@ -55,7 +55,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const parsed = z.safeParse(tourBodySchema, body);
|
||||
if (!parsed.success) {
|
||||
return apiError("Invalid input", 422);
|
||||
return apiError("Invalid input", 422, {
|
||||
errorCode: "onboarding.tour.invalid",
|
||||
});
|
||||
}
|
||||
|
||||
// Build the update field-by-field (no mass assignment). `completed`
|
||||
|
||||
Reference in new issue
Block a user