mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(api): accept an Idempotency-Key on the titration step and the custom symptom
Two more creates the client replays out of its offline outbox ran the handler again on the replay. A titration step carries no natural key at all, so a lost success response left the medication with two dose changes on the same day and the runway estimate reading off the wrong one. A custom cycle symptom is unique on its label, so the replay did not duplicate — it answered the 409 that uniqueness raises, reporting a conflict the client cannot act on for a row it has no record of creating. Both wrap in `withIdempotency`, the composition their siblings already ship, and both operations publish the header and the in-flight 409 so a client generated against the contract can tell a retry is safe. The integration file that drives the medication creates twice against real Postgres gains the titration case: one dose-change row after the replay, and a replayed 201 carrying `X-Idempotent-Replay: true`. `POST /api/mcp/tokens` is deliberately not on this list. Its response carries a freshly minted `hlk_` credential, and the wrapper refuses to persist a secret-shaped body — publishing the header there would promise a replay the mechanism will never perform.
This commit is contained in:
1 parent
3e44d68518
commit
040b430e73
6 files changed
+211
-140
No files matched your search
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* The three medication create routes the native client replays out of its
|
||||
* The four medication create routes the native client replays out of its
|
||||
* offline outbox, driven as routes against a real Postgres.
|
||||
*
|
||||
* Each case imports the shipped `POST` export and calls it twice with the same
|
||||
@@ -10,8 +10,8 @@
|
||||
* keep passing after a route dropped it.
|
||||
*
|
||||
* The client sends the header on every replay, so a lost success response used
|
||||
* to cost the user a duplicate medication, a duplicate symptom entry, or a
|
||||
* duplicate pen in the supply count. `POST /api/medications` had a dedupe of
|
||||
* to cost the user a duplicate medication, a duplicate symptom entry, a
|
||||
* duplicate pen in the supply count, or a second titration step. `POST /api/medications` had a dedupe of
|
||||
* its own, but only for a MIRRORED create keyed on
|
||||
* `(externalSource, externalId)`; a manually entered medication carries
|
||||
* neither field and had nothing to collapse a retry onto — so the manual shape
|
||||
@@ -228,3 +228,41 @@ describe("POST /api/medications/[id]/inventory — replayed container", () => {
|
||||
expect(rows).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/medications/[id]/glp1 — replayed titration step", () => {
|
||||
it("replays the first response and writes exactly one dose change", async () => {
|
||||
const medicationId = await seedMedication();
|
||||
const { POST } = await import("@/app/api/medications/[id]/glp1/route");
|
||||
const body = {
|
||||
doseChange: {
|
||||
effectiveFrom: "2026-09-01T00:00:00Z",
|
||||
doseValue: 2.5,
|
||||
doseUnit: "mg",
|
||||
note: "stepped up after four weeks",
|
||||
},
|
||||
};
|
||||
|
||||
const first = await (POST as Handler)(
|
||||
post(`/api/medications/${medicationId}/glp1`, body, "outbox-glp1-1"),
|
||||
{ params: Promise.resolve({ id: medicationId }) },
|
||||
);
|
||||
expect(first.status).toBe(201);
|
||||
const firstBody = await first.json();
|
||||
|
||||
const replay = await (POST as Handler)(
|
||||
post(`/api/medications/${medicationId}/glp1`, body, "outbox-glp1-1"),
|
||||
{ params: Promise.resolve({ id: medicationId }) },
|
||||
);
|
||||
expect(replay.status).toBe(201);
|
||||
expect(replay.headers.get("X-Idempotent-Replay")).toBe("true");
|
||||
expect(await replay.json()).toEqual(firstBody);
|
||||
|
||||
// The titration row carries no natural key at all, so without the wrapper
|
||||
// the replay would leave the medication with two dose changes on the same
|
||||
// day and the runway estimate reading off the wrong one.
|
||||
const rows = await getPrismaClient().medicationDoseChange.findMany({
|
||||
where: { medicationId },
|
||||
});
|
||||
expect(rows).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user