Skip to main content
Glama

Google Ads MCP Server

by martechery
auth-error-recovery.real.test.ts9.45 kB
import 'dotenv/config'; import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'; import { registerTools } from '../../../src/server-tools.js'; import { extractCurrentAuthDetails, createTestAuthEnv, type ExtractedAuthDetails } from '../../utils/auth-extractor.js'; type ToolHandler = (input?: any) => Promise<any> | any; class LiveServer { public tools: Record<string, ToolHandler> = {}; tool(def: any, handler: ToolHandler) { this.tools[def.name] = handler; } } describe('Auth Flow: Error Recovery', () => { let authDetails: ExtractedAuthDetails; let cleanup: (() => void) | null = null; const hasRequiredEnv = !!process.env.GOOGLE_ADS_DEVELOPER_TOKEN && !!process.env.GOOGLE_ADS_ACCOUNT_ID; beforeAll(async () => { authDetails = await extractCurrentAuthDetails(); }); afterEach(() => { if (cleanup) { cleanup(); cleanup = null; } vi.restoreAllMocks(); }); it('handles expired token error gracefully', async () => { if (!hasRequiredEnv) return; // Use an obviously expired/invalid token const expiredToken = 'ya29.expired_token_for_testing_12345'; cleanup = createTestAuthEnv({ GOOGLE_ADS_ACCESS_TOKEN: expiredToken, }); const server = new LiveServer(); registerTools(server as any); // Try to make API call with expired token try { await server.tools['list_resources']({ kind: 'accounts', output_format: 'table' }); } catch (error) { // Expected to fail - verify error handling expect(error).toBeDefined(); } // Check that manage_auth provides recovery guidance const res = await server.tools['manage_auth']({ action: 'status' }); const text = res.content[0].text as string; expect(text).toContain('Google Ads Auth Status'); expect(text).toContain('Auth type: env'); }, 30_000); it('provides helpful error messages for missing scopes', async () => { if (!hasRequiredEnv) return; // The error mapping utility should provide helpful messages const { mapAdsErrorMsg } = await import('../../../src/utils/errorMapping.js'); // Test various error scenarios const scopeError = mapAdsErrorMsg(403, 'access_token_scope_insufficient'); expect(scopeError).toContain('gcloud auth application-default login'); expect(scopeError).toContain('--scopes='); const tokenError = mapAdsErrorMsg(401, 'invalid token'); expect(tokenError).toContain('Refresh ADC'); const developerTokenError = mapAdsErrorMsg(403, 'developer token not ready'); expect(developerTokenError).toContain('GOOGLE_ADS_DEVELOPER_TOKEN'); }); it('handles network connectivity issues', async () => { if (!hasRequiredEnv || !authDetails.hasValidADC) return; // Mock fetch to simulate network errors const originalFetch = global.fetch; global.fetch = vi.fn().mockRejectedValue(new Error('Network request failed')); const server = new LiveServer(); registerTools(server as any); try { const res = await server.tools['list_resources']({ kind: 'accounts', output_format: 'table' }); const text = res.content[0].text as string; // Should handle network errors gracefully expect(text).toMatch(/(Error|Network|Failed|timeout)/i); } catch (error) { // Network errors are expected in this test expect(error).toBeDefined(); } finally { global.fetch = originalFetch; } }, 30_000); it('recovers from quota exceeded errors', async () => { if (!hasRequiredEnv) return; // Test quota error handling const { mapAdsErrorMsg } = await import('../../../src/utils/errorMapping.js'); const quotaError = mapAdsErrorMsg(429, 'quota exceeded'); expect(quotaError).toBeTruthy(); expect(quotaError).toContain('quota'); const rateLimitError = mapAdsErrorMsg(429, 'rate limit exceeded'); expect(rateLimitError).toBeTruthy(); expect(rateLimitError).toContain('rate limit'); }); it('handles missing developer token scenario', async () => { if (!hasRequiredEnv) return; cleanup = createTestAuthEnv({ GOOGLE_ADS_DEVELOPER_TOKEN: undefined, }); const server = new LiveServer(); registerTools(server as any); try { await server.tools['execute_gaql_query']({ customer_id: '1234567890', query: 'SELECT customer.id FROM customer LIMIT 1', output_format: 'table' }); } catch (error: any) { expect(error.message).toContain('Missing developer token'); } // Status should show missing developer token const res = await server.tools['manage_auth']({ action: 'status' }); const text = res.content[0].text as string; expect(text).toContain('GOOGLE_ADS_DEVELOPER_TOKEN: (not set)'); }, 15_000); it('provides recovery guidance after authentication failure', async () => { if (!hasRequiredEnv) return; // Simulate auth failure scenario cleanup = createTestAuthEnv({ GOOGLE_ADS_ACCESS_TOKEN: undefined, GOOGLE_APPLICATION_CREDENTIALS: '/non/existent/path.json', }); const server = new LiveServer(); registerTools(server as any); const res = await server.tools['manage_auth']({ action: 'status' }); const text = res.content[0].text as string; expect(text).toContain('Google Ads Auth Status'); // Should provide guidance for setting up authentication if (text.includes('No authentication') || text.includes('ADC not found')) { expect(text).toMatch(/(gcloud auth|GOOGLE_APPLICATION_CREDENTIALS|OAuth)/i); } }, 15_000); it('tests manage_auth recovery actions', async () => { if (!hasRequiredEnv) return; const server = new LiveServer(); registerTools(server as any); // Test refresh action for recovery const refreshRes = await server.tools['manage_auth']({ action: 'refresh', allow_subprocess: false }); const refreshText = refreshRes.content[0].text as string; expect(refreshText).toContain('gcloud auth application-default login'); expect(refreshText).toContain('cloud-platform'); expect(refreshText).toContain('adwords'); // Test switch action for account switching const switchRes = await server.tools['manage_auth']({ action: 'switch', config_name: 'work', allow_subprocess: false }); const switchText = switchRes.content[0].text as string; expect(switchText).toContain('gcloud config configurations activate work'); }, 15_000); it('validates error handling in different auth flows', async () => { if (!hasRequiredEnv) return; // Test that each auth method handles errors appropriately const server = new LiveServer(); registerTools(server as any); // Test with completely broken environment cleanup = createTestAuthEnv({ GOOGLE_ADS_ACCESS_TOKEN: 'invalid-token', GOOGLE_APPLICATION_CREDENTIALS: undefined, GOOGLE_OAUTH_CLIENT_ID: undefined, GOOGLE_OAUTH_CLIENT_SECRET: undefined, }); const res = await server.tools['manage_auth']({ action: 'status' }); const text = res.content[0].text as string; expect(text).toContain('Google Ads Auth Status'); // Should handle the broken state gracefully expect(text).toMatch(/(Auth type: env|Token available|ADC)/i); }, 15_000); it('tests timeout and retry scenarios', async () => { if (!hasRequiredEnv || !authDetails.hasValidADC) return; // Mock fetch to simulate slow responses const originalFetch = global.fetch; let callCount = 0; global.fetch = vi.fn().mockImplementation(async () => { callCount++; if (callCount === 1) { // First call times out await new Promise(resolve => setTimeout(resolve, 100)); throw new Error('Request timeout'); } // Second call succeeds (if retry logic exists) return new Response(JSON.stringify({ resourceNames: [] }), { status: 200, headers: { 'content-type': 'application/json' } }); }); const server = new LiveServer(); registerTools(server as any); try { const res = await server.tools['list_resources']({ kind: 'accounts', output_format: 'table' }); const text = res.content[0].text as string; // Should either succeed with retry or show appropriate error expect(text).toMatch(/(Accounts:|Error|timeout|failed)/i); } catch (error) { // Timeout errors are expected in this test expect(error).toBeDefined(); } finally { global.fetch = originalFetch; } }, 30_000); it('validates comprehensive error mapping coverage', async () => { if (!hasRequiredEnv) return; const { mapAdsErrorMsg } = await import('../../../src/utils/errorMapping.js'); // Test major error categories that the mapper handles const testCases = [ { status: 403, text: 'access_token_scope_insufficient' }, { status: 401, text: 'invalid token' }, { status: 403, text: 'developer token not ready' }, { status: 403, text: 'permission denied' }, { status: 400, text: 'queryerror invalid syntax' }, ]; for (const { status, text } of testCases) { const mapped = mapAdsErrorMsg(status, text); if (mapped) { expect(typeof mapped).toBe('string'); expect(mapped.length).toBeGreaterThan(10); } } }); });

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/martechery/mcp-google-ads-ts'

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