0de7453349
- install.test.ts: save and restore globalThis.fetch in beforeEach/afterEach to prevent leaking a mock fetch (without .preconnect) into subsequent test files - provider.test.ts: guard against originalFetch missing .preconnect when captured from a leaked mock (defensive null-safe binding) - discovery.test.ts: add writable:true to all Object.defineProperty fetch assignments so downstream plain assignments (globalThis.fetch = ...) are not silently ignored Root cause: install.test.ts set globalThis.fetch = mock(...) inside test bodies without restoring it, leaving a mock fetch (no .preconnect method) that caused provider.test.ts refresh tests to throw TypeError at the fetchMock construction
239 lines
9.2 KiB
TypeScript
239 lines
9.2 KiB
TypeScript
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, writable: 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, writable: 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, writable: 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("falls back to root well-known URL when resource has a sub-path", () => {
|
|
// given — resource URL has a /mcp path (e.g. https://mcp.sentry.dev/mcp)
|
|
const resource = "https://mcp.example.com/mcp"
|
|
const prmUrl = new URL("/.well-known/oauth-protected-resource", resource).toString()
|
|
const pathSuffixedAsUrl = "https://mcp.example.com/.well-known/oauth-authorization-server/mcp"
|
|
const rootAsUrl = "https://mcp.example.com/.well-known/oauth-authorization-server"
|
|
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 === pathSuffixedAsUrl) {
|
|
return new Response("not found", { status: 404 })
|
|
}
|
|
if (url === rootAsUrl) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
authorization_endpoint: "https://mcp.example.com/oauth/authorize",
|
|
token_endpoint: "https://mcp.example.com/oauth/token",
|
|
registration_endpoint: "https://mcp.example.com/oauth/register",
|
|
}),
|
|
{ status: 200 }
|
|
)
|
|
}
|
|
return new Response("not found", { status: 404 })
|
|
}
|
|
Object.defineProperty(globalThis, "fetch", { value: fetchMock, configurable: true, writable: true })
|
|
|
|
// when
|
|
return discoverOAuthServerMetadata(resource).then((result) => {
|
|
// then
|
|
expect(result).toEqual({
|
|
authorizationEndpoint: "https://mcp.example.com/oauth/authorize",
|
|
tokenEndpoint: "https://mcp.example.com/oauth/token",
|
|
registrationEndpoint: "https://mcp.example.com/oauth/register",
|
|
resource,
|
|
})
|
|
expect(calls).toEqual([prmUrl, pathSuffixedAsUrl, rootAsUrl])
|
|
})
|
|
})
|
|
|
|
test("throws when PRM, path-suffixed AS, and root AS all return 404", () => {
|
|
// given
|
|
const resource = "https://mcp.example.com/mcp"
|
|
const prmUrl = new URL("/.well-known/oauth-protected-resource", resource).toString()
|
|
const fetchMock = async (input: string | URL) => {
|
|
const url = typeof input === "string" ? input : input.toString()
|
|
if (url === prmUrl || url.includes(".well-known/oauth-authorization-server")) {
|
|
return new Response("not found", { status: 404 })
|
|
}
|
|
return new Response("not found", { status: 404 })
|
|
}
|
|
Object.defineProperty(globalThis, "fetch", { value: fetchMock, configurable: true, writable: true })
|
|
|
|
// when
|
|
const result = discoverOAuthServerMetadata(resource)
|
|
|
|
// then
|
|
return expect(result).rejects.toThrow("OAuth authorization server metadata not found")
|
|
})
|
|
|
|
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, writable: 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, writable: 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, writable: true })
|
|
|
|
// when
|
|
return discoverOAuthServerMetadata(resource)
|
|
.then(() => discoverOAuthServerMetadata(resource))
|
|
.then(() => {
|
|
// then
|
|
expect(calls).toEqual([prmUrl, asUrl])
|
|
})
|
|
})
|
|
})
|