Merge work/contract-touchups score-config registration

This commit is contained in:
Marc-André Bombeck committed 2026-08-01 11:58:17 +02:00
commit a830e8a9d3
3 files changed
+460

No files matched your search

@@ -63,6 +63,7 @@ import { auditLog } from "@/lib/auth/audit";
import { checkRateLimit } from "@/lib/rate-limit";
import { invalidateUserHealthScore } from "@/lib/cache/invalidate";
import type { ScorePillarId } from "@/lib/analytics/score/types";
import { buildOpenApiDocument } from "@/lib/openapi/registry";
const SESSION_OK = {
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
@@ -397,3 +398,145 @@ describe("PATCH — envelope and transport", () => {
expect(invalidateUserHealthScore).not.toHaveBeenCalled();
});
});
/**
* v1.35.1 — the route shipped in v1.35.0 with a settings surface in front
* of it and no published contract at all: `grep -c health-score-config
* docs/api/openapi.yaml` returned 0. This block is the second end.
*
* It does not compare the spec against a restatement of the spec. It drives
* the REAL route to produce each response, reads the codes and keys off the
* bytes it actually returns, and asserts the published contract names them.
* A response the route can emit but the contract does not describe fails
* here, which is the failure the release itself did not catch.
*/
describe("the published contract matches what the route serves", () => {
const PATH = "/api/auth/me/health-score-config";
function doc() {
return buildOpenApiDocument() as {
paths?: Record<
string,
Record<string, { responses?: Record<string, { description?: string }> }>
>;
components?: {
schemas?: Record<
string,
{ properties?: Record<string, unknown>; required?: string[] }
>;
};
};
}
/** Every `meta.errorCode` the PATCH route can actually produce. */
async function emittedErrorCodes(): Promise<Map<number, Set<string>>> {
const byStatus = new Map<number, Set<string>>();
const record = async (body: unknown, conflict = false) => {
vi.clearAllMocks();
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
primeUser(null);
vi.mocked(prisma.user.updateMany).mockResolvedValue({
count: conflict ? 0 : 1,
} as never);
const res = await (PATCH as (r: Request) => Promise<Response>)(
mkPatch(body),
);
const env = (await res.json()) as { meta?: { errorCode?: string } };
if (env.meta?.errorCode) {
const set = byStatus.get(res.status) ?? new Set<string>();
set.add(env.meta.errorCode);
byStatus.set(res.status, set);
}
};
// Too narrow, both reasons.
await record({ pillars: ["ACTIVITY"] });
await record({ pillars: ["BLOOD_PRESSURE", "GLYCAEMIA", "LIPIDS"] });
// Body that fails validation.
await record({ pillars: ["NOT_A_PILLAR"] });
// Present-but-unparseable base token.
await record({ pillars: ALL_EIGHT, baseUpdatedAt: null });
// Stale base token.
await record(
{ pillars: ALL_EIGHT, baseUpdatedAt: "2026-07-24T08:00:00.000Z" },
true,
);
return byStatus;
}
it("registers both verbs", () => {
const path = doc().paths?.[PATH];
expect(
path,
`${PATH} is not registered in the OpenAPI document`,
).toBeDefined();
expect(Object.keys(path ?? {}).sort()).toEqual(["get", "patch"]);
});
it("names every errorCode the route can emit, on the right status", async () => {
const emitted = await emittedErrorCodes();
const responses = doc().paths?.[PATH]?.patch?.responses ?? {};
// Sanity: the driver really did produce the codes, so a contract that
// named none of them could not pass by the loop never running.
expect([...(emitted.get(422) ?? [])].sort()).toEqual([
"health_score_config.invalid",
"health_score_config.too_narrow",
"invalid_base_updated_at",
]);
expect([...(emitted.get(409) ?? [])]).toEqual([
"health_score_config_conflict",
]);
for (const [status, codes] of emitted) {
const described = responses[String(status)]?.description ?? "";
for (const code of codes) {
expect(
described,
`${status} description does not mention \`${code}\``,
).toContain(code);
}
}
});
it("publishes exactly the fields a real response carries", async () => {
vi.clearAllMocks();
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
primeUser(null);
vi.mocked(prisma.user.updateMany).mockResolvedValue({ count: 1 } as never);
const res = await (PATCH as (r: Request) => Promise<Response>)(
mkPatch({ pillars: ALL_EIGHT }),
);
const env = (await res.json()) as { data: ResolvedBody };
expect(res.status).toBe(200);
const schema = doc().components?.schemas?.["HealthScoreConfigResponse"];
expect(schema, "HealthScoreConfigResponse is not emitted").toBeDefined();
expect(Object.keys(schema?.properties ?? {}).sort()).toEqual(
Object.keys(env.data).sort(),
);
// `updatedAt` is absent on a never-configured GET, so it must not be
// required — a generated decoder would reject that read.
expect(schema?.required ?? []).not.toContain("updatedAt");
});
it("leaves updatedAt off the read that genuinely has none", async () => {
vi.clearAllMocks();
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
primeUser(null);
const res = await (GET as (r: Request) => Promise<Response>)(
new Request(`http://localhost${PATH}`),
);
const env = (await res.json()) as { data: ResolvedBody };
const published = Object.keys(
doc().components?.schemas?.["HealthScoreConfigResponse"]?.properties ??
{},
);
expect(Object.keys(env.data)).not.toContain("updatedAt");
for (const key of Object.keys(env.data)) expect(published).toContain(key);
});
});