mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
fix(meds): phase-config returns the 422 multi-issue envelope
The phase-config PUT was the last v1.5.5 route still returning a flat 400 on Zod failure. Returning the multi-issue envelope means iOS gets per-field feedback the same way the bulk-delete and intake routes do. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
56523bafb1
commit
e6907d4ea9
2 files changed
+106
-2
No files matched your search
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* v1.5.5 F-1 H-3 — phase-config PUT now returns the multi-issue 422
|
||||
* envelope rather than the flat `400 Invalid input`.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
prisma: {
|
||||
reminderPhaseConfig: { upsert: vi.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/medications/route-guards", () => ({
|
||||
assertMedicationOwnership: vi.fn().mockResolvedValue(null),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth/session", () => ({ getSession: 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 { PUT } from "../route";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
|
||||
|
||||
const SESSION_OK = {
|
||||
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
|
||||
user: { id: "user-1", username: "tester", role: "USER" as const },
|
||||
};
|
||||
|
||||
const ROUTE_PARAMS = { params: Promise.resolve({ id: "med-1" }) };
|
||||
|
||||
function putReq(body: unknown): NextRequest {
|
||||
return new NextRequest(
|
||||
"http://localhost/api/medications/med-1/phase-config",
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(assertMedicationOwnership).mockResolvedValue(null);
|
||||
vi.mocked(prisma.reminderPhaseConfig.upsert).mockResolvedValue({
|
||||
medicationId: "med-1",
|
||||
} as never);
|
||||
});
|
||||
|
||||
describe("PUT /api/medications/[id]/phase-config — F-1 H-3 multi-issue 422", () => {
|
||||
it("returns the multi-issue envelope on a malformed body", async () => {
|
||||
// Three issues: greenValue not int, greenMode not enum, redValue overflow.
|
||||
const res = await PUT(
|
||||
putReq({
|
||||
greenValue: "not-a-number",
|
||||
greenMode: "BOGUS",
|
||||
yellowValue: 30,
|
||||
yellowMode: "MINUTES",
|
||||
orangeValue: 0,
|
||||
orangeMode: "MINUTES",
|
||||
redValue: 10_000,
|
||||
redMode: "MINUTES",
|
||||
}),
|
||||
ROUTE_PARAMS,
|
||||
);
|
||||
expect(res.status).toBe(422);
|
||||
const body = (await res.json()) as {
|
||||
data: null;
|
||||
error: string;
|
||||
details: {
|
||||
issues: Array<{ path: string; code: string; message: string }>;
|
||||
};
|
||||
};
|
||||
expect(body.data).toBeNull();
|
||||
expect(body.error).toBe("Validation failed");
|
||||
expect(body.details.issues.length).toBeGreaterThanOrEqual(2);
|
||||
for (const issue of body.details.issues) {
|
||||
expect(Object.keys(issue).sort()).toEqual(["code", "message", "path"]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,11 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { apiSuccess, apiError, safeJson } from "@/lib/api-response";
|
||||
import {
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { phaseConfigSchema } from "@/lib/validations/phase-config";
|
||||
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
|
||||
import { NextRequest } from "next/server";
|
||||
@@ -59,7 +63,11 @@ export const PUT = apiHandler(
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = phaseConfigSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiError("Invalid input", 400);
|
||||
// v1.5.5 F-1 H-3 — every v1.5.5 route returns the multi-issue
|
||||
// 422 envelope so iOS sees per-field feedback the same way the
|
||||
// bulk-delete and intake routes already do. The old `400 Invalid
|
||||
// input` flat response was the last hold-out in the surface.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
|
||||
const config = await prisma.reminderPhaseConfig.upsert({
|
||||
|
||||
Reference in new issue
Block a user