import { beforeEach, describe, expect, mock, test } from "bun:test"; import { Hono } from "hono"; import { createApiKey } from "../../src/server/services/auth.service"; import { createTestDb } from "../helpers/db"; // Mock @hono/oidc-auth - must be before importing middleware const mockGetAuth = mock(() => null as any); mock.module("@hono/oidc-auth", () => ({ getAuth: mockGetAuth, oidcAuthMiddleware: () => async (_c: any, next: any) => next(), processOAuthCallback: async (c: any) => c.json({ ok: true }), revokeSession: async () => {}, })); // Mock oauth.service — all exports included so later test files don't see // a partial module shape when Bun's module registry is shared across files. const mockVerifyAccessToken = mock(() => Promise.resolve(false)); mock.module("../../src/server/services/oauth.service", () => ({ verifyAccessToken: mockVerifyAccessToken, registerClient: mock(() => Promise.resolve({ clientId: "id" })), getClient: mock(() => Promise.resolve(null)), createAuthorizationCode: mock(() => Promise.resolve({ code: "code" })), exchangeCode: mock(() => Promise.resolve(null)), refreshAccessToken: mock(() => Promise.resolve(null)), cleanExpiredOAuthData: mock(() => Promise.resolve()), })); // Import middleware AFTER mocks are set up const { requireAuth } = await import("../../src/server/middleware/auth"); let db: any; let userId: number; beforeEach(async () => { ({ db, userId } = await createTestDb()); mockGetAuth.mockReset(); mockGetAuth.mockReturnValue(null); mockVerifyAccessToken.mockReset(); mockVerifyAccessToken.mockReturnValue(Promise.resolve(false)); }); function createApp() { const app = new Hono<{ Variables: { db?: any } }>(); app.use("*", async (c, next) => { c.set("db", db); await next(); }); // Public GET app.get("/items", (c) => c.json({ ok: true })); // Protected POST app.post("/items", requireAuth, (c) => c.json({ ok: true })); return app; } describe("auth middleware", () => { test("allows GET requests without auth (middleware not applied to GET)", async () => { const app = createApp(); const res = await app.request("/items"); expect(res.status).toBe(200); }); test("rejects POST with no auth credentials", async () => { const app = createApp(); const res = await app.request("/items", { method: "POST" }); expect(res.status).toBe(401); const body = await res.json(); expect(body.error).toBe("Authentication required"); }); test("allows POST with valid API key", async () => { const app = createApp(); const key = await createApiKey(db, userId, "test"); const res = await app.request("/items", { method: "POST", headers: { "X-API-Key": key.rawKey }, }); expect(res.status).toBe(200); }); test("rejects POST with invalid API key", async () => { const app = createApp(); const res = await app.request("/items", { method: "POST", headers: { "X-API-Key": "invalid-key-value" }, }); expect(res.status).toBe(401); const body = await res.json(); expect(body.error).toBe("Invalid API key"); }); test("allows POST with valid Bearer token", async () => { const app = createApp(); mockVerifyAccessToken.mockReturnValue(Promise.resolve(true)); const res = await app.request("/items", { method: "POST", headers: { Authorization: "Bearer valid-token-123" }, }); expect(res.status).toBe(200); }); test("rejects POST with invalid Bearer token", async () => { const app = createApp(); mockVerifyAccessToken.mockReturnValue(Promise.resolve(false)); const res = await app.request("/items", { method: "POST", headers: { Authorization: "Bearer invalid-token" }, }); expect(res.status).toBe(401); const body = await res.json(); expect(body.error).toBe("Invalid or expired token"); }); test("allows POST with valid OIDC session", async () => { const app = createApp(); mockGetAuth.mockReturnValue({ sub: "user-123", email: "test@example.com" }); const res = await app.request("/items", { method: "POST" }); expect(res.status).toBe(200); }); test("API key takes priority over OIDC session", async () => { const app = createApp(); const key = await createApiKey(db, userId, "test"); mockGetAuth.mockReturnValue({ sub: "user-123" }); const res = await app.request("/items", { method: "POST", headers: { "X-API-Key": key.rawKey }, }); expect(res.status).toBe(200); }); });