test(sharing): point the refusal arm at a route that can never be delegable

`GET /api/measurements` was the resolver test's example of a route refusing
under a switch. It is delegable now, so that arm was asserting the opposite
of what ships and went red on the migration — which is the arm doing its job,
not a defect in it.

Split in two. The measurements route keeps both of its real claims: unchanged
un-switched, and the owner's rows and not the delegate's once switched. The
refusal moves to `GET /api/share-links`, which is non-delegable permanently
and by design — a delegate who could mint a clinician link would hold a door
that survives the owner revoking their access. A route that can never join
the frozen list cannot break this arm the same way twice.
This commit is contained in:
Marc-André Bombeck committed 2026-08-02 17:36:44 +02:00
1 parent 7a39a649be
commit 81e5c7464b
1 file changed
+41 -13
@@ -15,15 +15,24 @@
* into the second, and the owner's revoke button would mean "at some point
* later" instead of "now".
*
* The do-no-harm arm runs against a REAL shipped route (`GET /api/measurements`,
* a bare-`requireAuth` handler nobody has migrated). A mock cannot fake that,
* and the arm is worth nothing unless the route it exercises is one somebody
* actually ships.
* Both arms run against REAL shipped routes, and since the route migration they
* exercise both sides of the boundary rather than one:
*
* The delegable handler is assembled here rather than imported because no route
* declares `requireRecordAuth` yet — S3 lands the frozen list empty on purpose,
* and S6 migrates the first route into it. Everything under it is real: the
* real `apiHandler`, the real resolver, the real grant module, the real
* * `GET /api/measurements` is delegable now. Un-switched it must answer
* exactly what it answered before the resolver existed, which is the
* do-no-harm claim and the one worth the most.
* * `GET /api/share-links` is refused, permanently and by design — a delegate
* must never mint a door that outlives their own revocation. It is what a
* route that never declared a mode does under a switch. Naming a
* permanently-refused route rather than a merely-unmigrated one is
* deliberate: this arm broke once already when the route it pointed at
* became delegable, and a route that can never join the list cannot break
* it the same way twice.
*
* The delegable handler below is still assembled here rather than imported. A
* shipped route would work, but it would bind this file to one domain's query
* shape, and what is under test is the resolver. Everything beneath it is real:
* the real `apiHandler`, the real resolver, the real grant module, the real
* database.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -276,7 +285,7 @@ describe("acting-account resolver — the decision is never cached", () => {
});
describe("acting-account resolver — do no harm", () => {
it("leaves an un-switched request on a real shipped route exactly as it was", async () => {
it("leaves an un-switched request on a real delegable route exactly as it was", async () => {
const user = await makeUser("solo");
await seedWeight(user.id, 72);
await signIn(user.id);
@@ -292,7 +301,7 @@ describe("acting-account resolver — do no harm", () => {
).toEqual([72]);
});
it("refuses that same route while a switch is active, and serves nobody's rows", async () => {
it("serves the owner's rows on that route once the caller has switched", async () => {
const { owner, delegate } = await household();
const session = await signIn(delegate.id);
await switchTo(session.id, owner.id);
@@ -301,13 +310,32 @@ describe("acting-account resolver — do no harm", () => {
const response = await GET(
new NextRequest("http://localhost/api/measurements?type=WEIGHT"),
);
expect(response.status).toBe(200);
const body = await response.json();
// The owner's 81 kg, and — the failure that matters — not the delegate's
// own 64 kg, which is seeded alongside it.
expect(
body.data.measurements.map((m: { value: number }) => m.value),
).toEqual([81]);
});
it("refuses a route that never declared a mode, and serves nobody's rows", async () => {
// `GET /api/share-links` is permanently non-delegable: a delegate who
// could mint a clinician link would hold a door that survives the owner
// revoking their access. It resolves through the bare `requireAuth`
// default arm, which is what makes the refusal structural rather than a
// check somebody remembered to write.
const { owner, delegate } = await household();
const session = await signIn(delegate.id);
await switchTo(session.id, owner.id);
const { GET } = await import("@/app/api/share-links/route");
const response = await GET();
expect(response.status).toBe(403);
const body = await response.json();
expect(body.meta.errorCode).toBe("sharing.not_permitted");
expect(body.data).toBeNull();
// Not the owner's rows, and — the failure that matters — not the
// delegate's own either.
expect(JSON.stringify(body)).not.toContain("64");
expect(owner.id).not.toBe(delegate.id);
});
});