Merge managed record settings

This commit is contained in:
Marc-André Bombeck committed 2026-08-06 04:05:16 +02:00
commit 201a24e404
36 files changed
+3712 -14

No files matched your search

@@ -0,0 +1,127 @@
import { describe, expect, it } from "vitest";
import {
isManagedRecordSettingsFamily,
MANAGED_RECORD_SETTINGS_FIELD_ALLOWLIST,
MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS,
managedModulePreferencesFrom,
parseManagedRecordSettingsPatch,
} from "@/lib/record-settings/configuration";
import { WRITABLE_MODULE_KEYS } from "@/lib/validations/modules";
describe("managed record settings configuration contract", () => {
it("enumerates the only target-record fields each settings DTO may write", () => {
expect(MANAGED_RECORD_SETTINGS_FIELD_ALLOWLIST).toEqual({
profile: [
"displayName",
"heightCm",
"dateOfBirth",
"gender",
"locale",
"timezone",
"unitPreference",
"timeFormat",
"dateFormat",
],
modules: ["modulePreferences"],
notifications: ["moodReminderEnabled", "notificationPreferences"],
thresholds: ["overrides"],
coach: ["disableCoach", "preferences"],
insights: ["layout"],
});
});
it.each([
["profile", { email: "not-allowed@example.test" }],
["modules", { role: "ADMIN" }],
[
"notifications",
{
notificationPreferences: { medication: { deliveryDefault: "client" } },
},
],
["thresholds", { glucoseUnit: "imperial" }],
["coach", { memories: [] }],
["insights", { provider: "external" }],
] as const)("rejects a disallowed %s field", (family, patch) => {
expect(() => parseManagedRecordSettingsPatch(family, patch)).toThrow();
});
it.each([
[
"profile",
{
displayName: "Managed profile",
heightCm: 120,
dateOfBirth: "2016-04-03",
gender: "OTHER",
locale: "en",
timezone: "Europe/Berlin",
unitPreference: "metric",
timeFormat: "H24",
dateFormat: "DMY",
},
],
["modules", { modulePreferences: { mood: false } }],
[
"notifications",
{
moodReminderEnabled: true,
notificationPreferences: {
medication: { lowStockRunwayDays: 7, reorderLeadDays: 10 },
mood: { reminderHour: 22 },
},
},
],
["thresholds", { overrides: { WEIGHT: { min: 55, max: 80 } } }],
[
"coach",
{
disableCoach: false,
preferences: {
tone: "warm",
verbosity: "default",
excludeMetrics: [],
showEvidenceByDefault: false,
defaultWindow: "allTime",
},
},
],
[
"insights",
{
layout: {
version: 2,
tiles: [{ id: "weight", visible: true, order: 0 }],
},
},
],
] as const)("accepts a typed %s patch", (family, patch) => {
expect(() => parseManagedRecordSettingsPatch(family, patch)).not.toThrow();
});
it("uses the complete canonical module inventory for a fresh record", () => {
expect(Object.keys(MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS)).toEqual(
WRITABLE_MODULE_KEYS,
);
expect(managedModulePreferencesFrom(null)).toEqual(
MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS,
);
});
it("accepts only the canonical profile gender enum", () => {
expect(() =>
parseManagedRecordSettingsPatch("profile", { gender: "MALE" }),
).not.toThrow();
expect(() =>
parseManagedRecordSettingsPatch("profile", { gender: "UNKNOWN" }),
).toThrow();
});
it.each(["toString", "constructor", "__proto__", "hasOwnProperty"])(
"rejects inherited family name %s",
(family) => {
expect(isManagedRecordSettingsFamily(family)).toBe(false);
},
);
});
@@ -0,0 +1,115 @@
import { describe, expect, it } from "vitest";
import {
assertRecordSettingsAccess,
type RecordSettingsAccess,
} from "@/lib/record-settings/access";
import {
classifySettingsDestination,
isGuardianSettingsWriteAllowed,
SETTINGS_DESTINATION_INVENTORY,
} from "@/lib/record-settings/classification";
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);
});
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();
});
});
@@ -0,0 +1,66 @@
import { readdir, readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { MANAGED_RECORD_SETTINGS_FIELD_ALLOWLIST } from "@/lib/record-settings";
const fromRoot = (...segments: string[]) => resolve(process.cwd(), ...segments);
const RECORD_SETTINGS_ROUTE_ROOT = fromRoot("src/app/api/record-settings");
describe("record settings route guard", () => {
it("freezes the non-empty guardian-only route inventory", async () => {
const discovered = (
await readdir(RECORD_SETTINGS_ROUTE_ROOT, {
recursive: true,
})
)
.filter((entry) => entry.endsWith("route.ts"))
.map((entry) => `app/api/record-settings/${entry}`)
.sort();
expect(discovered).toHaveLength(3);
expect(discovered).toEqual([
"app/api/record-settings/[family]/route.ts",
"app/api/record-settings/integrations/route.ts",
"app/api/record-settings/route.ts",
]);
});
it("resolves every record-settings route through the Guardian fence", async () => {
const routes = [
"route.ts",
"integrations/route.ts",
"[family]/route.ts",
] as const;
const sources = await Promise.all(
routes.map((route) =>
readFile(resolve(RECORD_SETTINGS_ROUTE_ROOT, route), "utf8"),
),
);
for (const source of sources) {
expect(source).toContain("requireGuardianAuth");
expect(source).toContain("resolveGuardianRecordSettingsAccess");
}
});
it("makes the field-specific route exhaustive over the approved DTO families", async () => {
const source = await readFile(
resolve(RECORD_SETTINGS_ROUTE_ROOT, "[family]/route.ts"),
"utf8",
);
const families = Object.keys(MANAGED_RECORD_SETTINGS_FIELD_ALLOWLIST);
expect(families.length).toBeGreaterThan(0);
for (const family of families) {
expect(source).toContain(`case \"${family}\"`);
}
expect(source).toContain("if (!isManagedRecordSettingsFamily(rawFamily))");
expect(source).toContain('apiError("Settings family not found", 404)');
expect(
source.indexOf("isManagedRecordSettingsFamily(rawFamily)"),
).toBeLessThan(source.indexOf("const context = await requireGuardianAuth"));
});
});
+105
View File
@@ -0,0 +1,105 @@
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const fromRoot = (...segments: string[]) => resolve(process.cwd(), ...segments);
function expectMatchingMessageKeys(expected: unknown, actual: unknown): void {
if (
expected === null ||
typeof expected !== "object" ||
actual === null ||
typeof actual !== "object"
) {
expect(typeof actual).toBe(typeof expected);
return;
}
const expectedRecord = expected as Record<string, unknown>;
const actualRecord = actual as Record<string, unknown>;
expect(Object.keys(actualRecord).sort()).toEqual(
Object.keys(expectedRecord).sort(),
);
for (const key of Object.keys(expectedRecord)) {
expectMatchingMessageKeys(expectedRecord[key], actualRecord[key]);
}
}
describe("managed record settings UI", () => {
it("gates shared deep links and renders integration status without controls", async () => {
const [
statusView,
managedSettingsView,
settingsPage,
layoutModulePage,
sectionGate,
...localeBundles
] = await Promise.all([
readFile(
fromRoot("src/components/settings/managed-integration-status.tsx"),
"utf8",
),
readFile(
fromRoot("src/components/settings/managed-record-settings-section.tsx"),
"utf8",
),
readFile(fromRoot("src/app/settings/[section]/page.tsx"), "utf8"),
readFile(fromRoot("src/app/settings/layout/[module]/page.tsx"), "utf8"),
readFile(
fromRoot("src/components/settings/record-settings-section-gate.tsx"),
"utf8",
),
...["de", "en", "es", "fr", "it", "pl"].map((locale) =>
readFile(fromRoot(`messages/${locale}.json`), "utf8"),
),
]);
expect(statusView).toContain("recordSettingsIntegrations");
expect(statusView).toContain("/api/record-settings/integrations");
expect(statusView).toContain("assertRecordSettingsResponseForRecord");
expect(statusView).not.toContain("<Button");
expect(statusView).not.toContain("<Link");
expect(settingsPage).toContain("RecordSettingsSectionGate");
expect(sectionGate).toContain("if (!inSharedRecord) return children");
expect(sectionGate).toContain("ManagedIntegrationStatus");
expect(sectionGate).toContain("ManagedRecordSettingsSection");
expect(sectionGate).toContain("if (isLoading)");
expect(layoutModulePage).toContain(
'RecordSettingsSectionGate section="layout"',
);
expect(managedSettingsView).toContain(
"safeParseManagedRecordSettingsPatch",
);
expect(managedSettingsView).toContain("/api/record-settings/${family}");
expect(managedSettingsView).not.toContain("/api/auth/");
expect(managedSettingsView).not.toContain("/api/user/");
expect(managedSettingsView).not.toContain("<textarea");
expect(managedSettingsView).not.toContain("Display name");
expect(managedSettingsView).not.toContain("Enable mood reminder");
const referenceMessages = JSON.parse(localeBundles[0]).settings.sharedRecord
.managedSettings;
for (const bundle of localeBundles) {
const sharedRecord = JSON.parse(bundle).settings.sharedRecord;
expect(sharedRecord).toMatchObject({
unavailableTitle: expect.any(String),
integrationStatusDescription: expect.any(String),
managedSettings: {
profile: {
title: expect.any(String),
displayName: expect.any(String),
},
modules: { title: expect.any(String) },
notifications: { title: expect.any(String) },
thresholds: { title: expect.any(String) },
coach: { title: expect.any(String) },
insights: { title: expect.any(String) },
},
});
expectMatchingMessageKeys(
referenceMessages,
sharedRecord.managedSettings,
);
}
});
});
+12 -3
View File
@@ -1745,7 +1745,14 @@ const DELEGABLE_MANAGE_ROUTES: Record<string, ManageEntry> = {
* the auth layer, or "no module reaches it" would be true because the symbol
* had been renamed rather than because no module reaches it.
*/
const GUARDIAN_ROUTES: Record<string, string> = {};
const GUARDIAN_ROUTES: Record<string, string> = {
"app/api/record-settings/route.ts":
"The managed record's settings descriptor; it is refused for an adult record even with MANAGE.",
"app/api/record-settings/integrations/route.ts":
"Verified integration connection status for the managed record, without any provider control surface.",
"app/api/record-settings/[family]/route.ts":
"The closed, field-specific managed-record configuration DTOs; identity and credentials remain excluded.",
};
/**
* Route modules that deliberately resolve the ACTOR and keep working under a
@@ -1784,9 +1791,11 @@ const ACTOR_ROUTES: Record<string, string> = {
* number rather than one per list on purpose: filling either of the new
* literals collides here by construction, so two people filling them at once
* find out from the merge rather than from production. Import-status polling
* adds two record entries and two MANAGE entries: 190 → 194.
* adds two record entries and two MANAGE entries: 190 → 194. Managed-record
* settings add two guardian-only entries: 194 → 196. Field-specific
* managed-record configuration adds one more: 196 → 197.
*/
const FROZEN_ENTRY_COUNT = 194;
const FROZEN_ENTRY_COUNT = 197;
/**
* The two surfaces that authenticate a Bearer token outside `requireAuth` —