mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
The journey's isolation went one way only. `test.describe.configure` orders the tests inside the file; the suite is fullyParallel with two workers, so everything else runs beside it — and its own trigger, the admin reminder sweep, walked the whole instance and dispatched for every account that had an overdue dose. With the mail env pointed at the local responder, that would have started sending real messages for accounts this file has never heard of, and the exact-equality assertion on the accepted recipients would have been reading them. The sweep now names the account it then counts, and the comments that defended the opposite direction say what the exposure actually is. Its own server, too, and for two reasons. The `SMTP_*` vars decide whether the Email card renders at all, so on the shared server they put a card the axe scan and the card-anatomy sweep had never measured onto `/settings/integrations` for every spec that opens the route. And unset, `HEALTHLOG_PROCESS_TYPE` starts the pg-boss reminder scheduler inside the process serving requests, whose every-fifteen-minutes tick either mints the missed-dose row that makes the sweep skip its own dispatch or dispatches beside it. A third server carries both: mail configured, `web`, and only this spec talks to it. The shared server keeps `all` on measured grounds rather than taste. Worker status is per-process and in-memory, so a `web` process reports the worker stopped, `/admin` renders that row destructive-red, and axe fails it at 3.97:1 against the muted tile — a real contrast defect any operator running the documented web/worker split already sees, and not this branch's to fix. That limit is written where the server is defined. Also here: the stub draws its port per run instead of pinning one, and says so by name when the port is taken; teardown ends its sockets and survives a setup that never got as far as starting it; the ledger reset stops deleting rows whose subject is this account but whose recipient is somebody else, and clears intake events explicitly rather than through a cascade; the attempt poll refuses a window larger than the twenty rows the diagnostic returns; and the header names the precondition the file cannot enforce and the internal reasons it pins on purpose. Verified against a production build and a throwaway Postgres: the journey three times over at one worker, then the a11y and settings geometry specs at two. Both assertions broken once to see them fail.
117 lines
4.3 KiB
TypeScript
117 lines
4.3 KiB
TypeScript
/**
|
|
* Account hygiene for the notification-dispatch journey.
|
|
*
|
|
* Every verdict in that journey is a COUNT over one account's delivery ledger
|
|
* inside a window the test opens itself — one email attempt, no ntfy attempt,
|
|
* one APNs skip carrying a named reason. A channel row, a paired device, a
|
|
* medication or a preference blob left behind by an earlier run would put a
|
|
* second dispatch decision inside that window, and the count would be reading
|
|
* the previous run rather than this one. `--repeat-each` is the case that
|
|
* makes it certain rather than likely.
|
|
*
|
|
* So the account is put back to "nothing configured" before every test:
|
|
* no channels, no devices, no ledger, no medications, default preferences and
|
|
* the default timezone. Deletes rather than disables — a disabled channel row
|
|
* is itself one of the journey's subjects, so leaving one behind would seed
|
|
* the answer to a question the test asks.
|
|
*
|
|
* Reaching for Postgres directly is the channel `global-setup.ts` already
|
|
* uses to seed and to clear buckets, and every statement here is parameter
|
|
* bound on the account's own id.
|
|
*/
|
|
import pg from "pg";
|
|
|
|
import { E2E_NOTIFY } from "./global-setup";
|
|
|
|
async function withPool<T>(fn: (pool: pg.Pool) => Promise<T>): Promise<T> {
|
|
const url = process.env.DATABASE_URL;
|
|
if (!url) throw new Error("[notification-fixture] DATABASE_URL is not set");
|
|
const pool = new pg.Pool({ connectionString: url });
|
|
try {
|
|
return await fn(pool);
|
|
} finally {
|
|
await pool.end();
|
|
}
|
|
}
|
|
|
|
/** The notification account's id, or a loud failure if the seed never ran. */
|
|
export async function notificationAccountId(): Promise<string> {
|
|
return withPool(async (pool) => {
|
|
const res = await pool.query<{ id: string }>(
|
|
"SELECT id FROM users WHERE username = $1",
|
|
[E2E_NOTIFY.username],
|
|
);
|
|
const id = res.rows[0]?.id;
|
|
if (!id) {
|
|
throw new Error(
|
|
"[notification-fixture] e2e-notify is not seeded — global-setup must run first",
|
|
);
|
|
}
|
|
return id;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Put the notification account back to a freshly-registered state.
|
|
*
|
|
* The rate-limit buckets go too. The card's own test control allows five
|
|
* probes per five minutes and device registration twenty per quarter-hour —
|
|
* production numbers, and both below what three repeats of this journey
|
|
* spend. A run that ended on the fixture's 429 would say nothing about the
|
|
* dispatcher.
|
|
*/
|
|
export async function resetNotificationFixture(): Promise<void> {
|
|
await withPool(async (pool) => {
|
|
const res = await pool.query<{ id: string }>(
|
|
"SELECT id FROM users WHERE username = $1",
|
|
[E2E_NOTIFY.username],
|
|
);
|
|
const userId = res.rows[0]?.id;
|
|
if (!userId) {
|
|
throw new Error(
|
|
"[notification-fixture] e2e-notify is not seeded — global-setup must run first",
|
|
);
|
|
}
|
|
|
|
await pool.query("DELETE FROM notification_channels WHERE user_id = $1", [
|
|
userId,
|
|
]);
|
|
await pool.query("DELETE FROM devices WHERE user_id = $1", [userId]);
|
|
// `user_id` and `recipient_user_id`, and deliberately NOT
|
|
// `record_user_id`: that column is the health-record SUBJECT, so a
|
|
// delivery ABOUT this account sent TO a guardian carries it while the
|
|
// recipient is somebody else. Deleting on it would take a row out of
|
|
// another account's ledger. The diagnostic this journey reads filters on
|
|
// the recipient, so the two agree.
|
|
await pool.query(
|
|
`DELETE FROM push_attempts
|
|
WHERE user_id = $1 OR recipient_user_id = $1`,
|
|
[userId],
|
|
);
|
|
// The medication delete cascades to its intake events, but a stale event
|
|
// is what makes the sweep decide a dose was already taken and skip its
|
|
// dispatch, so the statement is written out rather than inherited from a
|
|
// foreign key a future fixture might stop relying on.
|
|
await pool.query(
|
|
"DELETE FROM medication_intake_events WHERE user_id = $1",
|
|
[userId],
|
|
);
|
|
await pool.query("DELETE FROM medications WHERE user_id = $1", [userId]);
|
|
await pool.query(
|
|
`UPDATE users
|
|
SET notification_prefs = NULL,
|
|
timezone = DEFAULT
|
|
WHERE id = $1`,
|
|
[userId],
|
|
);
|
|
await pool.query("DELETE FROM rate_limits WHERE key = ANY($1)", [
|
|
[
|
|
`email-test:${userId}`,
|
|
`ntfy-test:${userId}`,
|
|
`devices:register:${userId}`,
|
|
`notification-prefs:patch:${userId}`,
|
|
],
|
|
]);
|
|
});
|
|
}
|