mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
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.
181 lines
6.3 KiB
TypeScript
181 lines
6.3 KiB
TypeScript
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 };
|
|
/** The record on screen. `managed` unless a case says otherwise. */
|
|
const recordRef: { value: { kind: "managed" | "shared"; level: string } } = {
|
|
value: { kind: "managed", level: "manage" },
|
|
};
|
|
|
|
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: recordRef.value.level,
|
|
recordKind: recordRef.value.kind,
|
|
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>,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The destinations a record offers, by what kind of record it is.
|
|
*
|
|
* v1.37.0 — a managed profile offers guardian configuration AND the record
|
|
* content a MANAGE holder may write, because a Guardian holds MANAGE. The
|
|
* `manage-writable` kind is what carries the second half; before it existed
|
|
* the anamnesis form had no page a Guardian could open, while the routes
|
|
* behind it had admitted the write since the level shipped.
|
|
*/
|
|
const MANAGED_KINDS = new Set(["managed-guardian", "manage-writable"]);
|
|
|
|
describe("managed record navigation", () => {
|
|
it("shows only managed settings destinations while retaining an unavailable deep-link heading", () => {
|
|
recordRef.value = { kind: "managed", level: "manage" };
|
|
const html = renderSettings();
|
|
const managedDestinations = SETTINGS_SECTIONS.filter((section) =>
|
|
MANAGED_KINDS.has(classifySettingsDestination(section.slug).kind),
|
|
);
|
|
const unavailableDestinations = SETTINGS_SECTIONS.filter(
|
|
(section) =>
|
|
!MANAGED_KINDS.has(classifySettingsDestination(section.slug).kind),
|
|
);
|
|
|
|
// The partition is non-empty on both sides. A classification change that
|
|
// emptied either would make one of the loops below prove nothing.
|
|
expect(managedDestinations.length).toBeGreaterThan(1);
|
|
expect(unavailableDestinations.length).toBeGreaterThan(1);
|
|
// And the destination this release opened is really on the offered side.
|
|
expect(managedDestinations.map((s) => s.slug)).toContain("anamnesis");
|
|
|
|
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("offers an adult manager the record content and none of the configuration", () => {
|
|
// The other half of the same classification, and the half that is easiest
|
|
// to get wrong in the generous direction: a delegate manages somebody's
|
|
// health record, not their account. Anamnese is offered; modules,
|
|
// thresholds and notification routing are not, even though a Guardian
|
|
// reaches all three.
|
|
recordRef.value = { kind: "shared", level: "manage" };
|
|
const html = renderSettings();
|
|
|
|
expect(html).toContain('href="/settings/anamnesis"');
|
|
for (const guardianOnly of [
|
|
"account",
|
|
"modules",
|
|
"notifications",
|
|
"thresholds",
|
|
"coach",
|
|
]) {
|
|
expect(html, guardianOnly).not.toContain(
|
|
`href="/settings/${guardianOnly}"`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("offers a WRITE delegate no settings destination at all", () => {
|
|
// The refusal side. `manage-writable` is admitted by LEVEL, not by being
|
|
// in a shared record: a WRITE grant reaches no Settings page, and a
|
|
// destination it could see would be one the route below refuses.
|
|
recordRef.value = { kind: "shared", level: "write" };
|
|
const html = renderSettings();
|
|
|
|
expect(html).not.toContain('href="/settings/anamnesis"');
|
|
for (const section of SETTINGS_SECTIONS) {
|
|
expect(html, section.slug).not.toContain(
|
|
`href="/settings/${section.slug}"`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("offers a managed entry below MANAGE nothing at all", () => {
|
|
// A guardian grant is always MANAGE today, which is exactly why the level
|
|
// check was easy to leave out of the managed branch. Without it a managed
|
|
// entry that ever arrived below MANAGE would list destinations the section
|
|
// gate refuses, sending somebody to a page that explains it cannot open.
|
|
recordRef.value = { kind: "managed", level: "read" };
|
|
const html = renderSettings();
|
|
|
|
for (const section of SETTINGS_SECTIONS) {
|
|
expect(html, section.slug).not.toContain(
|
|
`href="/settings/${section.slug}"`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("keeps the exit control present while administering a managed record", () => {
|
|
recordRef.value = { kind: "managed", level: "manage" };
|
|
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"');
|
|
});
|
|
});
|