mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
fix(02-12.1): reach fenced document routes through the app transport
A browser-issued <img src>, <iframe src> or <a href download> cannot carry the record-session assertion, so the fence refused all three on any session that had been inside a shared record — permanently, since none of them will ever send a header. - add a structural guard: no client file may name a record-fenced route in a src=, href= or window.location sink - fetch the thumbnail, the inline preview and the download through the app transport and hand the DOM an object URL, revoked on unmount - make the two API probes in the managed-profiles browser spec assert the same context the app beside them asserts
This commit is contained in:
1 parent
8fcbdcc0df
commit
ffe38c9bb6
7 files changed
+739
-27
No files matched your search
@@ -18,6 +18,43 @@ const DOMAIN_READS = {
|
||||
documents: "/api/documents/inbound",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* v1.37.0 — the record-session assertion the app is currently sending.
|
||||
*
|
||||
* The two API probes in this file are deliberate: they check a status code the
|
||||
* UI does not surface (a MANAGE-only generated read) and a payload the UI
|
||||
* paginates. But a bare `fetch` inside `page.evaluate` carries no
|
||||
* record-session headers, and the fence refuses an unasserted request on any
|
||||
* session that has been inside a shared record — so the probes started failing
|
||||
* with 403 for a reason that had nothing to do with what they test.
|
||||
*
|
||||
* They are NOT an old-bundle simulation and must not become one: that case is
|
||||
* covered deliberately in `src/__tests__/record-session-fence-compat.test.ts`
|
||||
* and `tests/integration/record-session-fence.test.ts`, where the 403 and the
|
||||
* recovery are the subject. Here the subject is the grant level, so the probe
|
||||
* asserts exactly what the app beside it asserts — copied off a real app
|
||||
* request rather than computed, so it cannot drift from what the client
|
||||
* actually believes.
|
||||
*/
|
||||
function trackFenceAssertion(page: Page): () => {
|
||||
"x-healthlog-record-epoch": string;
|
||||
"x-healthlog-record-scope": string;
|
||||
} {
|
||||
let latest = { epoch: "bootstrap", scope: "bootstrap" };
|
||||
page.on("request", (request) => {
|
||||
const headers = request.headers();
|
||||
const epoch = headers["x-healthlog-record-epoch"];
|
||||
const scope = headers["x-healthlog-record-scope"];
|
||||
if (epoch === undefined || scope === undefined) return;
|
||||
if (epoch === "bootstrap") return;
|
||||
latest = { epoch, scope };
|
||||
});
|
||||
return () => ({
|
||||
"x-healthlog-record-epoch": latest.epoch,
|
||||
"x-healthlog-record-scope": latest.scope,
|
||||
});
|
||||
}
|
||||
|
||||
async function openSwitcher(page: Page) {
|
||||
await page.getByRole("button", { name: "User menu" }).first().click();
|
||||
await page.locator('[data-slot="account-switcher-trigger"]').click();
|
||||
@@ -294,6 +331,7 @@ test.describe.serial("scoped sharing browser journeys", () => {
|
||||
test("lets an adult WRITE grant add only to the selected record", async ({
|
||||
page,
|
||||
}) => {
|
||||
const fenceHeaders = trackFenceAssertion(page);
|
||||
const record = E2E_LEVEL_RECORDS.find(
|
||||
(candidate) => candidate.access === "WRITE",
|
||||
);
|
||||
@@ -324,10 +362,10 @@ test.describe.serial("scoped sharing browser journeys", () => {
|
||||
expect((await post).status()).toBeLessThan(300);
|
||||
|
||||
await page.reload();
|
||||
const payload = await page.evaluate(async () => {
|
||||
const response = await fetch("/api/measurements");
|
||||
const payload = await page.evaluate(async (headers) => {
|
||||
const response = await fetch("/api/measurements", { headers });
|
||||
return { status: response.status, body: await response.json() };
|
||||
});
|
||||
}, fenceHeaders());
|
||||
expect(payload.status).toBeLessThan(300);
|
||||
expect(JSON.stringify(payload.body)).toContain("124");
|
||||
await expect(banner).toHaveAttribute("data-account-id", accountId);
|
||||
@@ -342,6 +380,7 @@ test.describe.serial("scoped sharing browser journeys", () => {
|
||||
test("opens MANAGE-only generated reads without opening adult Settings", async ({
|
||||
page,
|
||||
}) => {
|
||||
const fenceHeaders = trackFenceAssertion(page);
|
||||
const record = E2E_LEVEL_RECORDS.find(
|
||||
(candidate) =>
|
||||
candidate.access === "MANAGE" && candidate.recordKind === "shared",
|
||||
@@ -357,10 +396,10 @@ test.describe.serial("scoped sharing browser journeys", () => {
|
||||
|
||||
await page.goto("/");
|
||||
await expect(banner).toHaveAttribute("data-access-level", "manage");
|
||||
const generatedRead = await page.evaluate(async () => {
|
||||
const response = await fetch("/api/dashboard/summary");
|
||||
const generatedRead = await page.evaluate(async (headers) => {
|
||||
const response = await fetch("/api/dashboard/summary", { headers });
|
||||
return response.status;
|
||||
});
|
||||
}, fenceHeaders());
|
||||
expect(generatedRead).toBeLessThan(300);
|
||||
await expect(
|
||||
page.locator('[data-slot="shared-record-unavailable"]'),
|
||||
|
||||
Reference in new issue
Block a user