mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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:
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({
|
||||
|
||||
Reference in new issue
Block a user