import { describe, test, expect, beforeEach, afterEach } from "bun:test" import { discoverOAuthServerMetadata, resetDiscoveryCache } from "./discovery" describe("discoverOAuthServerMetadata", () => { const originalFetch = globalThis.fetch beforeEach(() => { resetDiscoveryCache() }) afterEach(() => { Object.defineProperty(globalThis, "fetch", { value: originalFetch, configurable: true }) }) test("returns endpoints from PRM + AS discovery", () => { // given const resource = "https://mcp.example.com" const prmUrl = new URL("/.well-known/oauth-protected-resource", resource).toString() const authServer = "https://auth.example.com" const asUrl = new URL("/.well-known/oauth-authorization-server", authServer).toString() const calls: string[] = [] const fetchMock = async (input: string | URL) => { const url = typeof input === "string" ? input : input.toString() calls.push(url) if (url === prmUrl) { return new Response(JSON.stringify({ authorization_servers: [authServer] }), { status: 200 }) } if (url === asUrl) { return new Response( JSON.stringify({ authorization_endpoint: "https://auth.example.com/authorize", token_endpoint: "https://auth.example.com/token", registration_endpoint: "https://auth.example.com/register", }), { status: 200 } ) } return new Response("not found", { status: 404 }) } Object.defineProperty(globalThis, "fetch", { value: fetchMock, configurable: true }) // when return discoverOAuthServerMetadata(resource).then((result) => { // then expect(result).toEqual({ authorizationEndpoint: "https://auth.example.com/authorize", tokenEndpoint: "https://auth.example.com/token", registrationEndpoint: "https://auth.example.com/register", resource, }) expect(calls).toEqual([prmUrl, asUrl]) }) }) test("falls back to RFC 8414 when PRM returns 404", () => { // given const resource = "https://mcp.example.com" const prmUrl = new URL("/.well-known/oauth-protected-resource", resource).toString() const asUrl = new URL("/.well-known/oauth-authorization-server", resource).toString() const calls: string[] = [] const fetchMock = async (input: string | URL) => { const url = typeof input === "string" ? input : input.toString() calls.push(url) if (url === prmUrl) { return new Response("not found", { status: 404 }) } if (url === asUrl) { return new Response( JSON.stringify({ authorization_endpoint: "https://mcp.example.com/authorize", token_endpoint: "https://mcp.example.com/token", }), { status: 200 } ) } return new Response("not found", { status: 404 }) } Object.defineProperty(globalThis, "fetch", { value: fetchMock, configurable: true }) // when return discoverOAuthServerMetadata(resource).then((result) => { // then expect(result).toEqual({ authorizationEndpoint: "https://mcp.example.com/authorize", tokenEndpoint: "https://mcp.example.com/token", registrationEndpoint: undefined, resource, }) expect(calls).toEqual([prmUrl, asUrl]) }) }) test("throws when both PRM and AS discovery return 404", () => { // given const resource = "https://mcp.example.com" const prmUrl = new URL("/.well-known/oauth-protected-resource", resource).toString() const asUrl = new URL("/.well-known/oauth-authorization-server", resource).toString() const fetchMock = async (input: string | URL) => { const url = typeof input === "string" ? input : input.toString() if (url === prmUrl || url === asUrl) { return new Response("not found", { status: 404 }) } return new Response("not found", { status: 404 }) } Object.defineProperty(globalThis, "fetch", { value: fetchMock, configurable: true }) // when const result = discoverOAuthServerMetadata(resource) // then return expect(result).rejects.toThrow("OAuth authorization server metadata not found") }) test("throws when AS metadata is malformed", () => { // given const resource = "https://mcp.example.com" const prmUrl = new URL("/.well-known/oauth-protected-resource", resource).toString() const authServer = "https://auth.example.com" const asUrl = new URL("/.well-known/oauth-authorization-server", authServer).toString() const fetchMock = async (input: string | URL) => { const url = typeof input === "string" ? input : input.toString() if (url === prmUrl) { return new Response(JSON.stringify({ authorization_servers: [authServer] }), { status: 200 }) } if (url === asUrl) { return new Response(JSON.stringify({ authorization_endpoint: "https://auth.example.com/authorize" }), { status: 200, }) } return new Response("not found", { status: 404 }) } Object.defineProperty(globalThis, "fetch", { value: fetchMock, configurable: true }) // when const result = discoverOAuthServerMetadata(resource) // then return expect(result).rejects.toThrow("token_endpoint") }) test("caches discovery results per resource URL", () => { // given const resource = "https://mcp.example.com" const prmUrl = new URL("/.well-known/oauth-protected-resource", resource).toString() const authServer = "https://auth.example.com" const asUrl = new URL("/.well-known/oauth-authorization-server", authServer).toString() const calls: string[] = [] const fetchMock = async (input: string | URL) => { const url = typeof input === "string" ? input : input.toString() calls.push(url) if (url === prmUrl) { return new Response(JSON.stringify({ authorization_servers: [authServer] }), { status: 200 }) } if (url === asUrl) { return new Response( JSON.stringify({ authorization_endpoint: "https://auth.example.com/authorize", token_endpoint: "https://auth.example.com/token", }), { status: 200 } ) } return new Response("not found", { status: 404 }) } Object.defineProperty(globalThis, "fetch", { value: fetchMock, configurable: true }) // when return discoverOAuthServerMetadata(resource) .then(() => discoverOAuthServerMetadata(resource)) .then(() => { // then expect(calls).toEqual([prmUrl, asUrl]) }) }) })