mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
test(e2e): give the doctor-report journey its own account
Generating a report writes: the route remembers the scope and the practice name on the account row, and the export panel a later visit opens is therefore the collapsed "repeat run" state. The journey was generating on the shared fixture account, which is the account `settings-export.spec.ts` asserts the first-run state against in nine places — an expanded picker, an empty summary, a disabled button. The two files run in an unconstrained order under two workers, so the first symptom was an order-dependent red in CI; against a persistent local database the flip never cleared and the red was permanent. The journey now seeds, generates and reads back on `e2e-report-owner`, with its own cookie jar. The shared account and the whole-record fixtures also get `report_selection_json` and `last_report_practice_name` cleared on every seed, which repairs a database an earlier run already dirtied and keeps a delegate's scope choice from being left on a record owner's row. The account's date order is pinned rather than left to the locale, and its export bucket is purged beside the share-link and login buckets: the 10-per-hour ceiling is shared by every export route, keys on the actor and outlives a run by an hour, so repeated local runs would otherwise meet a 429 the spec reports as a missing artefact.
This commit is contained in:
1 parent
7b06a235e1
commit
33f5bed802
3 files changed
+117
-16
No files matched your search
@@ -26,12 +26,21 @@
|
||||
* period and simply carries no blood-pressure section. An empty report is a
|
||||
* true statement about an empty window; a truncated or unopenable file is not.
|
||||
*
|
||||
* Its own account, and not the shared one: generating a report WRITES. The
|
||||
* route remembers the scope and the practice name on the account row, so the
|
||||
* panel a later visit opens is the collapsed "repeat run" state — which is
|
||||
* exactly the state `settings-export.spec.ts` asserts the opposite of, against
|
||||
* the shared account, in nine places. See `E2E_REPORT_OWNER`.
|
||||
*
|
||||
* The fixture is seeded through the PAGE's own `fetch` under the session the
|
||||
* spec is already authenticated as — not through a second Playwright request
|
||||
* context — so the rows are written by exactly the credential that later reads
|
||||
* them back. Seeding is idempotent: a re-post of the same reading collides on
|
||||
* the measurement's natural key and answers 409, which is why `--repeat-each`
|
||||
* and a second local run both stay green.
|
||||
* them back. The ROWS are idempotent: a re-post of the same reading collides on
|
||||
* the measurement's natural key and answers 409, so `--repeat-each` and a
|
||||
* second local run write nothing twice. The export QUOTA is not idempotent —
|
||||
* every export route shares one 10-per-hour bucket keyed on the actor, and it
|
||||
* outlives the run — so `global-setup` clears this account's bucket before each
|
||||
* run rather than the file pretending the press is free.
|
||||
*
|
||||
* The delegate half of the story — a READ-level delegate cannot generate the
|
||||
* owner's report — lives in `doctor-report-delegate.spec.ts`, which needs its
|
||||
@@ -42,7 +51,7 @@ import { readFile } from "node:fs/promises";
|
||||
import type { Page, Response } from "@playwright/test";
|
||||
import { PDFParse } from "pdf-parse";
|
||||
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
import { REPORT_OWNER_STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
import { expect, test } from "./setup/test";
|
||||
|
||||
/** The allergy and the drug the report has to name. Both plain ASCII: the
|
||||
@@ -360,9 +369,9 @@ function periodSpanDays(text: string): number {
|
||||
}
|
||||
|
||||
test.describe.serial("the doctor report", () => {
|
||||
// The seeded account owns the readings, the allergy and the drug, and the
|
||||
// page's own `fetch` writes them under its session.
|
||||
test.use({ storageState: STORAGE_STATE_PATH });
|
||||
// This journey's own account owns the readings, the allergy and the drug, and
|
||||
// the page's own `fetch` writes them under its session.
|
||||
test.use({ storageState: REPORT_OWNER_STORAGE_STATE_PATH });
|
||||
|
||||
test("generates a PDF that names the period, the vitals and the medication", async ({
|
||||
page,
|
||||
|
||||
+100
-9
@@ -101,6 +101,31 @@ export const E2E_SCOPE_DELEGATE = {
|
||||
role: "USER",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* The doctor-report journey's own account.
|
||||
*
|
||||
* Generating a report WRITES: the route remembers the scope and the practice
|
||||
* name on the account row, and the panel a later visit opens is therefore the
|
||||
* "repeat run" state with the picker collapsed. `settings-export.spec.ts`
|
||||
* asserts the opposite — an expanded picker, an empty summary, a disabled
|
||||
* button — against the shared account, so a journey that generated there would
|
||||
* turn nine of its assertions red the moment the two files happened to run in
|
||||
* the wrong order, and keep them red on every later run against the same
|
||||
* database.
|
||||
*
|
||||
* Its own account also keeps the seeded readings, the emergency profile and
|
||||
* the hourly export bucket off the account sixty other specs read.
|
||||
*
|
||||
* `date_format` is pinned rather than left on AUTO: the spec reads the period
|
||||
* line out of the rendered PDF and has to know which field is the month.
|
||||
*/
|
||||
export const E2E_REPORT_OWNER = {
|
||||
email: "e2e-report-owner@healthlog.test",
|
||||
username: "e2e-report-owner",
|
||||
password: "Hd6!Tw3pV9nQ2xLb",
|
||||
role: "USER",
|
||||
} as const;
|
||||
|
||||
/** One non-personal target record per closed sharing domain. */
|
||||
export const E2E_SCOPE_RECORDS = [
|
||||
{
|
||||
@@ -261,6 +286,15 @@ export const REPORT_DELEGATE_STORAGE_STATE_PATH = resolve(
|
||||
"e2e/setup/storageStateReportDelegate.json",
|
||||
);
|
||||
|
||||
/**
|
||||
* The doctor-report journey's jar. Its own account, so what generating a report
|
||||
* stamps on the account row cannot reach the shared one — see `E2E_REPORT_OWNER`.
|
||||
*/
|
||||
export const REPORT_OWNER_STORAGE_STATE_PATH = resolve(
|
||||
process.cwd(),
|
||||
"e2e/setup/storageStateReportOwner.json",
|
||||
);
|
||||
|
||||
/**
|
||||
* The banner-geometry spec's jar, and the reason it exists is a release-shaped
|
||||
* one rather than a load-shaped one.
|
||||
@@ -368,7 +402,9 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
|
||||
insights_cached_text = NULL,
|
||||
onboarding_completed_at = EXCLUDED.onboarding_completed_at,
|
||||
onboarding_tour_completed = EXCLUDED.onboarding_tour_completed,
|
||||
module_preferences_json = EXCLUDED.module_preferences_json`,
|
||||
module_preferences_json = EXCLUDED.module_preferences_json,
|
||||
report_selection_json = NULL,
|
||||
last_report_practice_name = NULL`,
|
||||
[
|
||||
cuid(),
|
||||
E2E_USER.username,
|
||||
@@ -419,6 +455,40 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
|
||||
[E2E_GUARDIAN.username],
|
||||
);
|
||||
|
||||
// The doctor-report journey's account. Seeded like the shared one — a
|
||||
// record with a height, a date of birth and a gender, so the report has a
|
||||
// patient header to print — and with the two report columns cleared, which
|
||||
// is what puts the panel back into its first-run state on every run.
|
||||
await pool.query(
|
||||
`INSERT INTO users
|
||||
(id, username, email, password_hash, role, created_at, updated_at,
|
||||
height_cm, date_of_birth, gender, date_format,
|
||||
onboarding_completed_at, onboarding_tour_completed)
|
||||
VALUES ($1, $2, $3, $4, 'USER', $5, $5,
|
||||
180, $6, 'MALE', 'MDY',
|
||||
$5, true)
|
||||
ON CONFLICT (username) DO UPDATE SET
|
||||
email = EXCLUDED.email,
|
||||
password_hash = EXCLUDED.password_hash,
|
||||
updated_at = EXCLUDED.updated_at,
|
||||
height_cm = EXCLUDED.height_cm,
|
||||
date_of_birth = EXCLUDED.date_of_birth,
|
||||
gender = EXCLUDED.gender,
|
||||
date_format = EXCLUDED.date_format,
|
||||
onboarding_completed_at = EXCLUDED.onboarding_completed_at,
|
||||
onboarding_tour_completed = EXCLUDED.onboarding_tour_completed,
|
||||
report_selection_json = NULL,
|
||||
last_report_practice_name = NULL`,
|
||||
[
|
||||
cuid(),
|
||||
E2E_REPORT_OWNER.username,
|
||||
E2E_REPORT_OWNER.email,
|
||||
await hashPassword(E2E_REPORT_OWNER.password),
|
||||
now,
|
||||
dob,
|
||||
],
|
||||
);
|
||||
|
||||
// The scoped-record journey uses a dedicated delegate so its preseeded
|
||||
// grants cannot affect the invitation lifecycle exercised by the sharing
|
||||
// journey above. Its own module preferences are explicitly off: a target
|
||||
@@ -529,7 +599,9 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
|
||||
managed_profile_at = EXCLUDED.managed_profile_at,
|
||||
onboarding_completed_at = EXCLUDED.onboarding_completed_at,
|
||||
onboarding_tour_completed = EXCLUDED.onboarding_tour_completed,
|
||||
module_preferences_json = EXCLUDED.module_preferences_json`,
|
||||
module_preferences_json = EXCLUDED.module_preferences_json,
|
||||
report_selection_json = NULL,
|
||||
last_report_practice_name = NULL`,
|
||||
[
|
||||
cuid(),
|
||||
record.username,
|
||||
@@ -589,6 +661,20 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
|
||||
[E2E_USER.username],
|
||||
);
|
||||
|
||||
// The export bucket, for the same reason and with the same shape. Every
|
||||
// export route shares one 10-per-hour bucket keyed on the ACTOR, the
|
||||
// counter outlives a run by a full hour, and the doctor-report journey
|
||||
// spends two of it per pass — so a fourth run inside the hour would meet a
|
||||
// 429 and report a missing artefact rather than a spent quota. Only the two
|
||||
// accounts that generate a report are cleared.
|
||||
await pool.query(
|
||||
`DELETE FROM rate_limits
|
||||
WHERE key IN (
|
||||
SELECT 'export:' || id::text FROM users WHERE username = ANY($1)
|
||||
)`,
|
||||
[[E2E_REPORT_OWNER.username, E2E_SCOPE_DELEGATE.username]],
|
||||
);
|
||||
|
||||
// Opt the seed account into cycle tracking. The user is seeded as
|
||||
// `MALE`, and v1.18.0 resolves the `cycle` module through
|
||||
// `isCycleEnabled(gender, CycleProfile)` — NULL `cycleTrackingEnabled`
|
||||
@@ -658,12 +744,13 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
|
||||
WHERE key LIKE 'sharing:%'`,
|
||||
);
|
||||
|
||||
// The login bucket, for the same reason. This setup signs in TEN times now
|
||||
// (the shared jar, the owner, and one jar apiece for every spec that moves
|
||||
// a session's record selector), and the ceiling is five attempts per IP per
|
||||
// quarter-hour — so two local runs in a row would otherwise end with a 429
|
||||
// from the fixture rather than a failure from the product. Only the auth
|
||||
// surfaces' own buckets are cleared; nothing else in the table is touched.
|
||||
// The login bucket, for the same reason. This setup signs in THIRTEEN
|
||||
// times now (the shared jar, the owner, the report owner, and one jar
|
||||
// apiece for every spec that moves a session's record selector), and the
|
||||
// ceiling is five attempts per IP per quarter-hour — so two local runs in a
|
||||
// row would otherwise end with a 429 from the fixture rather than a failure
|
||||
// from the product. Only the auth surfaces' own buckets are cleared;
|
||||
// nothing else in the table is touched.
|
||||
await pool.query(`DELETE FROM rate_limits WHERE key LIKE 'auth:%'`);
|
||||
|
||||
// One marker weight per record. Re-seeded by delete-then-insert so a
|
||||
@@ -733,7 +820,7 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
|
||||
* Log one account in, clearing the login bucket first.
|
||||
*
|
||||
* The ceiling is FIVE attempts per IP per quarter-hour and this setup now
|
||||
* signs in ten times, so clearing once before the batch is no longer
|
||||
* signs in thirteen times, so clearing once before the batch is no longer
|
||||
* enough — the sixth would be answered by the fixture's own 429 rather
|
||||
* than by the product. Only the auth surfaces' buckets are touched, and
|
||||
* only between logins this setup is itself performing.
|
||||
@@ -773,6 +860,10 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
|
||||
await capture(E2E_SCOPE_DELEGATE, SEAM_BANNERS_STORAGE_STATE_PATH);
|
||||
await capture(E2E_SCOPE_DELEGATE, REPORT_DELEGATE_STORAGE_STATE_PATH);
|
||||
|
||||
// The doctor-report journey's own account, for the reason written on
|
||||
// E2E_REPORT_OWNER: generating a report writes to the account row.
|
||||
await capture(E2E_REPORT_OWNER, REPORT_OWNER_STORAGE_STATE_PATH);
|
||||
|
||||
// v1.37.0 — the guardian's jar, and only then its second factor.
|
||||
//
|
||||
// Order is the whole of it. `requireFreshMfa` refuses an account with no
|
||||
|
||||
@@ -26,5 +26,6 @@ export {
|
||||
FENCE_OFFLINE_STORAGE_STATE_PATH,
|
||||
CROSS_TAB_STORAGE_STATE_PATH,
|
||||
REPORT_DELEGATE_STORAGE_STATE_PATH,
|
||||
REPORT_OWNER_STORAGE_STATE_PATH,
|
||||
GUARDIAN_STORAGE_STATE_PATH,
|
||||
} from "./global-setup";
|
||||
Reference in new issue
Block a user