Codebase MCP

import { discoverOAuthMetadata, startAuthorization, exchangeAuthorization, refreshAuthorization, registerClient, } from "./auth.js"; // Mock fetch globally const mockFetch = jest.fn(); global.fetch = mockFetch; describe("OAuth Authorization", () => { beforeEach(() => { mockFetch.mockReset(); }); describe("discoverOAuthMetadata", () => { const validMetadata = { issuer: "https://auth.example.com", authorization_endpoint: "https://auth.example.com/authorize", token_endpoint: "https://auth.example.com/token", registration_endpoint: "https://auth.example.com/register", response_types_supported: ["code"], code_challenge_methods_supported: ["S256"], }; it("returns metadata when discovery succeeds", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => validMetadata, }); const metadata = await discoverOAuthMetadata("https://auth.example.com"); expect(metadata).toEqual(validMetadata); const calls = mockFetch.mock.calls; expect(calls.length).toBe(1); const [url, options] = calls[0]; expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server"); expect(options.headers).toEqual({ "MCP-Protocol-Version": "2024-11-05" }); }); it("returns metadata when first fetch fails but second without MCP header succeeds", async () => { // Set up a counter to control behavior let callCount = 0; // Mock implementation that changes behavior based on call count mockFetch.mockImplementation((_url, _options) => { callCount++; if (callCount === 1) { // First call with MCP header - fail with TypeError (simulating CORS error) // We need to use TypeError specifically because that's what the implementation checks for return Promise.reject(new TypeError("Network error")); } else { // Second call without header - succeed return Promise.resolve({ ok: true, status: 200, json: async () => validMetadata }); } }); // Should succeed with the second call const metadata = await discoverOAuthMetadata("https://auth.example.com"); expect(metadata).toEqual(validMetadata); // Verify both calls were made expect(mockFetch).toHaveBeenCalledTimes(2); // Verify first call had MCP header expect(mockFetch.mock.calls[0][1]?.headers).toHaveProperty("MCP-Protocol-Version"); }); it("throws an error when all fetch attempts fail", async () => { // Set up a counter to control behavior let callCount = 0; // Mock implementation that changes behavior based on call count mockFetch.mockImplementation((_url, _options) => { callCount++; if (callCount === 1) { // First call - fail with TypeError return Promise.reject(new TypeError("First failure")); } else { // Second call - fail with different error return Promise.reject(new Error("Second failure")); } }); // Should fail with the second error await expect(discoverOAuthMetadata("https://auth.example.com")) .rejects.toThrow("Second failure"); // Verify both calls were made expect(mockFetch).toHaveBeenCalledTimes(2); }); it("returns undefined when discovery endpoint returns 404", async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 404, }); const metadata = await discoverOAuthMetadata("https://auth.example.com"); expect(metadata).toBeUndefined(); }); it("throws on non-404 errors", async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, }); await expect( discoverOAuthMetadata("https://auth.example.com") ).rejects.toThrow("HTTP 500"); }); it("validates metadata schema", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ // Missing required fields issuer: "https://auth.example.com", }), }); await expect( discoverOAuthMetadata("https://auth.example.com") ).rejects.toThrow(); }); }); describe("startAuthorization", () => { const validMetadata = { issuer: "https://auth.example.com", authorization_endpoint: "https://auth.example.com/auth", token_endpoint: "https://auth.example.com/tkn", response_types_supported: ["code"], code_challenge_methods_supported: ["S256"], }; const validClientInfo = { client_id: "client123", client_secret: "secret123", redirect_uris: ["http://localhost:3000/callback"], client_name: "Test Client", }; it("generates authorization URL with PKCE challenge", async () => { const { authorizationUrl, codeVerifier } = await startAuthorization( "https://auth.example.com", { clientInformation: validClientInfo, redirectUrl: "http://localhost:3000/callback", } ); expect(authorizationUrl.toString()).toMatch( /^https:\/\/auth\.example\.com\/authorize\?/ ); expect(authorizationUrl.searchParams.get("response_type")).toBe("code"); expect(authorizationUrl.searchParams.get("code_challenge")).toBe("test_challenge"); expect(authorizationUrl.searchParams.get("code_challenge_method")).toBe( "S256" ); expect(authorizationUrl.searchParams.get("redirect_uri")).toBe( "http://localhost:3000/callback" ); expect(codeVerifier).toBe("test_verifier"); }); it("uses metadata authorization_endpoint when provided", async () => { const { authorizationUrl } = await startAuthorization( "https://auth.example.com", { metadata: validMetadata, clientInformation: validClientInfo, redirectUrl: "http://localhost:3000/callback", } ); expect(authorizationUrl.toString()).toMatch( /^https:\/\/auth\.example\.com\/auth\?/ ); }); it("validates response type support", async () => { const metadata = { ...validMetadata, response_types_supported: ["token"], // Does not support 'code' }; await expect( startAuthorization("https://auth.example.com", { metadata, clientInformation: validClientInfo, redirectUrl: "http://localhost:3000/callback", }) ).rejects.toThrow(/does not support response type/); }); it("validates PKCE support", async () => { const metadata = { ...validMetadata, response_types_supported: ["code"], code_challenge_methods_supported: ["plain"], // Does not support 'S256' }; await expect( startAuthorization("https://auth.example.com", { metadata, clientInformation: validClientInfo, redirectUrl: "http://localhost:3000/callback", }) ).rejects.toThrow(/does not support code challenge method/); }); }); describe("exchangeAuthorization", () => { const validTokens = { access_token: "access123", token_type: "Bearer", expires_in: 3600, refresh_token: "refresh123", }; const validClientInfo = { client_id: "client123", client_secret: "secret123", redirect_uris: ["http://localhost:3000/callback"], client_name: "Test Client", }; it("exchanges code for tokens", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => validTokens, }); const tokens = await exchangeAuthorization("https://auth.example.com", { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", }); expect(tokens).toEqual(validTokens); expect(mockFetch).toHaveBeenCalledWith( expect.objectContaining({ href: "https://auth.example.com/token", }), expect.objectContaining({ method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, }) ); const body = mockFetch.mock.calls[0][1].body as URLSearchParams; expect(body.get("grant_type")).toBe("authorization_code"); expect(body.get("code")).toBe("code123"); expect(body.get("code_verifier")).toBe("verifier123"); expect(body.get("client_id")).toBe("client123"); expect(body.get("client_secret")).toBe("secret123"); }); it("validates token response schema", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ // Missing required fields access_token: "access123", }), }); await expect( exchangeAuthorization("https://auth.example.com", { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", }) ).rejects.toThrow(); }); it("throws on error response", async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 400, }); await expect( exchangeAuthorization("https://auth.example.com", { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", }) ).rejects.toThrow("Token exchange failed"); }); }); describe("refreshAuthorization", () => { const validTokens = { access_token: "newaccess123", token_type: "Bearer", expires_in: 3600, refresh_token: "newrefresh123", }; const validClientInfo = { client_id: "client123", client_secret: "secret123", redirect_uris: ["http://localhost:3000/callback"], client_name: "Test Client", }; it("exchanges refresh token for new tokens", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => validTokens, }); const tokens = await refreshAuthorization("https://auth.example.com", { clientInformation: validClientInfo, refreshToken: "refresh123", }); expect(tokens).toEqual(validTokens); expect(mockFetch).toHaveBeenCalledWith( expect.objectContaining({ href: "https://auth.example.com/token", }), expect.objectContaining({ method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, }) ); const body = mockFetch.mock.calls[0][1].body as URLSearchParams; expect(body.get("grant_type")).toBe("refresh_token"); expect(body.get("refresh_token")).toBe("refresh123"); expect(body.get("client_id")).toBe("client123"); expect(body.get("client_secret")).toBe("secret123"); }); it("validates token response schema", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ // Missing required fields access_token: "newaccess123", }), }); await expect( refreshAuthorization("https://auth.example.com", { clientInformation: validClientInfo, refreshToken: "refresh123", }) ).rejects.toThrow(); }); it("throws on error response", async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 400, }); await expect( refreshAuthorization("https://auth.example.com", { clientInformation: validClientInfo, refreshToken: "refresh123", }) ).rejects.toThrow("Token refresh failed"); }); }); describe("registerClient", () => { const validClientMetadata = { redirect_uris: ["http://localhost:3000/callback"], client_name: "Test Client", }; const validClientInfo = { client_id: "client123", client_secret: "secret123", client_id_issued_at: 1612137600, client_secret_expires_at: 1612224000, ...validClientMetadata, }; it("registers client and returns client information", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => validClientInfo, }); const clientInfo = await registerClient("https://auth.example.com", { clientMetadata: validClientMetadata, }); expect(clientInfo).toEqual(validClientInfo); expect(mockFetch).toHaveBeenCalledWith( expect.objectContaining({ href: "https://auth.example.com/register", }), expect.objectContaining({ method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(validClientMetadata), }) ); }); it("validates client information response schema", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ // Missing required fields client_secret: "secret123", }), }); await expect( registerClient("https://auth.example.com", { clientMetadata: validClientMetadata, }) ).rejects.toThrow(); }); it("throws when registration endpoint not available in metadata", async () => { const metadata = { issuer: "https://auth.example.com", authorization_endpoint: "https://auth.example.com/authorize", token_endpoint: "https://auth.example.com/token", response_types_supported: ["code"], }; await expect( registerClient("https://auth.example.com", { metadata, clientMetadata: validClientMetadata, }) ).rejects.toThrow(/does not support dynamic client registration/); }); it("throws on error response", async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 400, }); await expect( registerClient("https://auth.example.com", { clientMetadata: validClientMetadata, }) ).rejects.toThrow("Dynamic client registration failed"); }); }); });