/**
* postgres-mcp - Authorization Server Discovery Tests
*
* Tests for RFC 8414 Authorization Server Metadata discovery.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
AuthorizationServerDiscovery,
createAuthServerDiscovery,
} from "../AuthorizationServerDiscovery.js";
import { AuthServerDiscoveryError } from "../errors.js";
// Mock global fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe("AuthorizationServerDiscovery", () => {
const defaultConfig = {
authServerUrl: "http://localhost:8080/realms/postgres-mcp",
};
const validMetadata = {
issuer: "http://localhost:8080/realms/postgres-mcp",
token_endpoint:
"http://localhost:8080/realms/postgres-mcp/protocol/openid-connect/token",
jwks_uri:
"http://localhost:8080/realms/postgres-mcp/protocol/openid-connect/certs",
authorization_endpoint:
"http://localhost:8080/realms/postgres-mcp/protocol/openid-connect/auth",
registration_endpoint:
"http://localhost:8080/realms/postgres-mcp/clients-registrations/openid-connect",
grant_types_supported: [
"authorization_code",
"client_credentials",
"refresh_token",
],
};
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.useRealTimers();
});
describe("discover", () => {
it("should fetch and return authorization server metadata", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(validMetadata),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
const metadata = await discovery.discover();
expect(metadata.issuer).toBe("http://localhost:8080/realms/postgres-mcp");
expect(metadata.token_endpoint).toBeDefined();
expect(metadata.jwks_uri).toBeDefined();
});
it("should use the correct well-known URL", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(validMetadata),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
await discovery.discover();
// Just verify correct URL is called - implementation details like signal/headers may vary
expect(mockFetch).toHaveBeenCalledTimes(1);
const [url] = mockFetch.mock.calls[0] as [string, unknown];
expect(url).toBe(
"http://localhost:8080/realms/postgres-mcp/.well-known/oauth-authorization-server",
);
});
it("should cache metadata for subsequent calls", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(validMetadata),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
// First call
await discovery.discover();
// Second call - should use cache
await discovery.discover();
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it("should throw error for HTTP failures", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: "Not Found",
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
await expect(discovery.discover()).rejects.toThrow(
AuthServerDiscoveryError,
);
});
it("should throw error for network failures", async () => {
mockFetch.mockRejectedValueOnce(new Error("Network error"));
const discovery = new AuthorizationServerDiscovery(defaultConfig);
await expect(discovery.discover()).rejects.toThrow(
AuthServerDiscoveryError,
);
});
it("should throw error for invalid metadata (missing issuer)", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ token_endpoint: "something" }),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
await expect(discovery.discover()).rejects.toThrow(
AuthServerDiscoveryError,
);
});
it("should throw error for invalid metadata (missing token_endpoint)", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ issuer: "something" }),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
await expect(discovery.discover()).rejects.toThrow(
AuthServerDiscoveryError,
);
});
});
describe("getJwksUri", () => {
it("should return JWKS URI from metadata", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(validMetadata),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
const uri = await discovery.getJwksUri();
expect(uri).toBe(
"http://localhost:8080/realms/postgres-mcp/protocol/openid-connect/certs",
);
});
it("should throw when metadata has no jwks_uri", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
issuer: validMetadata.issuer,
token_endpoint: validMetadata.token_endpoint,
// No jwks_uri
}),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
await expect(discovery.getJwksUri()).rejects.toThrow(
AuthServerDiscoveryError,
);
});
});
describe("getTokenEndpoint", () => {
it("should return token endpoint from metadata", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(validMetadata),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
const endpoint = await discovery.getTokenEndpoint();
expect(endpoint).toBe(
"http://localhost:8080/realms/postgres-mcp/protocol/openid-connect/token",
);
});
});
describe("getRegistrationEndpoint", () => {
it("should return registration endpoint when available", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(validMetadata),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
const endpoint = await discovery.getRegistrationEndpoint();
expect(endpoint).toBeDefined();
});
it("should return undefined when not available", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
issuer: validMetadata.issuer,
token_endpoint: validMetadata.token_endpoint,
}),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
const endpoint = await discovery.getRegistrationEndpoint();
expect(endpoint).toBeUndefined();
});
});
describe("supportsGrantType", () => {
it("should return true for supported grant types", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(validMetadata),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
expect(await discovery.supportsGrantType("client_credentials")).toBe(
true,
);
expect(await discovery.supportsGrantType("authorization_code")).toBe(
true,
);
});
it("should return false for unsupported grant types", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(validMetadata),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
await discovery.discover(); // Prime the cache
mockFetch.mockClear();
expect(await discovery.supportsGrantType("implicit")).toBe(false);
expect(mockFetch).not.toHaveBeenCalled(); // Uses cache
});
it("should return false when grant_types_supported is missing", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
issuer: validMetadata.issuer,
token_endpoint: validMetadata.token_endpoint,
}),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
expect(await discovery.supportsGrantType("client_credentials")).toBe(
false,
);
});
});
describe("invalidateCache", () => {
it("should clear the metadata cache", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve(validMetadata),
});
const discovery = new AuthorizationServerDiscovery(defaultConfig);
// First call
await discovery.discover();
expect(mockFetch).toHaveBeenCalledTimes(1);
// Invalidate
discovery.invalidateCache();
// Second call - should fetch again
await discovery.discover();
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});
describe("createAuthServerDiscovery factory", () => {
it("should create an AuthorizationServerDiscovery instance", () => {
const discovery = createAuthServerDiscovery(defaultConfig);
expect(discovery).toBeInstanceOf(AuthorizationServerDiscovery);
});
});
});