/** * Unit tests for OAuth discovery endpoint response shapes. * * These endpoints are public, unauthenticated, and return JSON metadata * that MCP clients use to discover OAuth endpoints. The response shapes * are contractual — changing them breaks MCP client compatibility. */ import { describe, it, expect } from "vitest"; import { GET as getAuthorizationServer } from "../../../src/astro/routes/api/well-known/oauth-authorization-server.js"; // We import the GET handlers directly — they're plain functions that take // an Astro-like context and return a Response. import { GET as getProtectedResource } from "../../../src/astro/routes/api/well-known/oauth-protected-resource.js"; import { VALID_SCOPES } from "../../../src/auth/api-tokens.js"; /** Minimal mock of what the route handlers actually use from the Astro context. */ function mockContext(origin = "https://example.com") { return { url: new URL("/.well-known/test", origin), locals: { emdash: undefined }, } as unknown as Parameters[0]; } describe("Protected Resource Metadata (RFC 9728)", () => { it("returns correct resource and authorization_servers", async () => { const response = await getProtectedResource(mockContext()); expect(response.status).toBe(200); const body = (await response.json()) as Record; expect(body.resource).toBe("https://example.com/_emdash/api/mcp"); expect(body.authorization_servers).toEqual(["https://example.com/_emdash"]); }); it("includes all valid scopes", async () => { const response = await getProtectedResource(mockContext()); const body = (await response.json()) as { scopes_supported: string[] }; expect(body.scopes_supported).toEqual([...VALID_SCOPES]); }); it("advertises header-based bearer method", async () => { const response = await getProtectedResource(mockContext()); const body = (await response.json()) as { bearer_methods_supported: string[] }; expect(body.bearer_methods_supported).toEqual(["header"]); }); it("sets CORS and cache headers", async () => { const response = await getProtectedResource(mockContext()); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); expect(response.headers.get("Cache-Control")).toContain("public"); }); it("uses the request origin for URLs", async () => { const response = await getProtectedResource(mockContext("https://cms.mysite.com")); const body = (await response.json()) as Record; expect(body.resource).toBe("https://cms.mysite.com/_emdash/api/mcp"); expect(body.authorization_servers).toEqual(["https://cms.mysite.com/_emdash"]); }); }); describe("Authorization Server Metadata (RFC 8414)", () => { it("returns correct issuer and endpoints", async () => { const response = await getAuthorizationServer(mockContext()); expect(response.status).toBe(200); const body = (await response.json()) as Record; expect(body.issuer).toBe("https://example.com/_emdash"); expect(body.authorization_endpoint).toBe("https://example.com/_emdash/oauth/authorize"); expect(body.token_endpoint).toBe("https://example.com/_emdash/api/oauth/token"); expect(body.device_authorization_endpoint).toBe( "https://example.com/_emdash/api/oauth/device/code", ); }); it("supports authorization_code, refresh_token, and device_code grants", async () => { const response = await getAuthorizationServer(mockContext()); const body = (await response.json()) as { grant_types_supported: string[] }; expect(body.grant_types_supported).toContain("authorization_code"); expect(body.grant_types_supported).toContain("refresh_token"); expect(body.grant_types_supported).toContain("urn:ietf:params:oauth:grant-type:device_code"); }); it("requires S256 code challenge method only", async () => { const response = await getAuthorizationServer(mockContext()); const body = (await response.json()) as { code_challenge_methods_supported: string[] }; expect(body.code_challenge_methods_supported).toEqual(["S256"]); }); it("only supports code response type", async () => { const response = await getAuthorizationServer(mockContext()); const body = (await response.json()) as { response_types_supported: string[] }; expect(body.response_types_supported).toEqual(["code"]); }); it("supports public clients (no auth method)", async () => { const response = await getAuthorizationServer(mockContext()); const body = (await response.json()) as { token_endpoint_auth_methods_supported: string[] }; expect(body.token_endpoint_auth_methods_supported).toEqual(["none"]); }); it("advertises dynamic client registration", async () => { const response = await getAuthorizationServer(mockContext()); const body = (await response.json()) as { registration_endpoint: string }; expect(body.registration_endpoint).toBe("https://example.com/_emdash/api/oauth/register"); }); it("includes all valid scopes", async () => { const response = await getAuthorizationServer(mockContext()); const body = (await response.json()) as { scopes_supported: string[] }; expect(body.scopes_supported).toEqual([...VALID_SCOPES]); }); it("sets CORS and cache headers", async () => { const response = await getAuthorizationServer(mockContext()); expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); expect(response.headers.get("Cache-Control")).toContain("public"); }); it("does not advertise unsupported client_id metadata documents", async () => { const response = await getAuthorizationServer(mockContext()); const body = (await response.json()) as Record; expect(body).not.toHaveProperty("client_id_metadata_document_supported"); }); });