Merge commit '38214e472' into work/v137-record-session-fence

This commit is contained in:
Marc-André Bombeck committed 2026-08-06 17:10:56 +02:00
commit a08e82d0be
81 files changed
+3505 -386

No files matched your search

@@ -0,0 +1,104 @@
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
import { classifySettingsDestination } from "@/lib/record-settings";
const pathnameRef = { value: "/settings/security" };
const activeRecordRef = { value: true };
vi.mock("next/navigation", () => ({
usePathname: () => pathnameRef.value,
}));
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({
user: activeRecordRef.value
? {
id: "guardian",
modules: undefined,
accountAccess: {
active: {
accountId: "managed-record",
username: "managed-record",
displayName: "Managed record",
access: "write",
level: "manage",
recordKind: "managed",
sections: null,
canWrite: true,
},
accounts: [],
canSwitch: true,
},
}
: null,
isAuthenticated: true,
isLoading: false,
refetch: vi.fn(),
}),
}));
vi.mock("@/hooks/use-account-switch", () => ({
useAccountSwitch: () => ({
mutate: vi.fn(),
isPending: false,
}),
}));
import {
SETTINGS_SECTIONS,
SettingsShell,
} from "@/components/settings/settings-shell";
import { SharedRecordBanner } from "@/components/layout/shared-record-banner";
function renderSettings(pathname = "/settings/security") {
pathnameRef.value = pathname;
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<SettingsShell>
<div>managed settings content</div>
</SettingsShell>
</I18nProvider>,
);
}
describe("managed record navigation", () => {
it("shows only managed settings destinations while retaining an unavailable deep-link heading", () => {
const html = renderSettings();
const managedDestinations = SETTINGS_SECTIONS.filter(
(section) =>
classifySettingsDestination(section.slug).kind === "managed-guardian",
);
const unavailableDestinations = SETTINGS_SECTIONS.filter(
(section) =>
classifySettingsDestination(section.slug).kind !== "managed-guardian",
);
expect(html).toContain('data-active-record-id="managed-record"');
expect(html).toContain('id="settings-section-security-title"');
expect(html).not.toMatch(/href="[^"]*managed-record/);
for (const section of managedDestinations) {
expect(
html.match(new RegExp(`href="/settings/${section.slug}"`, "g"))
?.length ?? 0,
).toBe(2);
}
for (const section of unavailableDestinations) {
expect(html).not.toContain(`href="/settings/${section.slug}"`);
}
});
it("keeps the exit control present while administering a managed record", () => {
const html = renderToStaticMarkup(
<I18nProvider initialLocale="en">
<SharedRecordBanner />
</I18nProvider>,
);
expect(html).toContain('data-account-id="managed-record"');
expect(html).toContain('data-slot="shared-record-banner-exit"');
});
});
@@ -0,0 +1,135 @@
import { describe, expect, it } from "vitest";
import {
SHARED_RECORD_DOMAIN_ROUTE_FAMILIES,
resolveSharedRecordNavigation,
} from "@/lib/navigation/shared-record";
import { SHARE_DOMAINS } from "@/lib/sharing/scope";
import {
isDestinationInSharedRecord,
visibleNavDestinations,
} from "@/components/layout/nav-model";
import { resolveRecordPresentation } from "@/lib/navigation/record-presentation";
describe("shared-record navigation", () => {
it("keeps every granted route family in one closed eight-domain inventory", () => {
expect(Object.keys(SHARED_RECORD_DOMAIN_ROUTE_FAMILIES)).toEqual(
SHARE_DOMAINS,
);
for (const domain of SHARE_DOMAINS) {
expect(
SHARED_RECORD_DOMAIN_ROUTE_FAMILIES[domain].length,
`${domain} needs at least one route family`,
).toBeGreaterThan(0);
}
});
it("offers a scoped grant only its matching doorway", () => {
const navigation = resolveSharedRecordNavigation(["labs"]);
expect(navigation.destinationHrefs).toEqual(["/labs"]);
expect(navigation.allowsPath("/labs/panel/7")).toBe(true);
expect(navigation.allowsPath("/medications")).toBe(false);
expect(
visibleNavDestinations(undefined, true, true, ["labs"]).map(
(destination) => destination.href,
),
).toEqual(["/labs"]);
expect(isDestinationInSharedRecord("/labs/panel/7", ["labs"])).toBe(true);
expect(isDestinationInSharedRecord("/medications", ["labs"])).toBe(false);
});
it("keeps profile facts outside Settings and makes profile the scoped doorway", () => {
const navigation = resolveSharedRecordNavigation(["profile"]);
expect(SHARED_RECORD_DOMAIN_ROUTE_FAMILIES.profile).toEqual(["/profile"]);
expect(navigation.destinationHrefs).toEqual(["/profile"]);
expect(navigation.allowsPath("/profile")).toBe(true);
expect(navigation.allowsPath("/settings/anamnesis")).toBe(false);
});
it("uses the active record scope instead of the actor's module flags", () => {
const actorModules = {
medications: false,
labs: false,
mood: false,
mentalHealth: false,
cycle: false,
illness: false,
inboundDocuments: false,
};
expect(
visibleNavDestinations(actorModules, true, true, ["medications"]).map(
(destination) => destination.href,
),
).toEqual(["/medications"]);
});
it("keeps whole-record navigation distinct from an all-sections scope", () => {
const wholeRecord = resolveSharedRecordNavigation(null);
const allSections = resolveSharedRecordNavigation([...SHARE_DOMAINS]);
expect(wholeRecord.allowsPath("/")).toBe(true);
expect(allSections.allowsPath("/")).toBe(false);
});
it("refuses unclassified direct URLs in presentation before pages start reads", () => {
const navigation = resolveSharedRecordNavigation(["documents"]);
expect(navigation.allowsPath("/documents/record-1")).toBe(true);
expect(navigation.allowsPath("/settings/account")).toBe(false);
expect(navigation.allowsPath("/new-unclassified-route")).toBe(false);
});
it("keeps server-resolved access and record kind distinct in chrome", () => {
expect(
resolveRecordPresentation({
level: "read",
canWrite: false,
recordKind: "shared",
}),
).toEqual({ access: "view", recordKind: "shared" });
expect(
resolveRecordPresentation({
level: "write",
canWrite: true,
recordKind: "shared",
}),
).toEqual({ access: "view-and-add", recordKind: "shared" });
expect(
resolveRecordPresentation({
level: "manage",
canWrite: true,
recordKind: "shared",
}),
).toEqual({ access: "manage", recordKind: "shared" });
expect(
resolveRecordPresentation({
level: "manage",
canWrite: true,
recordKind: "managed",
}),
).toEqual({ access: "manage", recordKind: "managed" });
});
it("fails closed when a malformed entry reaches presentation", () => {
expect(
resolveRecordPresentation({
level: "manage",
canWrite: false,
recordKind: "managed",
} as never),
).toEqual({ access: "view", recordKind: "self" });
expect(
resolveRecordPresentation({
level: "unknown",
canWrite: true,
recordKind: "shared",
} as never),
).toEqual({ access: "view", recordKind: "self" });
});
});
+16 -2
View File
@@ -879,6 +879,10 @@ const DELEGABLE_ROUTES: Record<string, DelegableEntry> = {
domain: "profile",
why: "The record's health-profile facts, read through `readHealthProfileFacts(user.id)`. The module holds write helpers, and the GET calls none of them.",
},
"app/api/profile/summary/route.ts": {
domain: "profile",
why: "The bounded shared profile read combines only the record's allergies, family history, and current lifestyle facts. It carries no settings, AI configuration, mutations, or encrypted free text.",
},
"app/api/mood-entries/route.ts": {
domain: "mind",
why: "The record's mood entries. The nested include is the tag vocabulary — a key and a kind, no identifiers. Note that `GET /api/mood/tags` is refused and stays refused: it pulls the owner's tag LAYOUT, which is a presentation preference and belongs to the person, not the record.",
@@ -939,6 +943,14 @@ const DELEGABLE_ROUTES: Record<string, DelegableEntry> = {
domain: "cycle",
why: "The record's cycle history. Whether cycle tracking applies is a property of the record, and the gate gets there by re-loading the profile and gender for the resolved user id — note that the `gender` argument at the call site is dead (`requireCycleEnabled` names it `_gender`), so it is the id that carries the substitution, not that argument.",
},
"app/api/cycle/calendar/route.ts": {
domain: "cycle",
why: "The initial cycle calendar read. Its gate resolves tracking eligibility from the substituted record id, and the handler only reads that record's bounded calendar inputs.",
},
"app/api/cycle/insights/route.ts": {
domain: "cycle",
why: "The initial cycle insights read. Its rate-limit key and every phase-correlation input use the resolved record id; it creates no provider egress or mutation.",
},
"app/api/cycle/day-logs/route.ts": {
domain: "cycle",
why: 'One day of the record\'s cycle log. The `auditLog("cycle.day-log.upsert")` in this file belongs to the PUT, which keeps `requireAuth()`.',
@@ -1793,9 +1805,11 @@ const ACTOR_ROUTES: Record<string, string> = {
* find out from the merge rather than from production. Import-status polling
* 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.
* managed-record configuration adds one more: 196 → 197. The bounded
* read-only profile summary makes the next explicit admission: 197 → 198.
* The two initial Cycle reads follow as separate, scoped admissions: 198 → 200.
*/
const FROZEN_ENTRY_COUNT = 197;
const FROZEN_ENTRY_COUNT = 200;
/**
* The two surfaces that authenticate a Bearer token outside `requireAuth` —
@@ -1,6 +1,3 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
@@ -13,10 +10,6 @@ import {
NOTIFICATION_CHANNELS,
NOTIFICATION_DELIVERY_MATRIX,
} from "../../tests/fixtures/v137/notification-matrix";
import {
ACCESSIBILITY_STATES,
RELEASE_JOURNEYS,
} from "../../tests/fixtures/v137/e2e-journeys";
describe("sharing and handler inventories", () => {
it("freezes the exact sharing-domain vocabulary", () => {
@@ -104,28 +97,3 @@ describe("security principals and compatibility", () => {
expect(LEGACY_ACCOUNT_PAYLOADS[3]?.legacyDecoder).toBe("deny");
});
});
describe("journey and accessibility inventory", () => {
it("names every release journey and accessibility state exactly once", () => {
expect(RELEASE_JOURNEYS).toHaveLength(8);
expect(ACCESSIBILITY_STATES).toHaveLength(5);
expect(new Set(RELEASE_JOURNEYS.map(({ name }) => name)).size).toBe(
RELEASE_JOURNEYS.length,
);
expect(new Set(ACCESSIBILITY_STATES.map(({ name }) => name)).size).toBe(
ACCESSIBILITY_STATES.length,
);
});
it("leaves the browser anchors discoverable without focused or skipped cases", () => {
const anchorFiles = [
"v137-sharing-managed-profiles.spec.ts",
"v137-sharing-managed-profiles-a11y.spec.ts",
];
for (const file of anchorFiles) {
const source = readFileSync(join(process.cwd(), "e2e", file), "utf8");
expect(source).not.toMatch(/\b(?:test|describe)\.(?:only|skip)\b/);
}
});
});