import { describe, expect, test } from "bun:test"; import { FailureLimiter, codeKey, hashSecret, newEnrolmentCode, newInviteToken, normaliseCode, } from "../src/onboarding/tokens"; describe("enrolment codes", () => { test("have the shape that was promised and do not repeat", () => { const codes = new Set(Array.from({ length: 200 }, newEnrolmentCode)); expect(codes.size).toBe(200); for (const code of codes) expect(code).toMatch(/^[0-9A-HJKMNP-TV-Z]{5}-[0-9A-HJKMNP-TV-Z]{5}$/); }); test("a code typed carelessly still finds the one that was issued", () => { const issued = "AB0C1-DEF23"; for (const typed of ["ab0c1-def23", "AB0C1DEF23", " ab0c1 def23 ", "ABOC1-DEF23", "AB0Cl-DEF23", "ABOCI-DEF23"]) { expect(codeKey(typed)).toBe(codeKey(issued)); } expect(codeKey("AB0C1-DEF24")).not.toBe(codeKey(issued)); }); test("normalising is idempotent", () => { expect(normaliseCode(normaliseCode("ab-oc1"))).toBe(normaliseCode("ab-oc1")); }); }); describe("invitation tokens", () => { test("are long, URL-safe and unique, and only their hash is kept", () => { const a = newInviteToken(); expect(a).toMatch(/^[A-Za-z0-9_-]{43}$/); expect(newInviteToken()).not.toBe(a); expect(hashSecret(a)).toMatch(/^[0-9a-f]{64}$/); expect(hashSecret(a)).not.toContain(a); }); }); describe("FailureLimiter", () => { test("blocks after too many failures and forgets them after the window", () => { const limiter = new FailureLimiter(3, 1000); for (let i = 0; i < 3; i++) { expect(limiter.blocked("ip", i)).toBe(false); limiter.fail("ip", i); } expect(limiter.blocked("ip", 10)).toBe(true); expect(limiter.blocked("other", 10)).toBe(false); expect(limiter.blocked("ip", 2000)).toBe(false); }); });