mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
- bound profile creation at ten an hour per account, the ceiling its guardian sibling has carried since it shipped - distinguish a failed roster read from a slow one, with the way back - cover the three caller states the roster read has to refuse: an unaccepted invitation, an expired grant, and MANAGE held elsewhere - cover what deletion does to a guardian standing inside the record
112 lines
4.2 KiB
TypeScript
112 lines
4.2 KiB
TypeScript
/**
|
|
* v1.37.0 — the step-up the managed-profile journey needs, and nothing else.
|
|
*
|
|
* ## Why a fixture exists at all
|
|
*
|
|
* Every route in the managed-profile family resolves
|
|
* `requireFreshMfa(MFA_STEP_UP_MAX_AGE_SECONDS)`. That gate has two halves:
|
|
* the account must have a second factor enrolled, and the SESSION must carry a
|
|
* `mfaVerifiedAt` newer than five minutes. `global-setup.ts` supplies the first
|
|
* half once; the second cannot be supplied once, because a full suite run takes
|
|
* longer than the window and the journey would be gated on how long the specs
|
|
* ahead of it took.
|
|
*
|
|
* So the freshness stamp is refreshed immediately before the journey acts. It
|
|
* is not a bypass and it does not weaken the route: the route still runs its
|
|
* gate, the session still has to carry a stamp, and a session without one is
|
|
* still refused. What it simulates is the person having just proved their
|
|
* factor — which is the state the product requires and the only state in which
|
|
* this surface is usable at all.
|
|
*
|
|
* Seeded through `pg`, the same transport `global-setup.ts` and
|
|
* `vault-fixture.ts` use, because there is no product endpoint that says "I
|
|
* proved a factor" without a real factor to prove.
|
|
*/
|
|
import pg from "pg";
|
|
|
|
import { E2E_GUARDIAN } from "./global-setup";
|
|
|
|
function connect(): pg.Pool {
|
|
const url = process.env.DATABASE_URL;
|
|
if (!url) {
|
|
throw new Error("[managed-profile-fixture] DATABASE_URL is not set");
|
|
}
|
|
return new pg.Pool({ connectionString: url });
|
|
}
|
|
|
|
/**
|
|
* Make the guardian's step-up fresh, and clear anything an earlier run left.
|
|
*
|
|
* Idempotent, and safe to call from a `beforeEach`: both statements are
|
|
* unconditional writes scoped to this one fixture account.
|
|
*/
|
|
export async function refreshGuardianStepUp(): Promise<void> {
|
|
const pool = connect();
|
|
try {
|
|
const stamped = await pool.query(
|
|
`UPDATE sessions SET mfa_verified_at = NOW()
|
|
WHERE user_id = (SELECT id FROM users WHERE username = $1)`,
|
|
[E2E_GUARDIAN.username],
|
|
);
|
|
// A zero-row update means the jar this journey runs under points at a
|
|
// session that is not there, and every act below would be refused for a
|
|
// reason that has nothing to do with what it tests. Fail loudly here.
|
|
if ((stamped.rowCount ?? 0) === 0) {
|
|
throw new Error(
|
|
`[managed-profile-fixture] no session for ${E2E_GUARDIAN.username}`,
|
|
);
|
|
}
|
|
// And the factor itself, in case a spec ran before global setup stamped it.
|
|
await pool.query(
|
|
`UPDATE users SET totp_confirmed_at = COALESCE(totp_confirmed_at, NOW())
|
|
WHERE username = $1`,
|
|
[E2E_GUARDIAN.username],
|
|
);
|
|
// The guardian-invitation bucket, for the same reason `global-setup.ts`
|
|
// clears the auth bucket between its own logins: the ceiling is ten
|
|
// invitations an hour per caller (and ten creations), and this journey spends
|
|
// one of each every time it
|
|
// runs. On CI that never matters; locally, the eleventh run inside an hour
|
|
// is answered by the product's own 429 rather than by what the test is
|
|
// about, and the failure reads as a broken invitation form. Only this one
|
|
// fixture account's bucket is touched.
|
|
await pool.query(
|
|
`DELETE FROM rate_limits
|
|
WHERE key IN (
|
|
'managed-profile:guardian-invite:' ||
|
|
(SELECT id FROM users WHERE username = $1),
|
|
'managed-profile:create:' ||
|
|
(SELECT id FROM users WHERE username = $1)
|
|
)`,
|
|
[E2E_GUARDIAN.username],
|
|
);
|
|
} finally {
|
|
await pool.end();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove every managed profile this account looks after.
|
|
*
|
|
* A managed profile is a real account row, so a journey that created one and
|
|
* left it behind makes the next run's roster assertions read the wrong record.
|
|
* Deleting the profile takes its grants with it.
|
|
*/
|
|
export async function clearGuardianProfiles(): Promise<void> {
|
|
const pool = connect();
|
|
try {
|
|
await pool.query(
|
|
`DELETE FROM users
|
|
WHERE managed_profile_at IS NOT NULL
|
|
AND id IN (
|
|
SELECT g.grantor_id FROM account_grants g
|
|
JOIN users u ON u.id = g.grantee_id
|
|
WHERE u.username = $1
|
|
)`,
|
|
[E2E_GUARDIAN.username],
|
|
);
|
|
} finally {
|
|
await pool.end();
|
|
}
|
|
}
|