mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Pin the two writes this branch withdrew, instead of leaving their old proof
Taking `POST /api/allergies` and `POST /api/family-history` out of the delegable set moved one end and left the other: the integration suite still asserted that a delegate holding a WRITE grant gets a 201 from both, so ten cases went red on a decision that was deliberate. They are pinned the other way round now, with the reason in the file. A delegate is refused at either grant level, nothing lands under either account, and — the leg that matters most and that nothing else here covered — the owner's own unswitched write still succeeds. Withdrawing a delegated write by breaking the ordinary one would have been the worse bug.
This commit is contained in:
1 parent
582cd79aa1
commit
89c872a169
1 file changed
+69
-9
@@ -426,10 +426,76 @@ writeContract("POST /api/biomarkers", {
|
||||
});
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* 4 — allergies */
|
||||
/* 4 + 5 — allergies and family history, the two that are NOT delegable */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
writeContract("POST /api/allergies", {
|
||||
/**
|
||||
* The opposite contract, and it earns its place beside the admitted ones.
|
||||
*
|
||||
* Both of these verbs were admitted and then withdrawn, and the argument for
|
||||
* admitting them was never wrong: an allergy is a plain statement about the
|
||||
* record's own body and the single most useful thing a caregiver could
|
||||
* contribute. What they lack is a caller. The only surface in the product that
|
||||
* posts to either lives in Settings, and a switch closes `/settings` — so no
|
||||
* delegate can reach the form at any grant level, and admitting the write
|
||||
* would freeze a permission ahead of anything that exercises it.
|
||||
*
|
||||
* Both READ arms stay delegable; the read suite pins them. What is pinned here
|
||||
* is the pair that has to move together: the route refuses, and the owner's own
|
||||
* unswitched write still lands. Withdrawing a delegated write by breaking the
|
||||
* ordinary one would be the worse bug, and no other test in this file would
|
||||
* have noticed.
|
||||
*
|
||||
* Re-admitting means flipping `requireAuth()` back to `requireRecordAuth`,
|
||||
* re-listing the route in `delegable-surface-guard.test.ts`, and replacing this
|
||||
* block with a `writeContract` — in the same diff as the caregiver-reachable
|
||||
* surface that made it worth doing.
|
||||
*/
|
||||
function refusedWriteContract(
|
||||
name: string,
|
||||
c: {
|
||||
call: () => Promise<Response>;
|
||||
ok: number;
|
||||
count: (userId: string) => Promise<number>;
|
||||
},
|
||||
) {
|
||||
describe(name, () => {
|
||||
for (const access of ["READ", "WRITE"] as const) {
|
||||
it(`refuses a delegate holding a ${access} grant, and writes nothing`, async () => {
|
||||
const owner = await makeUser("owner");
|
||||
const delegate = await makeUser("delegate");
|
||||
|
||||
await switchInto(owner.id, delegate.id, access);
|
||||
const response = await c.call();
|
||||
|
||||
// The undeclared-mode refusal, not the no-grant one: the route names
|
||||
// no sharing mode at all, so the carrier is refused before any grant
|
||||
// is consulted. A WRITE grant makes no difference, which is the point.
|
||||
expect(response.status).toBe(403);
|
||||
expect((await payload(response)).meta?.errorCode).toBe(
|
||||
"sharing.not_permitted",
|
||||
);
|
||||
|
||||
// Neither account, not just not the owner's — a handler that fell
|
||||
// back to the caller would have written a row somewhere.
|
||||
expect(await c.count(owner.id)).toBe(0);
|
||||
expect(await c.count(delegate.id)).toBe(0);
|
||||
});
|
||||
}
|
||||
|
||||
it("still lets the owner write it themselves", async () => {
|
||||
const owner = await makeUser("owner");
|
||||
await signIn(owner.id);
|
||||
|
||||
const response = await c.call();
|
||||
|
||||
expect(response.status).toBe(c.ok);
|
||||
expect(await c.count(owner.id)).toBe(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refusedWriteContract("POST /api/allergies", {
|
||||
call: async () => {
|
||||
const { POST } = await import("@/app/api/allergies/route");
|
||||
return post(POST as Handler, "/api/allergies", {
|
||||
@@ -439,15 +505,10 @@ writeContract("POST /api/allergies", {
|
||||
});
|
||||
},
|
||||
ok: 201,
|
||||
auditAction: "allergy.create",
|
||||
count: (userId) => getPrismaClient().allergy.count({ where: { userId } }),
|
||||
});
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* 5 — family history */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
writeContract("POST /api/family-history", {
|
||||
refusedWriteContract("POST /api/family-history", {
|
||||
call: async () => {
|
||||
const { POST } = await import("@/app/api/family-history/route");
|
||||
return post(POST as Handler, "/api/family-history", {
|
||||
@@ -456,7 +517,6 @@ writeContract("POST /api/family-history", {
|
||||
});
|
||||
},
|
||||
ok: 201,
|
||||
auditAction: "family-history.create",
|
||||
count: (userId) =>
|
||||
getPrismaClient().familyHistoryEntry.count({ where: { userId } }),
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user