Files
HealthLog/src/__tests__/managed-profile-contract.test.ts
T
Marc-André Bombeck 4d86dceb99 docs: publish the last-Guardian refusal where it is actually raised
The floor was described on the one route that cannot reach it and left unsaid
on the two that answer it every time: withdrawing the last Guardian grant and
renouncing it both raise `managed_profile.guardian.required`, and both
published only 'that access had already ended'. Both 409s now name their two
codes, and the annotation on the removal route enumerates all four emitters
rather than three.

A guard closes the class rather than the instance: every route module that
raises the refusal must publish it on its own operation, with the emitters
discovered from the sources.

The freeze guard gains its containment leg — a module becomes delegable by
review, not by discovery, so every file contributing a fenced verb has to be a
member of one of the surface guard's frozen allowlists.
2026-08-07 05:43:06 +02:00

160 lines
5.9 KiB
TypeScript

import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { openApiPaths } from "@/lib/openapi/routes";
const root = process.cwd();
/** Every `route.ts` under the API tree. */
async function apiRouteFiles(): Promise<string[]> {
const entries = await readdir(path.join(root, "src/app/api"), {
recursive: true,
});
return entries
.filter((entry) => entry.endsWith("route.ts"))
.map((entry) => path.join("src/app/api", entry));
}
/** The published path a route file serves, or null when it is unpublished. */
function publishedPathFor(file: string): string | null {
const apiPath = file
.replace(/^src\/app/, "")
.replace(/\/route\.ts$/, "")
.replace(/\[([^\]]+)\]/g, "{$1}");
return apiPath in openApiPaths ? apiPath : null;
}
describe("managed-profile contract", () => {
it("publishes the last-Guardian refusal on every published route that emits it", async () => {
// The failure this catches is the one the release audit found: the floor
// is enforced on four surfaces and was DESCRIBED on the one route that
// cannot reach it, while two routes that answer it every time said only
// "that access had already ended". Emitters are discovered from the route
// sources, so a fifth one cannot ship undescribed.
const files = await apiRouteFiles();
expect(files.length).toBeGreaterThan(100);
const emitters: string[] = [];
for (const file of files) {
const source = await readFile(path.join(root, file), "utf8");
if (!source.includes("LastManagedGuardianError")) continue;
if (!source.includes("managed_profile.guardian.required")) continue;
emitters.push(file);
}
// Non-zero, and known: the two grant routes. The lifecycle surfaces that
// also raise it (account deletion, the data wipe) answer through their own
// envelopes and are not in this spec.
expect(emitters).toContain("src/app/api/account/grants/[id]/route.ts");
expect(emitters).toContain(
"src/app/api/account/grants/[id]/renounce/route.ts",
);
const silent = emitters.filter((file) => {
const apiPath = publishedPathFor(file);
if (apiPath === null) return false;
return !JSON.stringify(openApiPaths[apiPath]).includes(
"managed_profile.guardian.required",
);
});
expect(
silent,
"published routes that emit the last-Guardian refusal without publishing it",
).toEqual([]);
});
it("requires a cookie-backed fresh MFA proof for creation", async () => {
const source = await readFile(
path.join(root, "src/app/api/managed-profiles/route.ts"),
"utf8",
);
expect(source).toContain("requireFreshMfa");
expect(source).not.toContain("requireFreshMfaIfEnrolled");
});
it("requires a cookie-backed fresh MFA proof for profile deletion", async () => {
const source = await readFile(
path.join(root, "src/app/api/managed-profiles/[id]/route.ts"),
"utf8",
);
expect(source).toContain("requireFreshMfa");
expect(source).not.toContain("requireFreshMfaIfEnrolled");
});
it("publishes no Guardian refusal a request cannot produce", async () => {
// `managed_profile.guardian.self` was written, shipped and never
// reachable: `self_grant` needs the grantor to equal the grantee, and the
// grantor of a Guardian grant is the PROFILE. Inviting yourself lands on
// the duplicate refusal; naming the profile is refused a step earlier as
// `managed_grantee`. Both doors are asserted in
// `tests/integration/managed-profile-surface.test.ts`, so the arm was
// known to be dead rather than assumed to be, and it was removed rather
// than frozen into the published contract.
const route = await readFile(
path.join(root, "src/app/api/managed-profiles/[id]/guardians/route.ts"),
"utf8",
);
const spec = await readFile(
path.join(root, "docs/api/openapi.yaml"),
"utf8",
);
// Non-zero control: the codes that ARE reachable are still named, so this
// cannot pass by matching an empty file or a renamed family.
expect(route).toContain("managed_profile.guardian.duplicate");
expect(route).toContain("managed_profile.guardian.managed_invitee");
expect(route).not.toContain('errorCode: "managed_profile.guardian.self"');
expect(spec).not.toContain("managed_profile.guardian.self");
});
it("does not publish an emancipation API surface", async () => {
const source = await readFile(
path.join(root, "prisma/schema.prisma"),
"utf8",
);
expect(source.toLowerCase()).not.toContain("emancipation");
const routes = await readdir(
path.join(root, "src/app/api/managed-profiles"),
{ recursive: true },
);
expect(
routes.some((entry) => entry.toLowerCase().includes("emancip")),
).toBe(false);
});
it("does not claim notification fanout before its consent plan exists", async () => {
const source = await readFile(
path.join(root, "prisma/schema.prisma"),
"utf8",
);
expect(source).not.toContain("notification dispatcher fans reminders");
expect(source).toContain("later notification plan");
});
it("uses the same record lock for every Guardian-reducing transition", async () => {
const lifecycle = await readFile(
path.join(root, "src/lib/managed-profiles/lifecycle.ts"),
"utf8",
);
const grants = await readFile(
path.join(root, "src/lib/sharing/grants.ts"),
"utf8",
);
const accountDeletion = await readFile(
path.join(root, "src/app/api/settings/account/route.ts"),
"utf8",
);
expect(lifecycle).toContain("pg_advisory_xact_lock");
expect(lifecycle).toContain("clearManagedProfileMarker");
expect(lifecycle).toContain("deleteManagedProfile");
expect(grants).toContain("reduceManagedProfileGuardian");
expect(accountDeletion).toContain("deleteGuardianAccountWithLifecycle");
});
});