mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
POST /api/allergies and POST /api/family-history have resolved
requireRecordAuth("manage", "profile") since the level shipped, while the only
form in the product that posts to either sat behind a Settings destination
classified unavailable. The write was admitted, audited and owner-scoped, and
nobody could reach it — the one-ended change this repository keeps
rediscovering, with the permission shipped and the surface left as a follow-up.
The destination now carries a classification that means what the routes mean:
reachable by everybody they already admit, which is a guardian inside the
profile they administer and an adult delegate holding manage. The four places
that decided visibility on the guardian-only literal were each updated
deliberately, and the shell that refused every settings path in an adult share
now admits exactly the destinations classified as record content. Record
configuration — modules, thresholds, notification routing — stays with the
owner at every level.
The page inside a record is its own component holding the two managers whose
routes answer for a delegate. Mounting the personal section would have put
three more forms on screen that refuse on submit. The family-history card says
whose statement the row is, which the route comment could not.
Both routes carried a long paragraph arguing the write was deliberately
unreachable. It is gone rather than left to send the next reader back to close
what this opens.
The integration coverage moves from the undeclared-mode refusal to a manage
contract: a manage delegate and a guardian each write and the row lands under
the owner with the delegate named as actor, while read and write delegates get
the insufficient-access refusal. Nothing exercised either route at manage
before.
166 lines
5.6 KiB
TypeScript
166 lines
5.6 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("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"');
|
|
});
|
|
});
|