diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 5f2d5e945..b69c0b124 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -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 diff --git a/e2e/settings-export.spec.ts b/e2e/settings-export.spec.ts index e2de1e22b..dea803069 100644 --- a/e2e/settings-export.spec.ts +++ b/e2e/settings-export.spec.ts @@ -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 diff --git a/src/app/api/meta/capabilities/__tests__/route.test.ts b/src/app/api/meta/capabilities/__tests__/route.test.ts index 970ca750a..e4102b843 100644 --- a/src/app/api/meta/capabilities/__tests__/route.test.ts +++ b/src/app/api/meta/capabilities/__tests__/route.test.ts @@ -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]); diff --git a/src/app/api/meta/capabilities/route.ts b/src/app/api/meta/capabilities/route.ts index 8db19ac72..b9ce59410 100644 --- a/src/app/api/meta/capabilities/route.ts +++ b/src/app/api/meta/capabilities/route.ts @@ -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, diff --git a/src/lib/openapi/routes/meta.ts b/src/lib/openapi/routes/meta.ts index 7ce36fdef..8b9249711 100644 --- a/src/lib/openapi/routes/meta.ts +++ b/src/lib/openapi/routes/meta.ts @@ -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(