mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(clinician-share): a document share exposes only the document
Sharing a single document from the vault minted a full-record share: the create form always defaulted `resourceTypes` to ["Patient","Observation"] and never signalled a documents-only intent, so the served /c/<token> view aggregated the whole doctor report — every vital, lab, medication and wellness figure — alongside the attached file. An empty section blob was indistinguishable from "defaults", so the report always resolved to the full record. Introduce an explicit documents-only share: - add a `documentOnly` flag to the create payload; when set the server forces an empty report scope (all-off sections, no FHIR resource types, FHIR API off) and requires at least one document, so a document link can never widen into a record — whatever the body sends. - gate the clinician-view aggregation on an enabled section: a share with no section on never calls the report aggregator, so no health metric leaves the database. The recipient sees only the attached document(s). - the document detail Share action opens the create form in documents-only mode: hide the FHIR toggle, the FHIR resource-type checkboxes and the history-range selector, keep the picker/label/expiry/passphrase, and always carry the launched document into the created link. - Settings sharing keeps the full record-share form unchanged. Extract the create-payload builder as a pure function and cover the scope contract, the empty-scope data load, and the served-view metric-free guarantee with tests; refresh the share e2e and regenerate the OpenAPI contract.
This commit is contained in:
1 parent
fa42c8bc0b
commit
d4965033c2
13 files changed
+503
-92
No files matched your search
@@ -234,12 +234,14 @@ test.describe("clinician document sharing", () => {
|
||||
await clinician.context().close();
|
||||
});
|
||||
|
||||
test("the document detail Share action opens the create flow with that document pre-attached", async ({
|
||||
test("the document Share action mints a DOCUMENT-ONLY link — the document, never the record", async ({
|
||||
page,
|
||||
browser,
|
||||
}) => {
|
||||
// Opens two stacked sheets (detail → share) and drives a real create with
|
||||
// the one-time QR render — heavier than the default 30s budget on a loaded
|
||||
// runner. Runs on the desktop and 390px mobile projects.
|
||||
// Opens two stacked sheets (detail → share), drives a real create with the
|
||||
// one-time QR render, then unlocks the link as a recipient to prove the
|
||||
// served view carries the document and ZERO health metrics. Heavier than
|
||||
// the default 30s budget. Runs on the desktop and 390px mobile projects.
|
||||
test.slow();
|
||||
|
||||
// Deep-link straight to the document detail sheet.
|
||||
@@ -263,6 +265,13 @@ test.describe("clinician document sharing", () => {
|
||||
await expect(chips).toHaveCount(1);
|
||||
await expect(chips.first()).toContainText("MRT Knie");
|
||||
|
||||
// Document-only mode hides every record-scope control: no FHIR API toggle,
|
||||
// no FHIR resource-type checkboxes, no history-range selector. Only the
|
||||
// document, label, and expiry remain.
|
||||
await expect(shareSheet.locator("#share-fhir")).toHaveCount(0);
|
||||
await expect(shareSheet.locator("#share-range")).toHaveCount(0);
|
||||
await expect(shareSheet.locator("#share-expiry")).toHaveCount(1);
|
||||
|
||||
// Create the link — the one-time reveal (with the scannable QR) lands in
|
||||
// the same sheet, carrying the one attached document.
|
||||
await shareSheet.getByRole("button", { name: "Create link" }).click();
|
||||
@@ -276,6 +285,46 @@ test.describe("clinician document sharing", () => {
|
||||
const box = await qrImg.boundingBox();
|
||||
expect(box, "QR image has a rendered box").not.toBeNull();
|
||||
expect(box!.width).toBeGreaterThanOrEqual(160);
|
||||
|
||||
// Read the one-time secrets off the reveal and unlock the link as a
|
||||
// recipient (no owner session).
|
||||
const shareUrl = (await reveal.locator("code").first().innerText()).trim();
|
||||
const token = shareUrl.split("/c/")[1];
|
||||
expect(token, "token parsed from share URL").toMatch(/^hls_[0-9a-f]{48}$/);
|
||||
const passphrase = (
|
||||
await shareSheet.getByTestId("share-passphrase").innerText()
|
||||
).trim();
|
||||
const clinician = await openUnlockedClinician(browser, {
|
||||
token,
|
||||
passphrase,
|
||||
label: "MRT Knie",
|
||||
});
|
||||
|
||||
// The served view lists the attached document…
|
||||
await expect(
|
||||
clinician.getByRole("heading", { name: "Documents" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
clinician.locator(
|
||||
`iframe[src*="/d/${MRT_DOC_ID}"], img[src*="/d/${MRT_DOC_ID}"]`,
|
||||
),
|
||||
).toHaveCount(1);
|
||||
|
||||
// …and NOTHING from the health record: no vitals, no labs, no medications,
|
||||
// no reporting-period line. This is the privacy guarantee — sharing a
|
||||
// document must never expose a single metric.
|
||||
await expect(
|
||||
clinician.getByRole("heading", { name: "Vital signs" }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
clinician.getByRole("heading", { name: "Lab values" }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
clinician.getByRole("heading", { name: "Medications & adherence" }),
|
||||
).toHaveCount(0);
|
||||
await expect(clinician.getByText("Reporting period:")).toHaveCount(0);
|
||||
|
||||
await clinician.context().close();
|
||||
});
|
||||
|
||||
test("picker enforces the document cap at the limit", async ({ page }) => {
|
||||
|
||||
Reference in new issue
Block a user