From c4a7a6c76ff388d9652cc1b920db11cdb956ac9a Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 5 Apr 2026 11:34:10 +0200 Subject: [PATCH] fix(16): restore OIDC-based oauth tests with userId support Merge conflict resolution picked the old password-based oauth tests. Restored the OIDC session mock version with proper userId destructuring. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/routes/oauth.test.ts | 223 +++++++++---------------------------- 1 file changed, 53 insertions(+), 170 deletions(-) diff --git a/tests/routes/oauth.test.ts b/tests/routes/oauth.test.ts index 3c17537..7c89b2c 100644 --- a/tests/routes/oauth.test.ts +++ b/tests/routes/oauth.test.ts @@ -1,26 +1,21 @@ -import { beforeEach, describe, expect, it } from "bun:test"; +import { beforeEach, describe, expect, it, mock } from "bun:test"; import { createHash, randomBytes } from "node:crypto"; import { Hono } from "hono"; -import { mcpRoutes } from "../../src/server/mcp/index.ts"; import { oauthRoutes, wellKnownRoute } from "../../src/server/routes/oauth.ts"; -import { createUser } from "../../src/server/services/auth.service.ts"; +import { createApiKey } from "../../src/server/services/auth.service.ts"; import { createTestDb } from "../helpers/db.ts"; -function createTestApp() { - const { db, userId } = createTestDb(); - const app = new Hono<{ Variables: { db?: any; userId?: number } }>(); - app.use("*", async (c, next) => { - c.set("db", db); - c.set("userId", userId); - await next(); - }); - app.route("/.well-known", wellKnownRoute); - app.route("/oauth", oauthRoutes); - return { app, db, userId }; -} +// Mock @hono/oidc-auth — must be before importing routes +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 () => {}, +})); -function createFullTestApp() { - const { db, userId } = createTestDb(); +async function createTestApp() { + const { db, userId } = await createTestDb(); const app = new Hono<{ Variables: { db?: any; userId?: number } }>(); app.use("*", async (c, next) => { c.set("db", db); @@ -29,7 +24,6 @@ function createFullTestApp() { }); app.route("/.well-known", wellKnownRoute); app.route("/oauth", oauthRoutes); - app.route("/mcp", mcpRoutes); return { app, db, userId }; } @@ -41,13 +35,17 @@ function generatePkce() { describe("OAuth Routes", () => { let app: Hono; - let db: ReturnType["db"]; + let db: Awaited>["db"]; + let userId: number; beforeEach(async () => { - const testApp = createTestApp(); + const testApp = await createTestApp(); app = testApp.app; db = testApp.db; - await createUser(db, "admin", "secret123"); + userId = testApp.userId; + mockGetAuth.mockReset(); + // Default: user is authenticated via OIDC + mockGetAuth.mockReturnValue({ sub: "test-user-logto-sub", email: "admin@example.com" }); }); describe("GET /.well-known/oauth-authorization-server", () => { @@ -100,8 +98,7 @@ describe("OAuth Routes", () => { }); describe("GET /oauth/authorize", () => { - it("returns 200 HTML with form when params are valid", async () => { - // Register a client first + it("returns 200 HTML with consent form when OIDC session exists", async () => { const regRes = await app.request("/oauth/register", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -127,10 +124,41 @@ describe("OAuth Routes", () => { const html = await res.text(); expect(html).toContain("GearBox"); - expect(html).toContain("password"); + expect(html).toContain("Authorize"); expect(html).toContain("Test Client"); }); + it("redirects to /login when no OIDC session", async () => { + mockGetAuth.mockReturnValue(null); + + const regRes = await app.request("/oauth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + client_name: "Test Client", + redirect_uris: ["http://localhost:3000/callback"], + }), + }); + const { client_id } = await regRes.json(); + const { challenge } = generatePkce(); + + const params = new URLSearchParams({ + response_type: "code", + client_id, + redirect_uri: "http://localhost:3000/callback", + code_challenge: challenge, + code_challenge_method: "S256", + state: "abc123", + }); + + const res = await app.request(`/oauth/authorize?${params}`, { + redirect: "manual", + }); + expect(res.status).toBe(302); + const location = res.headers.get("location"); + expect(location).toContain("/login"); + }); + it("returns 400 with invalid client_id", async () => { const { challenge } = generatePkce(); const params = new URLSearchParams({ @@ -147,45 +175,8 @@ describe("OAuth Routes", () => { }); }); - describe("POST /oauth/authorize", () => { - it("returns 200 HTML with error on wrong password", async () => { - // Register a client - const regRes = await app.request("/oauth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - client_name: "Test Client", - redirect_uris: ["http://localhost:3000/callback"], - }), - }); - const { client_id } = await regRes.json(); - const { challenge } = generatePkce(); - - const formBody = new URLSearchParams({ - username: "admin", - password: "wrongpassword", - client_id, - redirect_uri: "http://localhost:3000/callback", - code_challenge: challenge, - code_challenge_method: "S256", - state: "abc123", - }); - - const res = await app.request("/oauth/authorize", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: formBody.toString(), - }); - - expect(res.status).toBe(200); - const html = await res.text(); - expect(html).toContain("Invalid"); - }); - }); - describe("Full OAuth flow", () => { - it("register → authorize → token exchange", async () => { - // 1. Register client + it("register -> authorize (consent) -> token exchange", async () => { const regRes = await app.request("/oauth/register", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -197,10 +188,7 @@ describe("OAuth Routes", () => { const { client_id } = await regRes.json(); const { verifier, challenge } = generatePkce(); - // 2. POST /oauth/authorize with correct credentials const formBody = new URLSearchParams({ - username: "admin", - password: "secret123", client_id, redirect_uri: "http://localhost:3000/callback", code_challenge: challenge, @@ -223,7 +211,6 @@ describe("OAuth Routes", () => { const redirectUrl = new URL(location); const code = redirectUrl.searchParams.get("code")!; - // 3. Exchange code for tokens const tokenBody = new URLSearchParams({ grant_type: "authorization_code", code, @@ -247,108 +234,8 @@ describe("OAuth Routes", () => { }); }); - describe("Full OAuth → MCP Flow", () => { - it("complete flow: register → authorize → token → MCP call", async () => { - const { app, db } = createFullTestApp(); - await createUser(db, "admin", "secret123"); - const { verifier, challenge } = generatePkce(); - - // 1. Register client - const regRes = await app.request("/oauth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - client_name: "Claude", - redirect_uris: ["http://localhost/cb"], - }), - }); - const { client_id } = await regRes.json(); - - // 2. Authorize (simulate form POST) - const authRes = await app.request( - `/oauth/authorize?response_type=code&client_id=${client_id}&redirect_uri=${encodeURIComponent("http://localhost/cb")}&code_challenge=${challenge}&code_challenge_method=S256&state=test`, - { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - username: "admin", - password: "secret123", - client_id, - redirect_uri: "http://localhost/cb", - code_challenge: challenge, - code_challenge_method: "S256", - state: "test", - }).toString(), - }, - ); - const code = new URL(authRes.headers.get("location")!).searchParams.get( - "code", - )!; - - // 3. Exchange code for tokens - const tokenRes = await app.request("/oauth/token", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - grant_type: "authorization_code", - code, - code_verifier: verifier, - client_id, - redirect_uri: "http://localhost/cb", - }).toString(), - }); - const { access_token } = await tokenRes.json(); - - // 4. Use token to call MCP - const mcpRes = await app.request("/mcp", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json, text/event-stream", - Authorization: `Bearer ${access_token}`, - }, - body: JSON.stringify({ - jsonrpc: "2.0", - method: "initialize", - params: { - protocolVersion: "2025-03-26", - capabilities: {}, - clientInfo: { name: "test", version: "1.0" }, - }, - id: 1, - }), - }); - - expect(mcpRes.status).toBe(200); - }); - - it("rejects MCP call without auth when user exists", async () => { - const { app, db } = createFullTestApp(); - await createUser(db, "admin", "secret123"); - - const mcpRes = await app.request("/mcp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - jsonrpc: "2.0", - method: "initialize", - params: { - protocolVersion: "2025-03-26", - capabilities: {}, - clientInfo: { name: "test", version: "1.0" }, - }, - id: 1, - }), - }); - - expect(mcpRes.status).toBe(401); - expect(mcpRes.headers.get("www-authenticate")).toContain("Bearer"); - }); - }); - describe("Token refresh", () => { it("exchanges refresh token for new tokens", async () => { - // Full flow to get initial tokens const regRes = await app.request("/oauth/register", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -361,8 +248,6 @@ describe("OAuth Routes", () => { const { verifier, challenge } = generatePkce(); const formBody = new URLSearchParams({ - username: "admin", - password: "secret123", client_id, redirect_uri: "http://localhost:3000/callback", code_challenge: challenge, @@ -395,7 +280,6 @@ describe("OAuth Routes", () => { }); const tokens = await tokenRes.json(); - // Now refresh const refreshBody = new URLSearchParams({ grant_type: "refresh_token", refresh_token: tokens.refresh_token, @@ -414,7 +298,6 @@ describe("OAuth Routes", () => { expect(newTokens.refresh_token).toBeDefined(); expect(newTokens.token_type).toBe("Bearer"); expect(newTokens.expires_in).toBe(3600); - // Should be different tokens expect(newTokens.access_token).not.toBe(tokens.access_token); }); });