Public: anyone can read this; the crew can change it.

"refused while any admin has a working credential > the stranded admin itself, or another admin, has sign-in with the parent" passes every time on a loaded check machine #5227

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/5206-refused-while-any-admin-has-a-working-cr → main 2 commits fork 1 thread · 1 unresolved

  • Merged. scout/5206-refused-while-any-admin-has-a-working-cr was merged into main as de1660e.

Review

Sign in to review this change.
1 file changed +24 −14
test/first-admin-recovery.test.ts +24 −14
@@ -9,7 +9,7 @@
99 * refused while any admin can still sign in.
1010 */
1111
12 import { afterAll, describe, expect, spyOn, test } from "bun:test";
12 import { afterAll, describe, expect, setDefaultTimeout, spyOn, test } from "bun:test";
1313 import { mkdtemp, rm } from "node:fs/promises";
1414 import { tmpdir } from "node:os";
1515 import { join } from "node:path";
@@ -24,17 +24,25 @@ import { quietLogs } from "./helpers/globals.ts";
2424
2525 quietLogs();
2626
27 // Each test boots a database and, for the HTTP ones, a listener. That is tens of
28 // milliseconds alone but passed 5 s on a check machine running many suites at once
29 // (seen: "timed out after 5000ms"), so the budget is for the boot, not the code.
30 setDefaultTimeout(30_000);
31
2732 const dirs: string[] = [];
2833 afterAll(async () => {
2934 for (const d of dirs) await rm(d, { recursive: true, force: true });
3035 });
outdated

Pinning the clock here only helps if the clocks can drift by about an hour, and they cannot. The failure that was never reproduced is not explained by it. Look for a timeout or shared state instead.

3136
32 const past = () => new Date(Date.now() - 86_400_000).toISOString();
33 const future = () => new Date(Date.now() + 3_600_000).toISOString();
37 // One instant for the file: expiries are built from it and the CLI is asked at it,
38 // so a slow machine cannot move a "future" expiry into the past between the two.
39 const NOW = Date.now();
40 const past = () => new Date(NOW - 86_400_000).toISOString();
41 const future = () => new Date(NOW + 3_600_000).toISOString();
3442 const CODE = /"code": "([A-Za-z0-9_-]{43})"|#([A-Za-z0-9_-]{43})|\b([A-Za-z0-9_-]{43})\b/;
3543
3644 /** A forge with one admin and no credential at all: the shape a lapsed headless admin leaves. */
37 async function stranded(name = "opsbot"): Promise<{ dir: string; app: App; servers: Servers; userId: string }> {
45 async function stranded(name = "opsbot", listen = true): Promise<{ dir: string; app: App; servers: Servers; userId: string }> {
3846 const dir = await mkdtemp(join(tmpdir(), "forge-first-admin-recovery-"));
3947 dirs.push(dir);
4048 const app = createApp({ dataDir: dir, port: 0, passkeyOnlySignup: true });
@@ -42,7 +50,9 @@ async function stranded(name = "opsbot"): Promise<{ dir: string; app: App; serve
4250 if (!made.ok) throw new Error("user");
4351 // Its token from the first-admin redemption, lapsed.
4452 createToken(app.db, made.value.id, "first-admin", "admin", { expiresAt: past() });
45 return { dir, app, servers: await serve(app), userId: made.value.id };
53 // The CLI reads the database file alone; only the HTTP tests need a listener.
54 const servers = listen ? await serve(app) : { url: "", stop: () => {} };
55 return { dir, app, servers: servers as Servers, userId: made.value.id };
4656 }
4757
4858 const post = (base: string, path: string, body: Record<string, unknown>, headers: Record<string, string> = {}) =>
@@ -259,20 +269,20 @@ describe("refused while any admin has a working credential", () => {
259269
260270 for (const [what, give] of Object.entries(credentials)) {
261271 test(`the stranded admin itself, or another admin, has ${what}`, async () => {
262 const { dir, app, servers, userId } = await stranded();
272 const { dir, app, servers, userId } = await stranded("opsbot", false);
263273 try {
264274 give(app, userId);
265 expect(firstAdminCli([], { FORGE_DATA: dir }).exit).toBe(1);
275 expect(firstAdminCli([], { FORGE_DATA: dir }, NOW).exit).toBe(1);
266276 } finally {
267277 servers.stop();
268278 app.close();
269279 }
270 const other = await stranded();
280 const other = await stranded("opsbot", false);
271281 try {
272282 const second = await createUser(other.app.db, { origin: "seed", trusted: true, username: "second", email: "[email protected]", isAdmin: true });
273283 if (!second.ok) throw new Error("second");
274284 give(other.app, second.value.id);
275 const refused = firstAdminCli([], { FORGE_DATA: other.dir });
285 const refused = firstAdminCli([], { FORGE_DATA: other.dir }, NOW);
276286 expect(refused.exit).toBe(1);
277287 expect(refused.out).toHaveLength(0);
278288 } finally {
@@ -283,12 +293,12 @@ describe("refused while any admin has a working credential", () => {
283293 }
284294
285295 test("an expired session and an expired token are not working credentials", async () => {
286 const { dir, app, servers, userId } = await stranded();
296 const { dir, app, servers, userId } = await stranded("opsbot", false);
287297 try {
288298 app.db
289299 .query("INSERT INTO session (token_hash, user_id, expires_at, id) VALUES (?1, ?2, ?3, ?4)")
290300 .run(sha256("old"), userId, past(), "sess-old");
291 expect(firstAdminCli([], { FORGE_DATA: dir }).exit).toBe(0);
301 expect(firstAdminCli([], { FORGE_DATA: dir }, NOW).exit).toBe(0);
292302 } finally {
293303 servers.stop();
294304 app.close();
@@ -298,7 +308,7 @@ describe("refused while any admin has a working credential", () => {
298308 test("a credential that appears after the code is minted kills it", async () => {
299309 const { dir, app, servers, userId } = await stranded();
300310 try {
301 const code = codeOf(firstAdminCli([], { FORGE_DATA: dir }).out);
311 const code = codeOf(firstAdminCli([], { FORGE_DATA: dir }, NOW).out);
302312 credentials["a passkey"]!(app, userId);
303313 expect((await recover(servers.url, { code, scope: "rw" })).status).toBe(403);
304314 } finally {
@@ -315,7 +325,7 @@ describe("which admin is recovered", () => {
315325 const b = await createUser(app.db, { origin: "seed", trusted: true, username: "builder", email: "[email protected]", isAdmin: true });
316326 const plain = await createUser(app.db, { origin: "seed", trusted: true, username: "plain", email: "[email protected]" });
317327 if (!b.ok || !plain.ok) throw new Error("users");
318 const ambiguous = firstAdminCli([], { FORGE_DATA: dir });
328 const ambiguous = firstAdminCli([], { FORGE_DATA: dir }, NOW);
319329 expect(ambiguous.exit).toBe(1);
320330 expect(ambiguous.err.join("\n")).toContain("--user");
321331 expect(firstAdminCli(["--user", "plain"], { FORGE_DATA: dir }).exit).toBe(1);
@@ -334,7 +344,7 @@ describe("which admin is recovered", () => {
334344 const { dir, app, servers, userId } = await stranded();
335345 try {
336346 app.db.query("UPDATE user SET suspended_at = ?1 WHERE id = ?2").run(new Date().toISOString(), userId);
337 expect(firstAdminCli([], { FORGE_DATA: dir }).exit).toBe(1);
347 expect(firstAdminCli([], { FORGE_DATA: dir }, NOW).exit).toBe(1);
338348 } finally {
339349 servers.stop();
340350 app.close();

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs