Files
HealthLog/src/__tests__/record-settings-contract.test.ts
T
Marc-André Bombeck 6ae0cc993f fix: close four small gaps around the record surface
The privacy dashboard re-implemented the audit-retention resolver with its own
default and its own floor. Two implementations of one disclosure, quoted to the
same person on two pages and by the purge job; it uses the shared resolver now.

The record-content branch in the settings gate matched a classification rather
than a page, so a second manage-writable destination would have rendered the
allergy and family-history managers under its own heading without anybody
choosing that. It resolves through a slug map, and the contract test fails if
the map and the classification disagree in either direction — a destination
cannot join the kind without somebody deciding what it renders.

The managed-profile nav filter assumed a guardian grant is always manage. It is
today, which is why the check was easy to leave out; an entry below that level
would have listed destinations the gate refuses.

Interpolation used a replacement string, and String.replace reads $&, $`, $',
$1 and $$ out of one. A display name containing $' rewrote the sentence around
it — and display names land in exactly the sentences that say who did what in
somebody's health record. A replacer function has no $ grammar at all, which
removes the class rather than escaping the members of it somebody thought of.
2026-08-07 01:00:02 +02:00

184 lines
6.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
assertRecordSettingsAccess,
type RecordSettingsAccess,
} from "@/lib/record-settings/access";
import {
classifySettingsDestination,
isGuardianSettingsWriteAllowed,
isManageDelegateSettingsDestination,
SETTINGS_DESTINATION_INVENTORY,
} from "@/lib/record-settings/classification";
import { RECORD_CONTENT_SECTIONS } from "@/components/settings/record-settings-section-gate";
import { toRecordSettingsDto } from "@/lib/record-settings/dto";
import { resolveManagedIntegrationState } from "@/lib/record-settings/integrations";
import { assertRecordSettingsResponseForRecord } from "@/lib/record-settings/response";
import { recordSettingsKeys } from "@/lib/query-keys";
describe("record settings contract", () => {
it("names the target record in both the DTO and cache key", () => {
const dto = toRecordSettingsDto({
id: "record-1",
name: "Managed profile",
locale: "de",
timezone: "Europe/Berlin",
recordKind: "managed",
});
expect(dto).toEqual({
record: {
id: "record-1",
displayName: "Managed profile",
locale: "de",
timezone: "Europe/Berlin",
kind: "managed",
},
});
expect(recordSettingsKeys.detail("record-1")).toEqual([
"record-settings",
"record-1",
"detail",
]);
expect(recordSettingsKeys.recordSettingsIntegrations("record-1")).toEqual([
"record-settings",
"record-1",
"integrations",
]);
});
it("does not let an adult MANAGE grant use Guardian-only configuration", () => {
const access: RecordSettingsAccess = {
actorId: "adult-manager",
recordId: "managed-record",
recordKind: "managed",
relationship: "adult-manager",
};
expect(() => assertRecordSettingsAccess(access, "guardian")).toThrow(
"Guardian configuration is unavailable for this record",
);
});
it("classifies every Settings destination and limits Guardian writes", () => {
expect(Object.keys(SETTINGS_DESTINATION_INVENTORY)).not.toHaveLength(0);
expect(classifySettingsDestination("integrations")).toMatchObject({
kind: "managed-guardian",
guardianWritable: false,
});
expect(classifySettingsDestination("ai").kind).toBe("unavailable");
expect(classifySettingsDestination("dashboard").kind).toBe(
"adult-shared-unavailable",
);
expect(isGuardianSettingsWriteAllowed("account")).toBe(true);
expect(isGuardianSettingsWriteAllowed("notifications")).toBe(true);
expect(isGuardianSettingsWriteAllowed("integrations")).toBe(false);
expect(isGuardianSettingsWriteAllowed("ai")).toBe(false);
});
/**
* v1.37.0 — the anamnesis destination, which is the one piece of record
* CONTENT under `/settings`.
*
* `POST /api/allergies` and `POST /api/family-history` have resolved
* `requireRecordAuth("manage", "profile")` since MANAGE shipped, while the
* only form that posts to either was classified unavailable — an admitted
* write with no reachable caller. These three legs are the classification
* half of closing that; the integration file drives the routes themselves.
*/
it("opens the anamnesis destination to everybody the routes already admit", () => {
expect(classifySettingsDestination("anamnesis")).toEqual({
kind: "manage-writable",
guardianWritable: true,
});
// The Guardian half. Before this the destination existed and was
// read-only for the one person the profile exists for.
expect(isGuardianSettingsWriteAllowed("anamnesis")).toBe(true);
// The adult-delegate half, and its boundary in the same breath: a MANAGE
// grant opens the record's health background and nothing else under
// Settings. Record CONFIGURATION — modules, thresholds, notification
// routing — stays with the owner even though a Guardian may change it.
expect(isManageDelegateSettingsDestination("anamnesis")).toBe(true);
for (const guardianOnly of [
"account",
"modules",
"notifications",
"thresholds",
"insights",
"coach",
"integrations",
]) {
expect(
isManageDelegateSettingsDestination(guardianOnly),
guardianOnly,
).toBe(false);
}
});
it("gives every record-content destination a page of its own", () => {
// Both directions, and the first is the one that matters. `manage-writable`
// is a classification; the gate has to know which PAGE each slug is, or a
// destination added to the kind would silently inherit the allergy and
// family-history managers under its own heading. The map is what says
// which, and this is what stops the two drifting apart — a new
// manage-writable slug fails here, at the moment it is added, rather than
// rendering somebody else's forms.
const classified = Object.keys(SETTINGS_DESTINATION_INVENTORY)
.filter(isManageDelegateSettingsDestination)
.sort();
const rendered = Object.keys(RECORD_CONTENT_SECTIONS).sort();
expect(classified).not.toHaveLength(0);
expect(rendered).toEqual(classified);
});
it("leaves exactly one destination on the manage-writable list", () => {
// The list is short on purpose and a second entry is a consent decision,
// not a classification tidy-up. Pinning the count means a destination
// cannot join it as a side effect of somebody reclassifying a neighbour.
const manageWritable = Object.entries(SETTINGS_DESTINATION_INVENTORY)
.filter(([, value]) => value.kind === "manage-writable")
.map(([slug]) => slug);
expect(manageWritable).toEqual(["anamnesis"]);
});
it("does not report a synthetic ledger row as a managed connection", () => {
const connected = {
withings: false,
whoop: false,
fitbit: false,
nightscout: false,
polar: false,
oura: false,
"google-health": false,
strava: false,
} as const;
expect(
resolveManagedIntegrationState("connected", connected, "withings"),
).toBe("disconnected");
expect(
resolveManagedIntegrationState(
"error_reauth",
{ ...connected, oura: true },
"oura",
),
).toBe("error_reauth");
});
it("rejects a late response for another record in a second tab", () => {
expect(() =>
assertRecordSettingsResponseForRecord(
{ recordId: "managed-record-b" },
"managed-record-a",
),
).toThrow("did not match its key");
expect(() =>
assertRecordSettingsResponseForRecord(
{ recordId: "managed-record-a" },
"managed-record-a",
),
).not.toThrow();
});
});