import { describe, expect, it } from "@suaveplan/testing/runner"; import { AUTH_ROLLED_CODES, AuthRolledError, } from "../error-codes/error-codes.js"; import { DEFAULT_COOKIE_NAME, DEFAULT_MIN_PASSWORD_STRENGTH, DEFAULT_REFRESH_TTL_SEC, DEFAULT_ROTATION_PERIOD_MS, DEFAULT_ROUTE_PREFIX, DEFAULT_SESSION_TTL_SEC, parseRolledAuthOptions, } from "./options.js"; const baseRaw = (): Record => ({ store: { __mark: "store" }, keys: [ { kid: "k1", privateJwk: { kty: "RSA" }, publicJwk: { kty: "RSA" }, active: true, }, ], issuer: "https://api.example.com", audience: "front", }); describe("parseRolledAuthOptions", () => { it("accepts a minimal valid options bag", { timeout: 5000 }, () => { const opts = parseRolledAuthOptions(baseRaw()); expect(opts.issuer).toBe("https://api.example.com"); expect(opts.audience).toBe("front"); expect(opts.keys).toHaveLength(1); }); it("throws DATABASE_REQUIRED when raw is not an object", { timeout: 5000, }, () => { for (const bad of [null, undefined, 42, "hi", [1]]) { try { parseRolledAuthOptions(bad); throw new Error("did not throw"); } catch (err) { expect(err).toBeInstanceOf(AuthRolledError); expect((err as AuthRolledError).code).toBe( AUTH_ROLLED_CODES.DATABASE_REQUIRED ); } } }); it("throws DATABASE_REQUIRED when store is missing", { timeout: 5000, }, () => { const raw = baseRaw(); delete raw["store"]; try { parseRolledAuthOptions(raw); throw new Error("did not throw"); } catch (err) { expect(err).toBeInstanceOf(AuthRolledError); expect((err as AuthRolledError).code).toBe( AUTH_ROLLED_CODES.DATABASE_REQUIRED ); } }); it("throws DATABASE_REQUIRED when store is null", { timeout: 5000 }, () => { const raw = baseRaw(); raw["store"] = null; expect(() => parseRolledAuthOptions(raw)).toThrow(AuthRolledError); }); it("throws SIGNING_KEY_NOT_CONFIGURED on schema failure (zero keys)", { timeout: 5000, }, () => { const raw = baseRaw(); raw["keys"] = []; try { parseRolledAuthOptions(raw); throw new Error("did not throw"); } catch (err) { expect(err).toBeInstanceOf(AuthRolledError); expect((err as AuthRolledError).code).toBe( AUTH_ROLLED_CODES.SIGNING_KEY_NOT_CONFIGURED ); } }); it("throws SIGNING_KEY_NOT_CONFIGURED when more than one key is active", { timeout: 5000, }, () => { const raw = baseRaw(); raw["keys"] = [ { kid: "k1", privateJwk: { kty: "RSA" }, publicJwk: { kty: "RSA" }, active: true, }, { kid: "k2", privateJwk: { kty: "RSA" }, publicJwk: { kty: "RSA" }, active: true, }, ]; expect(() => parseRolledAuthOptions(raw)).toThrow(AuthRolledError); }); it("throws SIGNING_KEY_NOT_CONFIGURED when active key is revoked", { timeout: 5000, }, () => { const raw = baseRaw(); raw["keys"] = [ { kid: "k1", privateJwk: { kty: "RSA" }, publicJwk: { kty: "RSA" }, active: true, revokedAt: 1, }, ]; expect(() => parseRolledAuthOptions(raw)).toThrow(AuthRolledError); }); it("rejects non-slash routePrefix", { timeout: 5000 }, () => { const raw = baseRaw(); raw["routePrefix"] = "auth"; expect(() => parseRolledAuthOptions(raw)).toThrow(AuthRolledError); }); it("rejects unknown keys (strictObject)", { timeout: 5000 }, () => { const raw = baseRaw(); (raw as Record)["unknownField"] = 1; expect(() => parseRolledAuthOptions(raw)).toThrow(AuthRolledError); }); it("exposes named defaults", { timeout: 5000 }, () => { expect(DEFAULT_COOKIE_NAME).toBe("suaveplan.session"); expect(DEFAULT_ROUTE_PREFIX).toBe("/auth"); expect(DEFAULT_SESSION_TTL_SEC).toBe(3600); expect(DEFAULT_REFRESH_TTL_SEC).toBe(2592000); expect(DEFAULT_ROTATION_PERIOD_MS).toBe(86_400_000); expect(DEFAULT_MIN_PASSWORD_STRENGTH).toBe(3); }); });