Public: anyone can read this; the crew can change it.
A test pins sign-in through another forge: a login in flight is taken once and expires, and a parent link is one per account and issuer #6802
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6798-a-test-pins-sign-in-through-another-forg → main 1 commit fork
1 file
changed +110
−0
test/federation-login.test.ts added +110
| @@ -0,0 +1,110 @@ | ||
| 1 | /** Signing in through another forge (#748): a login in flight is taken once and expires; a parent link is one per account and issuer. */ | |
| 2 | ||
| 3 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 4 | import { mkdtemp, rm, writeFile } from "node:fs/promises"; | |
| 5 | import { tmpdir } from "node:os"; | |
| 6 | import { join } from "node:path"; | |
| 7 | import { createApp, type App } from "../src/app.ts"; | |
| 8 | import { | |
| 9 | LOGIN_TTL_MS, | |
| 10 | linkParent, | |
| 11 | linkedAccount, | |
| 12 | parentIssuerOf, | |
| 13 | startLogin, | |
| 14 | takeLogin, | |
| 15 | unlinkParent, | |
| 16 | } from "../src/core/federation.ts"; | |
| 17 | import { createUser, getUserByName } from "../src/core/users.ts"; | |
| 18 | import { sha256 } from "../src/kernel/hash.ts"; | |
| 19 | import { quietLogs } from "./helpers/globals.ts"; | |
| 20 | ||
| 21 | quietLogs(); | |
| 22 | ||
| 23 | const ISSUER = "https://parent.test"; | |
| 24 | const login = { verifier: "v-1", nonce: "n-1", issuer: ISSUER, redirectUri: `${ISSUER}/cb` }; | |
| 25 | ||
| 26 | describe("federation", () => { | |
| 27 | let dataDir = ""; | |
| 28 | let app: App; | |
| 29 | const id = (name: string) => getUserByName(app.db, name)!.id; | |
| 30 | ||
| 31 | beforeAll(async () => { | |
| 32 | dataDir = await mkdtemp(join(tmpdir(), "forge-federation-")); | |
| 33 | app = createApp({ dataDir, port: 0 }); | |
| 34 | for (const username of ["alice", "bob"]) { | |
| 35 | const made = await createUser(app.db, { | |
| 36 | origin: "seed", | |
| 37 | trusted: true, | |
| 38 | username, | |
| 39 | email: `${username}@x.test`, | |
| 40 | password: "a-long-federation-passphrase", | |
| 41 | }); | |
| 42 | if (!made.ok) throw new Error(username); | |
| 43 | } | |
| 44 | }, 30_000); | |
| 45 | ||
| 46 | afterAll(async () => { | |
| 47 | app.close(); | |
| 48 | await rm(dataDir, { recursive: true, force: true }); | |
| 49 | }); | |
| 50 | ||
| 51 | test("a started login is taken once with its stored fields", () => { | |
| 52 | startLogin(app.db, "state-a", { ...login, linkUser: "u1" }, 1000); | |
| 53 | expect(takeLogin(app.db, "state-a", 2000)).toEqual({ ...login, linkUser: "u1" }); | |
| 54 | expect(takeLogin(app.db, "state-a", 2000)).toBeUndefined(); | |
| 55 | }); | |
| 56 | ||
| 57 | test("a sign-in carries no linkUser", () => { | |
| 58 | startLogin(app.db, "state-b", login, 1000); | |
| 59 | expect(takeLogin(app.db, "state-b", 1001)).toEqual(login); | |
| 60 | }); | |
| 61 | ||
| 62 | test("a login taken after its lifetime is undefined, and an unknown state too", () => { | |
| 63 | startLogin(app.db, "state-c", login, 1000); | |
| 64 | expect(takeLogin(app.db, "state-c", 1000 + LOGIN_TTL_MS)).toBeUndefined(); | |
| 65 | startLogin(app.db, "state-d", login, 1000); | |
| 66 | expect(takeLogin(app.db, "state-d", 1000 + LOGIN_TTL_MS - 1)).toEqual(login); | |
| 67 | expect(takeLogin(app.db, "never-started", 1000)).toBeUndefined(); | |
| 68 | }); | |
| 69 | ||
| 70 | test("only the hash of the state is stored", () => { | |
| 71 | startLogin(app.db, "raw-state-secret", login, 5000); | |
| 72 | const rows = app.db.query("SELECT * FROM rp_login").all() as { state_hash: string }[]; | |
| 73 | expect(rows.map((r) => r.state_hash)).toContain(sha256("raw-state-secret")); | |
| 74 | expect(JSON.stringify(rows)).not.toContain("raw-state-secret"); | |
| 75 | takeLogin(app.db, "raw-state-secret", 5000); | |
| 76 | }); | |
| 77 | ||
| 78 | test("a linked parent identity finds its account and unlinking removes it", () => { | |
| 79 | expect(linkedAccount(app.db, ISSUER, "sub-1")).toBeUndefined(); | |
| 80 | expect(linkParent(app.db, id("alice"), ISSUER, "sub-1")).toEqual({ ok: true, value: "linked" }); | |
| 81 | expect(linkedAccount(app.db, ISSUER, "sub-1")?.username).toBe("alice"); | |
| 82 | expect(linkedAccount(app.db, "https://other.test", "sub-1")).toBeUndefined(); | |
| 83 | expect(unlinkParent(app.db, id("alice"), ISSUER)).toEqual({ ok: true, value: "unlinked" }); | |
| 84 | expect(linkedAccount(app.db, ISSUER, "sub-1")).toBeUndefined(); | |
| 85 | expect(unlinkParent(app.db, id("alice"), ISSUER)).toEqual({ ok: true, value: "none" }); | |
| 86 | }); | |
| 87 | ||
| 88 | test("an issuer and sub belong to one account; an account holds one sub per issuer", () => { | |
| 89 | linkParent(app.db, id("alice"), ISSUER, "sub-2"); | |
| 90 | expect(linkParent(app.db, id("alice"), ISSUER, "sub-2")).toEqual({ ok: true, value: "linked" }); | |
| 91 | expect(linkParent(app.db, id("bob"), ISSUER, "sub-2")).toEqual({ ok: false, error: "identity_taken" }); | |
| 92 | expect(linkParent(app.db, id("alice"), ISSUER, "sub-3")).toEqual({ ok: false, error: "already_linked" }); | |
| 93 | expect(linkedAccount(app.db, ISSUER, "sub-2")?.username).toBe("alice"); | |
| 94 | expect(linkedAccount(app.db, ISSUER, "sub-3")).toBeUndefined(); | |
| 95 | unlinkParent(app.db, id("alice"), ISSUER); | |
| 96 | }); | |
| 97 | ||
| 98 | test("parentIssuerOf reads the configured parent and is undefined without one", async () => { | |
| 99 | expect(parentIssuerOf({ dataDir, parentUrl: "https://parent.test/x" })).toBe(ISSUER); | |
| 100 | expect(parentIssuerOf({ dataDir, parentUrl: "http://parent.test" })).toBeUndefined(); | |
| 101 | expect(parentIssuerOf({ dataDir })).toBeUndefined(); | |
| 102 | const recorded = await mkdtemp(join(tmpdir(), "forge-federation-rec-")); | |
| 103 | try { | |
| 104 | await writeFile(join(recorded, "parent.json"), JSON.stringify({ upstream: "https://spawn.test" })); | |
| 105 | expect(parentIssuerOf({ dataDir: recorded })).toBe("https://spawn.test"); | |
| 106 | } finally { | |
| 107 | await rm(recorded, { recursive: true, force: true }); | |
| 108 | } | |
| 109 | }); | |
| 110 | }); |
new above
offline