/** * API Smoke Test * * This test performs true e2e testing of all API endpoints by making direct HTTP requests * to the running API server. It tests for: * - Basic connectivity and response codes * - Presenter errors ("Presenter not presented") * - Response format validation * - Error handling * * This test is designed to run in the Docker e2e environment and can be executed with: * npm run test:e2e:website (which runs everything in Docker) */ import { test, expect } from '@playwright/test'; import * as fs from 'fs/promises'; import * as path from 'path'; interface EndpointTestResult { endpoint: string; method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; status: number; success: boolean; error?: string; response?: unknown; hasPresenterError: boolean; responseTime: number; } interface TestReport { timestamp: string; summary: { total: number; success: number; failed: number; presenterErrors: number; avgResponseTime: number; }; results: EndpointTestResult[]; failures: EndpointTestResult[]; } const API_BASE_URL = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101'; test.describe('API Smoke Tests', () => { let testResults: EndpointTestResult[] = []; test.beforeAll(async ({ request }) => { console.log(`[API SMOKE] Testing API at: ${API_BASE_URL}`); // Wait for API to be ready const maxAttempts = 30; let apiReady = false; for (let i = 0; i < maxAttempts; i++) { try { const response = await request.get(`${API_BASE_URL}/health`); if (response.ok()) { apiReady = true; console.log(`[API SMOKE] API is ready after ${i + 1} attempts`); break; } } catch (error) { // Continue trying } await new Promise(resolve => setTimeout(resolve, 1000)); } if (!apiReady) { throw new Error('API failed to become ready'); } }); test.afterAll(async () => { await generateReport(); }); test('all public GET endpoints respond correctly', async ({ request }) => { const endpoints = [ // Race endpoints { method: 'GET' as const, path: '/races/all', name: 'Get all races' }, { method: 'GET' as const, path: '/races/total-races', name: 'Get total races count' }, { method: 'GET' as const, path: '/races/page-data', name: 'Get races page data' }, { method: 'GET' as const, path: '/races/all/page-data', name: 'Get all races page data' }, { method: 'GET' as const, path: '/races/reference/penalty-types', name: 'Get penalty types reference' }, // League endpoints { method: 'GET' as const, path: '/leagues/all', name: 'Get all leagues' }, { method: 'GET' as const, path: '/leagues/available', name: 'Get available leagues' }, // Team endpoints { method: 'GET' as const, path: '/teams/all', name: 'Get all teams' }, // Driver endpoints { method: 'GET' as const, path: '/drivers/leaderboard', name: 'Get driver leaderboard' }, { method: 'GET' as const, path: '/drivers/total-drivers', name: 'Get total drivers count' }, // Dashboard endpoints (may require auth, but should handle gracefully) { method: 'GET' as const, path: '/dashboard/overview', name: 'Get dashboard overview' }, // Analytics endpoints { method: 'GET' as const, path: '/analytics/metrics', name: 'Get analytics metrics' }, // Sponsor endpoints { method: 'GET' as const, path: '/sponsors/pricing', name: 'Get sponsorship pricing' }, // Payments endpoints { method: 'GET' as const, path: '/payments/wallet', name: 'Get wallet (may require auth)' }, // Notifications endpoints { method: 'GET' as const, path: '/notifications/unread', name: 'Get unread notifications' }, // Features endpoint { method: 'GET' as const, path: '/features', name: 'Get feature flags' }, ]; console.log(`\n[API SMOKE] Testing ${endpoints.length} public endpoints...`); for (const endpoint of endpoints) { await testEndpoint(request, endpoint); } // Check for presenter errors const presenterErrors = testResults.filter(r => r.hasPresenterError); if (presenterErrors.length > 0) { console.log('\n❌ PRESENTER ERRORS FOUND:'); presenterErrors.forEach(r => { console.log(` ${r.method} ${r.endpoint} - ${r.error}`); }); } // Assert no presenter errors expect(presenterErrors.length).toBe(0); }); test('POST endpoints handle requests gracefully', async ({ request }) => { const endpoints = [ { method: 'POST' as const, path: '/auth/login', name: 'Login', body: { email: 'test@example.com', password: 'test' } }, { method: 'POST' as const, path: '/auth/signup', name: 'Signup', body: { email: 'test@example.com', password: 'test', name: 'Test User' } }, { method: 'POST' as const, path: '/races/123/register', name: 'Register for race', body: { driverId: 'test-driver' } }, { method: 'POST' as const, path: '/races/protests/file', name: 'File protest', body: { raceId: '123', driverId: '456', description: 'Test protest' } }, { method: 'POST' as const, path: '/leagues/123/join', name: 'Join league', body: { driverId: 'test-driver' } }, { method: 'POST' as const, path: '/teams/123/join', name: 'Join team', body: { driverId: 'test-driver' } }, ]; console.log(`\n[API SMOKE] Testing ${endpoints.length} POST endpoints...`); for (const endpoint of endpoints) { await testEndpoint(request, endpoint); } // Check for presenter errors const presenterErrors = testResults.filter(r => r.hasPresenterError); expect(presenterErrors.length).toBe(0); }); test('parameterized endpoints handle missing IDs gracefully', async ({ request }) => { const endpoints = [ { method: 'GET' as const, path: '/races/non-existent-id', name: 'Get non-existent race' }, { method: 'GET' as const, path: '/races/non-existent-id/results', name: 'Get non-existent race results' }, { method: 'GET' as const, path: '/leagues/non-existent-id', name: 'Get non-existent league' }, { method: 'GET' as const, path: '/teams/non-existent-id', name: 'Get non-existent team' }, { method: 'GET' as const, path: '/drivers/non-existent-id', name: 'Get non-existent driver' }, { method: 'GET' as const, path: '/media/avatar/non-existent-id', name: 'Get non-existent avatar' }, ]; console.log(`\n[API SMOKE] Testing ${endpoints.length} parameterized endpoints with invalid IDs...`); for (const endpoint of endpoints) { await testEndpoint(request, endpoint); } // Check for presenter errors const presenterErrors = testResults.filter(r => r.hasPresenterError); expect(presenterErrors.length).toBe(0); }); async function testEndpoint( request: import('@playwright/test').APIRequestContext, endpoint: { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; path: string; name?: string; body?: unknown } ): Promise { const startTime = Date.now(); const fullUrl = `${API_BASE_URL}${endpoint.path}`; console.log(`\n[TEST] ${endpoint.method} ${endpoint.path} (${endpoint.name || 'Unknown'})`); try { let response; switch (endpoint.method) { case 'GET': response = await request.get(fullUrl); break; case 'POST': response = await request.post(fullUrl, { data: endpoint.body || {} }); break; case 'PUT': response = await request.put(fullUrl, { data: endpoint.body || {} }); break; case 'DELETE': response = await request.delete(fullUrl); break; case 'PATCH': response = await request.patch(fullUrl, { data: endpoint.body || {} }); break; } const responseTime = Date.now() - startTime; const status = response.status(); const body = await response.json().catch(() => null); const bodyText = await response.text().catch(() => ''); // Check for presenter errors const hasPresenterError = bodyText.includes('Presenter not presented') || bodyText.includes('presenter not presented') || (body && body.message && body.message.includes('Presenter not presented')) || (body && body.error && body.error.includes('Presenter not presented')); const success = status < 400 && !hasPresenterError; const result: EndpointTestResult = { endpoint: endpoint.path, method: endpoint.method, status, success, hasPresenterError, responseTime, response: body || bodyText.substring(0, 200), }; if (!success) { result.error = body?.message || bodyText.substring(0, 200); } testResults.push(result); if (hasPresenterError) { console.log(` ❌ PRESENTER ERROR: ${status} - ${body?.message || bodyText.substring(0, 100)}`); } else if (success) { console.log(` ✅ ${status} (${responseTime}ms)`); } else { console.log(` ⚠️ ${status} (${responseTime}ms) - ${body?.message || 'Error'}`); } } catch (error: unknown) { const responseTime = Date.now() - startTime; const errorString = error instanceof Error ? error.message : String(error); const result: EndpointTestResult = { endpoint: endpoint.path, method: endpoint.method, status: 0, success: false, hasPresenterError: false, responseTime, error: errorString, }; // Check if it's a presenter error if (errorString.includes('Presenter not presented')) { result.hasPresenterError = true; console.log(` ❌ PRESENTER ERROR (exception): ${errorString}`); } else { console.log(` ❌ EXCEPTION: ${errorString}`); } testResults.push(result); } } async function generateReport(): Promise { const summary = { total: testResults.length, success: testResults.filter(r => r.success).length, failed: testResults.filter(r => !r.success).length, presenterErrors: testResults.filter(r => r.hasPresenterError).length, avgResponseTime: testResults.reduce((sum, r) => sum + r.responseTime, 0) / testResults.length || 0, }; const report: TestReport = { timestamp: new Date().toISOString(), summary, results: testResults, failures: testResults.filter(r => !r.success), }; // Write JSON report const jsonPath = path.join(__dirname, '../../../api-smoke-report.json'); await fs.writeFile(jsonPath, JSON.stringify(report, null, 2)); // Write Markdown report const mdPath = path.join(__dirname, '../../../api-smoke-report.md'); let md = `# API Smoke Test Report\n\n`; md += `**Generated:** ${new Date().toISOString()}\n`; md += `**API Base URL:** ${API_BASE_URL}\n\n`; md += `## Summary\n\n`; md += `- **Total Endpoints:** ${summary.total}\n`; md += `- **✅ Success:** ${summary.success}\n`; md += `- **❌ Failed:** ${summary.failed}\n`; md += `- **⚠️ Presenter Errors:** ${summary.presenterErrors}\n`; md += `- **Avg Response Time:** ${summary.avgResponseTime.toFixed(2)}ms\n\n`; if (summary.presenterErrors > 0) { md += `## Presenter Errors\n\n`; const presenterFailures = testResults.filter(r => r.hasPresenterError); presenterFailures.forEach((r, i) => { md += `${i + 1}. **${r.method} ${r.endpoint}**\n`; md += ` - Status: ${r.status}\n`; md += ` - Error: ${r.error || 'No error message'}\n\n`; }); } if (summary.failed > 0 && summary.presenterErrors < summary.failed) { md += `## Other Failures\n\n`; const otherFailures = testResults.filter(r => !r.success && !r.hasPresenterError); otherFailures.forEach((r, i) => { md += `${i + 1}. **${r.method} ${r.endpoint}**\n`; md += ` - Status: ${r.status}\n`; md += ` - Error: ${r.error || 'No error message'}\n\n`; }); } await fs.writeFile(mdPath, md); console.log(`\n📊 Reports generated:`); console.log(` JSON: ${jsonPath}`); console.log(` Markdown: ${mdPath}`); console.log(`\nSummary: ${summary.success}/${summary.total} passed, ${summary.presenterErrors} presenter errors`); } });