import { describe, expect, test } from "bun:test"; import { MemoryAuthStore, hashToken, normalizeApiKey, tokenHashesEqual, } from "../../server/auth"; import { CAPABILITIES, capabilitiesForRoles, hasCapability, } from "../../server/authorization"; const expiresAt = "2026-09-03T00:00:00.000Z"; describe("authorization", () => { test("grants named capabilities through deny-by-default roles", () => { expect([...capabilitiesForRoles(["viewer"])]).toEqual(["kubernetes:read"]); expect(hasCapability(["operator"], "kubernetes:write")).toBe(true); expect(hasCapability(["operator"], "users:read")).toBe(false); expect([...capabilitiesForRoles(["unknown"])]).toEqual([]); expect([...capabilitiesForRoles(["admin"])]).toEqual([...CAPABILITIES]); }); }); describe("memory auth store", () => { test("keeps non-expiring keys active and preserves finite expiry validation", async () => { const store = new MemoryAuthStore(); await store.putUser({ username: "alice", passwordHash: "hash", roles: ["viewer"], }); const key = { id: "never-expiring-key-123", tokenHash: hashToken("never"), username: "alice", capabilities: ["kubernetes:read" as const], }; await store.createApiKey(key); expect(await store.getApiKey(key.tokenHash)).toMatchObject(key); expect((await store.listApiKeys("alice"))[0]?.expiresAt).toBeUndefined(); await store.createApiKey({ ...key, id: "finite-expiry-key-123", tokenHash: hashToken("finite"), expiresAt: "2020-01-01T00:00:00.000Z", }); expect(await store.deleteExpiredApiKeys()).toBe(1); expect(await store.getApiKey(key.tokenHash)).toMatchObject(key); expect( normalizeApiKey({ ...key, expiresAt: undefined }).expiresAt, ).toBeUndefined(); for (const expiresAt of ["none", "not-a-date", "2026-09-03", ""]) { expect(() => normalizeApiKey({ ...key, expiresAt })).toThrow( "ISO timestamp", ); } await expect( store.createApiKey({ ...key, id: "duplicate-key-id-123", tokenHash: hashToken("duplicate"), }), ).resolves.toBeUndefined(); expect(await store.revokeApiKey("bob", key.id)).toBe(false); expect(await store.revokeApiKey("alice", key.id)).toBe(true); expect(await store.getApiKey(key.tokenHash)).toBeUndefined(); await expect( store.createApiKey({ ...key, username: "missing" }), ).rejects.toThrow("not active"); await store.updateUser("alice", { disabled: true }); await expect(store.createApiKey({ ...key })).rejects.toThrow("not active"); }); test("maintains the API-key hash index across key mutations", async () => { const store = new MemoryAuthStore(); await store.putUser({ username: "alice", passwordHash: "hash", roles: ["viewer"], }); const activeHash = hashToken("active-api-key"); const expiredHash = hashToken("expired-api-key"); const activeKey = { id: "active-key-id-1234", tokenHash: activeHash, username: "alice", capabilities: ["kubernetes:read" as (typeof CAPABILITIES)[number]], expiresAt: new Date(Date.now() + 60_000).toISOString(), }; await store.createApiKey(activeKey); expect(store.apiKeysByTokenHash.get(activeHash)).toBe( store.apiKeys.get(activeKey.id), ); expect(await store.getApiKey(activeHash)).toEqual( store.apiKeys.get(activeKey.id), ); expect(await store.revokeApiKey("alice", activeKey.id)).toBe(true); expect(store.apiKeysByTokenHash.has(activeHash)).toBe(false); expect(await store.getApiKey(activeHash)).toBeUndefined(); const expiredKey = { ...activeKey, id: "expired-key-id-1234", tokenHash: expiredHash, expiresAt: "2020-01-01T00:00:00.000Z", }; await store.createApiKey(expiredKey); expect(store.apiKeysByTokenHash.get(expiredHash)).toBe( store.apiKeys.get(expiredKey.id), ); expect(await store.deleteExpiredApiKeys()).toBe(1); expect(store.apiKeysByTokenHash.has(expiredHash)).toBe(false); await store.createApiKey(activeKey); expect(await store.deleteUser("alice")).toBe(true); expect(store.apiKeysByTokenHash.has(activeHash)).toBe(false); expect(await store.getApiKey(activeHash)).toBeUndefined(); }); test("creates, lists, updates, and deletes users", async () => { const store = new MemoryAuthStore(); const bob = await store.createUser({ username: "bob", passwordHash: "hash-b", roles: ["viewer"], }); await store.createUser({ username: "alice", passwordHash: "hash-a", roles: ["operator"], }); expect(bob.authVersion).toBe(1); expect((await store.listUsers()).map((user) => user.username)).toEqual([ "alice", "bob", ]); await expect(store.createUser({ ...bob })).rejects.toThrow( "User already exists", ); const updated = await store.updateUser("bob", { roles: ["admin"], disabled: true, }); expect(updated).toMatchObject({ roles: ["admin"], disabled: true, authVersion: 2, }); expect(await store.updateUser("missing", {})).toBeUndefined(); expect(await store.deleteUser("bob")).toBe(true); expect(await store.deleteUser("bob")).toBe(false); }); test("references user authVersion and ignores legacy role snapshots", async () => { const store = new MemoryAuthStore(); await store.putUser({ username: "alice", passwordHash: "hash", roles: ["viewer"], }); const tokenHash = hashToken("token"); await store.putSession({ tokenHash, username: "alice", roles: ["admin"], expiresAt, }); expect(store.sessions.get(tokenHash)).toEqual({ tokenHash, username: "alice", authVersion: 1, expiresAt, }); await store.updateUser("alice", { roles: ["operator"] }); expect(await store.getSession(tokenHash)).toBeUndefined(); await expect( store.putSession({ tokenHash: hashToken("stale"), username: "alice", authVersion: 1, expiresAt, }), ).rejects.toThrow("stale"); }); test("revokes user sessions and cleans up expired sessions", async () => { const store = new MemoryAuthStore(); for (const username of ["alice", "bob"]) { await store.putUser({ username, passwordHash: "hash", roles: ["viewer"], }); } await store.putSession({ tokenHash: hashToken("alice-expired"), username: "alice", authVersion: 1, expiresAt: "2026-09-01T00:00:00.000Z", }); await store.putSession({ tokenHash: hashToken("alice-active"), username: "alice", authVersion: 1, expiresAt, }); await store.putSession({ tokenHash: hashToken("bob-expired"), username: "bob", authVersion: 1, expiresAt: "2026-09-01T00:00:00.000Z", }); expect( await store.listExpiredSessions(Date.parse("2026-09-02T00:00:00.000Z")), ).toHaveLength(2); expect( await store.deleteExpiredSessions(Date.parse("2026-09-02T00:00:00.000Z")), ).toBe(2); expect(await store.revokeUserSessions("alice")).toBe(1); expect(store.sessions.size).toBe(0); }); test("rejects invalid domain records and malformed token hashes", async () => { const store = new MemoryAuthStore(); await expect( store.putUser({ username: " alice", passwordHash: "hash", roles: ["viewer"], }), ).rejects.toThrow("Username"); await expect( store.putUser({ username: "alice", passwordHash: "hash", roles: ["root"] as never, }), ).rejects.toThrow("valid role"); expect(tokenHashesEqual("zz", "zz")).toBe(false); }); });