import { Hono } from "jsr:@hono/hono@4"; import { assertEquals } from "jsr:@std/assert"; import type { WSContext } from "jsr:@hono/hono@4/ws"; import { createAuthMiddleware, createWebSocketGuard, requireScope, } from "./hono.ts"; import { stub } from "jsr:@std/testing/mock"; import { AuthSdk } from "./mod.ts"; type Variables = { userId: string; scopes: string[]; }; Deno.test("AuthMiddleware - Authorized request sets userId and scopes", async () => { const sdk = new AuthSdk({ authApiUrl: "http://localhost" }); const validateSessionStub = stub( sdk, "validateSession", () => Promise.resolve({ valid: true, uuid: "test-user-id", scopes: ["commander", "viewer"], }), ); const app = new Hono<{ Variables: Variables }>(); app.use("*", createAuthMiddleware(sdk)); app.get( "/", (c) => c.json({ userId: c.get("userId"), scopes: c.get("scopes") }), ); const req = new Request("http://localhost/", { headers: { "Authorization": "Bearer valid-token", }, }); const res = await app.request(req); assertEquals(res.status, 200); const json = await res.json(); assertEquals(json, { userId: "test-user-id", scopes: ["commander", "viewer"], }); validateSessionStub.restore(); }); Deno.test("AuthMiddleware - Unauthorized request (missing token)", async () => { const sdk = new AuthSdk({ authApiUrl: "http://localhost" }); const app = new Hono<{ Variables: Variables }>(); app.use("*", createAuthMiddleware(sdk)); app.get("/", (c) => c.text(`User: ${c.get("userId")}`)); const req = new Request("http://localhost/"); const res = await app.request(req); assertEquals(res.status, 401); const json = await res.json(); assertEquals(json, { error: "Unauthorized: Missing session token" }); }); Deno.test("AuthMiddleware - Unauthorized request (invalid token)", async () => { const sdk = new AuthSdk({ authApiUrl: "http://localhost" }); const validateSessionStub = stub( sdk, "validateSession", () => Promise.resolve({ valid: false, error: "Invalid token" }), ); const app = new Hono<{ Variables: Variables }>(); app.use("*", createAuthMiddleware(sdk)); app.get("/", (c) => c.text(`User: ${c.get("userId")}`)); const req = new Request("http://localhost/", { headers: { "Authorization": "Bearer invalid-token", }, }); const res = await app.request(req); assertEquals(res.status, 401); const json = await res.json(); assertEquals(json, { error: "Invalid token" }); validateSessionStub.restore(); }); Deno.test("requireScope - allows user when scope is present", async () => { const app = new Hono<{ Variables: Variables }>(); app.use("*", (c, next) => { c.set("userId", "test-user"); c.set("scopes", ["commander", "viewer"]); return next(); }); app.get("/command", requireScope("commander"), (c) => c.text("Command OK")); const req = new Request("http://localhost/command"); const res = await app.request(req); assertEquals(res.status, 200); assertEquals(await res.text(), "Command OK"); }); Deno.test("requireScope - allows user when global admin scope is present", async () => { const app = new Hono<{ Variables: Variables }>(); app.use("*", (c, next) => { c.set("userId", "admin-user"); c.set("scopes", ["admin"]); return next(); }); app.get( "/restricted", requireScope("special-scope"), (c) => c.text("Admin Override OK"), ); const req = new Request("http://localhost/restricted"); const res = await app.request(req); assertEquals(res.status, 200); assertEquals(await res.text(), "Admin Override OK"); }); Deno.test("requireScope - rejects user when scope is missing", async () => { const app = new Hono<{ Variables: Variables }>(); app.use("*", (c, next) => { c.set("userId", "test-user"); c.set("scopes", ["viewer"]); return next(); }); app.get("/command", requireScope("commander"), (c) => c.text("Command OK")); const req = new Request("http://localhost/command"); const res = await app.request(req); assertEquals(res.status, 403); const json = await res.json(); assertEquals(json, { error: "Forbidden: Required scope 'commander' missing.", }); }); Deno.test("createWebSocketGuard - closes immediately if no token is provided", () => { const sdk = new AuthSdk({ authApiUrl: "http://localhost" }); const guard = createWebSocketGuard(sdk); let sentData: string | undefined; let closeCode: number | undefined; let closeReason: string | undefined; const mockWs = { send: (data: string) => { sentData = data; }, close: (code: number, reason: string) => { closeCode = code; closeReason = reason; }, } as unknown as WSContext; guard.onOpen!(new Event("open"), mockWs); assertEquals( sentData, JSON.stringify({ type: "AUTH_REVOKED", reason: "SESSION_EXPIRED" }), ); assertEquals(closeCode, 1008); assertEquals(closeReason, "Session Expired"); }); Deno.test("createWebSocketGuard - handles invalidate event and cleans up on close", () => { const sdk = new AuthSdk({ authApiUrl: "http://localhost" }); const token = "test-token-123"; const guard = createWebSocketGuard(sdk, token); let sentData: string | undefined; let closeCode: number | undefined; let closeReason: string | undefined; const mockWs = { send: (data: string) => { sentData = data; }, close: (code: number, reason: string) => { closeCode = code; closeReason = reason; }, } as unknown as WSContext; guard.onOpen!(new Event("open"), mockWs); // Trigger invalidation for a different token (should do nothing) sdk["emit"]("invalidate", "other-token"); assertEquals(sentData, undefined); assertEquals(closeCode, undefined); // Trigger invalidation for the matching token sdk["emit"]("invalidate", token); assertEquals( sentData, JSON.stringify({ type: "AUTH_REVOKED", reason: "SESSION_EXPIRED" }), ); assertEquals(closeCode, 1008); assertEquals(closeReason, "Session Expired"); // Verify listener is registered assertEquals(sdk["listeners"].get("invalidate")?.size, 1); // Verify listener cleanup on close guard.onClose!(new CloseEvent("close"), mockWs); assertEquals(sdk["listeners"].has("invalidate"), false); });