Skip to main content
Glama
oauth-flow.test.ts13.7 kB
/** * Integration Tests for OAuth Routes * * These tests verify OAuth route handling using SELF fetcher * to test the complete request/response cycle through the Worker. */ import { SELF, env } from "cloudflare:test"; import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; // Extend ProvidedEnv type to include OAUTH_KV declare module "cloudflare:test" { interface ProvidedEnv { OAUTH_KV: { put: ( key: string, value: string, options?: { expirationTtl?: number }, ) => Promise<void>; get: (key: string) => Promise<string | null>; delete?: (key: string) => Promise<void>; }; } } // Mock Globalping OAuth token and user data endpoints const createMockOAuthAPI = () => { const originalFetch = globalThis.fetch; let pendingRequests = 0; let idleResolvers: Array<() => void> = []; const mockFetch = vi.fn(async (input: string | URL | Request, init?: RequestInit) => { pendingRequests++; try { const urlString = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; const method = init?.method || (input instanceof Request ? input.method : "GET"); // Mock Globalping OAuth token endpoint if ( urlString.includes("auth.globalping.io/oauth/token") && !urlString.includes("/introspect") ) { if (method === "POST") { return new Response( JSON.stringify({ access_token: "gp_test_access_token_123", refresh_token: "gp_test_refresh_token_456", token_type: "Bearer", expires_in: 3600, scope: "measurements", }), { status: 200, headers: { "Content-Type": "application/json" }, }, ); } } // Mock Globalping OAuth token introspection endpoint (user data) if (urlString.includes("auth.globalping.io/oauth/token/introspect")) { if (method === "POST") { return new Response( JSON.stringify({ username: "test_user_oauth", active: true, }), { status: 200, headers: { "Content-Type": "application/json" }, }, ); } } return originalFetch(input, init); } finally { pendingRequests--; if (pendingRequests === 0) { for (const idleResolver of idleResolvers) { idleResolver(); } idleResolvers = []; } } }); const waitForIdle = () => { if (pendingRequests === 0) { return Promise.resolve(); } return new Promise<void>((resolve) => { idleResolvers.push(resolve); }); }; return { mockFetch, originalFetch, waitForIdle }; }; describe("OAuth Routes Integration", () => { let mockAPI: ReturnType<typeof createMockOAuthAPI>; beforeEach(() => { mockAPI = createMockOAuthAPI(); globalThis.fetch = mockAPI.mockFetch as any; }); afterEach(async () => { // Wait for all pending fetch operations to complete if (mockAPI) { await mockAPI.waitForIdle(); globalThis.fetch = mockAPI.originalFetch; } }); describe("Root Route", () => { it("should redirect to GitHub repository", async () => { const response = await SELF.fetch("http://localhost/", { method: "GET", headers: { Host: "localhost", }, redirect: "manual", }); expect(response.status).toBe(302); expect(response.headers.get("Location")).toBe( "https://github.com/jsdelivr/globalping-mcp-server", ); }); }); describe("Authorization Endpoint", () => { it("should validate and reject missing OAuth request parameters", async () => { const response = await SELF.fetch("http://localhost/authorize", { method: "GET", headers: { Host: "localhost", }, }); expect(response.status).toBe(200); const html = await response.text(); // OAuth provider may return either error based on validation order expect(html).toMatch(/Invalid request|Invalid redirect URI/); }); it("should reject invalid redirect URI", async () => { const response = await SELF.fetch( "http://localhost/authorize?client_id=test&redirect_uri=https://evil.com&response_type=code&state=test", { method: "GET", headers: { Host: "localhost", }, }, ); expect(response.status).toBe(200); const html = await response.text(); expect(html).toMatch(/Invalid request|Invalid redirect URI/); }); }); describe("OAuth Callback - Error Paths", () => { it("should handle missing code and state parameters", async () => { const response = await SELF.fetch("http://localhost/auth/callback", { method: "GET", headers: { Host: "localhost", }, }); expect(response.status).toBe(200); const html = await response.text(); expect(html).toContain("Code and state are missing"); }); it("should handle expired or invalid state", async () => { const response = await SELF.fetch( "http://localhost/auth/callback?code=test-code&state=invalid-state-xyz", { method: "GET", headers: { Host: "localhost", }, }, ); expect(response.status).toBe(200); const html = await response.text(); expect(html).toContain("State is outdated"); }); it("should handle OAuth provider errors", async () => { const response = await SELF.fetch( "http://localhost/auth/callback?error=access_denied&error_description=User+denied+access", { method: "GET", headers: { Host: "localhost", }, }, ); expect(response.status).toBe(200); const html = await response.text(); expect(html).toContain("Authentication error"); expect(html).toContain("access_denied"); }); it("should handle different OAuth error types", async () => { const errorTypes = [ { error: "invalid_request", description: "Missing required parameter" }, { error: "invalid_scope", description: "Invalid scope requested" }, { error: "server_error", description: "OAuth server error" }, { error: "temporarily_unavailable", description: "Service temporarily unavailable", }, ]; for (const { error, description } of errorTypes) { const response = await SELF.fetch( `http://localhost/auth/callback?error=${error}&error_description=${encodeURIComponent(description)}`, { method: "GET", headers: { Host: "localhost", }, }, ); expect(response.status).toBe(200); const html = await response.text(); expect(html).toContain("Authentication error"); expect(html).toContain(error); } }); }); describe("OAuth Callback - Success Paths", () => { it("should successfully complete OAuth flow with valid code and state", async () => { // First, register a client with the OAuth provider const clientId = "test-mcp-client"; const clientRedirectUri = "http://localhost:3000/callback"; const clientInfo = { clientId: clientId, redirectUris: [clientRedirectUri], clientName: "Test MCP Client", }; await env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(clientInfo)); // Create a valid state in KV by simulating the authorize flow const testState = "test-oauth-state-123"; const testCode = "test-auth-code-456"; // Store state data in KV that the callback will retrieve const stateData = { redirectUri: "http://localhost/auth/callback", clientRedirectUri: clientRedirectUri, codeVerifier: "test-verifier-123", codeChallenge: "test-challenge-456", clientId: clientId, state: testState, oauthReqInfo: { clientId: clientId, redirectUri: clientRedirectUri, state: "client-state-789", scope: "measurements", }, createdAt: Date.now(), }; await env.OAUTH_KV.put(`oauth_state_${testState}`, JSON.stringify(stateData), { expirationTtl: 600, }); // Now call the callback with valid code and state const response = await SELF.fetch( `http://localhost/auth/callback?code=${testCode}&state=${testState}`, { method: "GET", headers: { Host: "localhost", }, redirect: "manual", }, ); // Should redirect to the client's redirect URI with authorization code expect(response.status).toBe(302); const location = response.headers.get("Location"); expect(location).toBeTruthy(); expect(location).toContain("http://localhost:3000/callback"); expect(location).toContain("code="); // Verify token exchange was attempted expect(mockAPI.mockFetch).toHaveBeenCalledWith( expect.stringContaining("auth.globalping.io/oauth/token"), expect.objectContaining({ method: "POST", }), ); // Verify user data was fetched expect(mockAPI.mockFetch).toHaveBeenCalledWith( expect.stringContaining("auth.globalping.io/oauth/token/introspect"), expect.objectContaining({ method: "POST", }), ); // Verify state was deleted from KV const deletedState = await env.OAUTH_KV.get(`oauth_state_${testState}`); expect(deletedState).toBeNull(); }); it("should handle token exchange errors gracefully", async () => { // Override mock to return error for token exchange const errorMockFetch = vi.fn( async (input: string | URL | Request, init?: RequestInit) => { const urlString = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if ( urlString.includes("auth.globalping.io/oauth/token") && !urlString.includes("/introspect") ) { return new Response(JSON.stringify({ error: "invalid_grant" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } return mockAPI.originalFetch(input, init); }, ); globalThis.fetch = errorMockFetch as any; const testState = "test-token-error-state"; const stateData = { redirectUri: "http://localhost/auth/callback", clientRedirectUri: "http://localhost:3000/callback", codeVerifier: "test-verifier", codeChallenge: "test-challenge", clientId: "test-client", state: testState, oauthReqInfo: { clientId: "test-client", redirectUri: "http://localhost:3000/callback", state: "client-state", scope: "measurements", }, createdAt: Date.now(), }; await env.OAUTH_KV.put(`oauth_state_${testState}`, JSON.stringify(stateData), { expirationTtl: 600, }); const response = await SELF.fetch( `http://localhost/auth/callback?code=test-code&state=${testState}`, { method: "GET", headers: { Host: "localhost", }, }, ); expect(response.status).toBe(200); const html = await response.text(); expect(html).toContain("Token error"); }); it("should handle user data retrieval errors gracefully", async () => { // Override mock to return error for user data introspection const errorMockFetch = vi.fn( async (input: string | URL | Request, init?: RequestInit) => { const urlString = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if ( urlString.includes("auth.globalping.io/oauth/token") && !urlString.includes("/introspect") ) { return new Response( JSON.stringify({ access_token: "test-token", token_type: "Bearer", expires_in: 3600, }), { status: 200, headers: { "Content-Type": "application/json" }, }, ); } if (urlString.includes("/introspect")) { return new Response(JSON.stringify({ error: "invalid_token" }), { status: 401, headers: { "Content-Type": "application/json" }, }); } return mockAPI.originalFetch(input, init); }, ); globalThis.fetch = errorMockFetch as any; const testState = "test-userdata-error-state"; const stateData = { redirectUri: "http://localhost/auth/callback", clientRedirectUri: "http://localhost:3000/callback", codeVerifier: "test-verifier", codeChallenge: "test-challenge", clientId: "test-client", state: testState, oauthReqInfo: { clientId: "test-client", redirectUri: "http://localhost:3000/callback", state: "client-state", scope: "measurements", }, createdAt: Date.now(), }; await env.OAUTH_KV.put(`oauth_state_${testState}`, JSON.stringify(stateData), { expirationTtl: 600, }); const response = await SELF.fetch( `http://localhost/auth/callback?code=test-code&state=${testState}`, { method: "GET", headers: { Host: "localhost", }, }, ); expect(response.status).toBe(200); const html = await response.text(); expect(html).toContain("Failed to get user data"); }); }); describe("Error Response Format", () => { it("should return HTML error pages with proper structure", async () => { const response = await SELF.fetch("http://localhost/auth/callback", { method: "GET", headers: { Host: "localhost", }, }); expect(response.status).toBe(200); expect(response.headers.get("Content-Type")).toContain("text/html"); const html = await response.text(); expect(html).toContain("<!DOCTYPE html>"); expect(html).toContain("<html"); expect(html).toContain("</html>"); expect(html).toContain("Globalping MCP Server"); }); it("should include error message in response body", async () => { const response = await SELF.fetch("http://localhost/auth/callback?error=custom_error", { method: "GET", headers: { Host: "localhost", }, }); const html = await response.text(); expect(html).toContain("custom_error"); expect(html).toContain("Authentication error"); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jsdelivr/globalping-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server