fix(meds): phase-config upsert builds the Prisma payload field-by-field

Drop the parsed.data spread so a future schema extension cannot
silently land on the Prisma write.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-29 00:53:23 +02:00
1 parent e6907d4ea9
commit ff334ff10e
2 files changed
+88 -5

No files matched your search

@@ -94,3 +94,64 @@ describe("PUT /api/medications/[id]/phase-config — F-1 H-3 multi-issue 422", (
}
});
});
describe("PUT /api/medications/[id]/phase-config — F-1 H-4 field-by-field upsert", () => {
it("builds the create + update payloads field-by-field rather than spreading parsed.data", async () => {
const body = {
greenValue: 60,
greenMode: "MINUTES",
yellowValue: 30,
yellowMode: "MINUTES",
orangeValue: 0,
orangeMode: "MINUTES",
redValue: 240,
redMode: "MINUTES",
};
await PUT(putReq(body), ROUTE_PARAMS);
const callArg = vi.mocked(prisma.reminderPhaseConfig.upsert).mock.calls[0][0];
// create payload contains exactly the eight enumerated phase fields
// plus the `medicationId` route binding — nothing else.
expect(Object.keys(callArg.create).sort()).toEqual([
"greenMode",
"greenValue",
"medicationId",
"orangeMode",
"orangeValue",
"redMode",
"redValue",
"yellowMode",
"yellowValue",
]);
// update payload mirrors the eight phase fields (no `medicationId`).
expect(Object.keys(callArg.update).sort()).toEqual([
"greenMode",
"greenValue",
"orangeMode",
"orangeValue",
"redMode",
"redValue",
"yellowMode",
"yellowValue",
]);
});
it("ignores an unknown field smuggled in the body (no mass assignment)", async () => {
const body = {
greenValue: 60,
greenMode: "MINUTES",
yellowValue: 30,
yellowMode: "MINUTES",
orangeValue: 0,
orangeMode: "MINUTES",
redValue: 240,
redMode: "MINUTES",
// Unknown rogue field — should never reach the Prisma upsert.
maliciousField: "owned",
};
await PUT(putReq(body), ROUTE_PARAMS);
const callArg = vi.mocked(prisma.reminderPhaseConfig.upsert).mock.calls[0][0];
expect((callArg.create as Record<string, unknown>).maliciousField).toBeUndefined();
expect((callArg.update as Record<string, unknown>).maliciousField).toBeUndefined();
});
});
@@ -70,13 +70,35 @@ export const PUT = apiHandler(
return returnAllZodIssues(parsed.error, 422);
}
// v1.5.5 F-1 H-4 — build the Prisma payload field-by-field rather
// than spreading `parsed.data`. A future schema extension would
// otherwise land on the upsert silently; the explicit pick keeps
// the surface honest at the call site.
const {
greenValue,
greenMode,
yellowValue,
yellowMode,
orangeValue,
orangeMode,
redValue,
redMode,
} = parsed.data;
const phaseFields = {
greenValue,
greenMode,
yellowValue,
yellowMode,
orangeValue,
orangeMode,
redValue,
redMode,
};
const config = await prisma.reminderPhaseConfig.upsert({
where: { medicationId: id },
create: {
medicationId: id,
...parsed.data,
},
update: parsed.data,
create: { medicationId: id, ...phaseFields },
update: phaseFields,
});
annotate({