mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
fix(fhir): stop advertising a credential no client can obtain
The capability descriptor named `fhir:read` as the scope a narrow token needs for the REST face. No code path mints a token carrying it — every mint grants a wildcard, the medication ingest scope, or an MCP scope — so the face is reachable by the owner's cookie session or a wildcard device token and by nothing else. Advertising an unobtainable credential invites a client to go looking for one. The constant stays where it belongs, as the fail-closed check each route makes; the descriptor now reports the fact instead. End-to-end coverage for the scope picker: the first run opens it expanded with the fenced tier rendered and no group control over it, a group row clears the 44 px floor at 390 px, and a leaf grid opens from its chevron. All on stable test ids rather than text a breakpoint can hide.
This commit is contained in:
1 parent
f12290532f
commit
ffe99caccd
5 files changed
+72
-12
No files matched your search
@@ -15737,9 +15737,10 @@ components:
|
||||
restBaseUrl:
|
||||
type: string
|
||||
description: Base path of the read-only FHIR R4 REST face.
|
||||
readScope:
|
||||
type: string
|
||||
description: Bearer scope a narrow token needs to read the FHIR face.
|
||||
scopeMintable:
|
||||
type: boolean
|
||||
description: "Whether a narrow Bearer token for the FHIR face can be obtained. Always false: no mint path grants the
|
||||
scope, so the face is reachable by the owner's cookie session or a wildcard device token only."
|
||||
resourceTypes:
|
||||
type: array
|
||||
items:
|
||||
@@ -15760,7 +15761,7 @@ components:
|
||||
- snomedRoute
|
||||
- germanAtcDefaultLocales
|
||||
- restBaseUrl
|
||||
- readScope
|
||||
- scopeMintable
|
||||
- resourceTypes
|
||||
- operations
|
||||
- searchParams
|
||||
|
||||
@@ -53,6 +53,56 @@ test.describe("Settings → Export & Import", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("opens the scope picker expanded on a first run, fenced tier and all", async ({
|
||||
page,
|
||||
}) => {
|
||||
// The three panel states are the release's whole point, and the first one
|
||||
// is the only place a template is applied. It has to be VISIBLE — a
|
||||
// template behind a collapsed disclosure is opt-out wearing a hat — so the
|
||||
// picker, its twelve group rows and the fenced tier all render without a
|
||||
// click.
|
||||
await page.goto("/settings/gesundheitsakte", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await expect(page.getByTestId("report-scope-picker")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(page.getByTestId("report-group-row-vitals")).toBeVisible();
|
||||
await expect(page.getByTestId("report-group-row-sensitive")).toHaveCount(0);
|
||||
await expect(page.getByTestId("report-sensitive-tier")).toBeVisible();
|
||||
// The fenced tier has no group control — there is no single click that
|
||||
// turns on more than one sensitive leaf.
|
||||
await expect(page.getByTestId("report-group-check-sensitive")).toHaveCount(
|
||||
0,
|
||||
);
|
||||
await expect(page.getByTestId("report-scope-summary")).toBeVisible();
|
||||
});
|
||||
|
||||
test("clears the tap floor on the group rows at 390 px", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto("/settings/gesundheitsakte", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
const row = page.getByTestId("report-group-row-vitals");
|
||||
await expect(row).toBeVisible({ timeout: 10_000 });
|
||||
const box = await row.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
expect(box!.height).toBeGreaterThanOrEqual(44);
|
||||
});
|
||||
|
||||
test("opens a group's leaf grid from its chevron", async ({ page }) => {
|
||||
await page.goto("/settings/gesundheitsakte", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
const row = page.getByTestId("report-group-row-vitals");
|
||||
await expect(row).toBeVisible({ timeout: 10_000 });
|
||||
// Collapsed: the leaves are behind the disclosure, which is what makes a
|
||||
// repeat run two interactions.
|
||||
await expect(page.getByTestId("report-leaf-PULSE")).toHaveCount(0);
|
||||
await row.getByRole("button", { expanded: false }).click();
|
||||
await expect(page.getByTestId("report-leaf-PULSE")).toBeVisible();
|
||||
});
|
||||
|
||||
test("renders the import surfaces with stable testids", async ({ page }) => {
|
||||
await page.goto("/settings/export", { waitUntil: "domcontentloaded" });
|
||||
// v1.15.7 (issue #281) — the Apple Health and generic-JSON import
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
GERMAN_ATC_DEFAULT_LOCALES,
|
||||
} from "@/lib/fhir/build-bundle";
|
||||
import {
|
||||
FHIR_READ_SCOPE,
|
||||
FHIR_REST_RESOURCE_TYPES,
|
||||
FHIR_EVERYTHING_OPERATION,
|
||||
FHIR_SEARCH_PARAMS,
|
||||
@@ -87,7 +86,7 @@ type CapabilitiesBody = {
|
||||
snomedRoute: string;
|
||||
germanAtcDefaultLocales: string[];
|
||||
restBaseUrl: string;
|
||||
readScope: string;
|
||||
scopeMintable: boolean;
|
||||
resourceTypes: string[];
|
||||
operations: string[];
|
||||
searchParams: string[];
|
||||
@@ -169,7 +168,10 @@ describe("GET /api/meta/capabilities — drift guards", () => {
|
||||
const res = await call();
|
||||
const body = (await res.json()) as CapabilitiesBody;
|
||||
expect(body.data.fhir.restBaseUrl).toBe("/api/fhir");
|
||||
expect(body.data.fhir.readScope).toBe(FHIR_READ_SCOPE);
|
||||
// The face checks `fhir:read`, but nothing mints a token carrying it, so
|
||||
// advertising it as an obtainable credential would be a claim the code
|
||||
// cannot support.
|
||||
expect(body.data.fhir.scopeMintable).toBe(false);
|
||||
expect(body.data.fhir.resourceTypes).toEqual([...FHIR_REST_RESOURCE_TYPES]);
|
||||
expect(body.data.fhir.operations).toEqual([FHIR_EVERYTHING_OPERATION]);
|
||||
expect(body.data.fhir.searchParams).toEqual([...FHIR_SEARCH_PARAMS]);
|
||||
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
GERMAN_ATC_DEFAULT_LOCALES,
|
||||
} from "@/lib/fhir/build-bundle";
|
||||
import {
|
||||
FHIR_READ_SCOPE,
|
||||
FHIR_REST_RESOURCE_TYPES,
|
||||
FHIR_EVERYTHING_OPERATION,
|
||||
FHIR_SEARCH_PARAMS,
|
||||
@@ -111,7 +110,13 @@ export const GET = apiHandler(async () => {
|
||||
// whole-record operation, the honoured search params and the narrow
|
||||
// Bearer scope. All sourced from the canonical `rest.ts` constants.
|
||||
restBaseUrl: "/api/fhir",
|
||||
readScope: FHIR_READ_SCOPE,
|
||||
// The face checks a narrow `fhir:read` scope, and NO code path mints a
|
||||
// token carrying it — every mint grants a wildcard, the medication
|
||||
// ingest scope, or an MCP scope. So the face is reachable by the owner's
|
||||
// cookie session or a wildcard device token, and by nothing else. It is
|
||||
// reported that way rather than advertising a credential a client cannot
|
||||
// obtain.
|
||||
scopeMintable: false,
|
||||
resourceTypes: FHIR_REST_RESOURCE_TYPES,
|
||||
operations: [FHIR_EVERYTHING_OPERATION],
|
||||
searchParams: FHIR_SEARCH_PARAMS,
|
||||
|
||||
@@ -70,9 +70,11 @@ const capabilitiesResponse = z
|
||||
restBaseUrl: z
|
||||
.string()
|
||||
.describe("Base path of the read-only FHIR R4 REST face."),
|
||||
readScope: z
|
||||
.string()
|
||||
.describe("Bearer scope a narrow token needs to read the FHIR face."),
|
||||
scopeMintable: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"Whether a narrow Bearer token for the FHIR face can be obtained. Always false: no mint path grants the scope, so the face is reachable by the owner's cookie session or a wildcard device token only.",
|
||||
),
|
||||
resourceTypes: z
|
||||
.array(z.string())
|
||||
.describe(
|
||||
|
||||
Reference in new issue
Block a user