mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
The managed-record integration projection mapped the ledger's new `unknown` state to `disconnected`, but that mapping runs after the connection map has already established a real credential. A guardian who connected Withings on a child's record saw "Not connected" until the first hourly sync landed, beside a row whose own `connected` field said true — an invitation to revoke and redo the grant they had just made. `state` on this projection reports that a pipe exists, not that it is live; liveness is the sync verdict the card beside the pill already reads. With no ledger row and a credential present, the pipe exists.
216 lines
7.9 KiB
TypeScript
216 lines
7.9 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");
|
|
// v1.37.19 — "dashboard" left SETTINGS_SECTION_SLUGS (its URL is
|
|
// owned by a permanent redirect to /settings/layout/dashboard), so an
|
|
// unknown destination falls closed to plain unavailable; "layout" is
|
|
// the surviving adult-shared-unavailable representative.
|
|
expect(classifySettingsDestination("dashboard").kind).toBe("unavailable");
|
|
expect(classifySettingsDestination("layout").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("reports a real credential with no ledger row as connected", () => {
|
|
// The connection map is the credential: this branch is only reached once
|
|
// it says a pipe exists. `unknown` on top of it means the sync ledger
|
|
// holds no row yet — an hourly provider connected minutes ago — and this
|
|
// projection's `state` is documented as "a pipe exists", not as liveness.
|
|
// Calling it "Not connected" invites a guardian to revoke the grant they
|
|
// just made.
|
|
const connected = {
|
|
withings: true,
|
|
whoop: false,
|
|
fitbit: false,
|
|
nightscout: false,
|
|
polar: false,
|
|
oura: false,
|
|
"google-health": false,
|
|
strava: false,
|
|
} as const;
|
|
|
|
expect(
|
|
resolveManagedIntegrationState("unknown", connected, "withings"),
|
|
).toBe("connected");
|
|
// No credential still outranks it.
|
|
expect(resolveManagedIntegrationState("unknown", connected, "oura")).toBe(
|
|
"disconnected",
|
|
);
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|