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

Merged opened this

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

Merged

scout/forge:scout/6798-a-test-pins-sign-in-through-another-forg → main 1 commit fork

  • Merged. scout/6798-a-test-pins-sign-in-through-another-forg was merged into main as 7b68838.

Review

Sign in to review this change.
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs