/** * League API Tests * * This test suite performs comprehensive API testing for league-related endpoints. * It validates: * - Response structure matches expected DTO * - Required fields are present * - Data types are correct * - Edge cases (empty results, missing data) * - Business logic (sorting, filtering, calculations) * * 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, request } from '@playwright/test'; import * as fs from 'fs/promises'; import * as path from 'path'; interface TestResult { endpoint: string; method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; status: number; success: boolean; error?: string; response?: unknown; hasPresenterError: boolean; responseTime: number; } const API_BASE_URL = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101'; // Auth file paths const USER_AUTH_FILE = path.join(__dirname, '.auth/user-session.json'); const ADMIN_AUTH_FILE = path.join(__dirname, '.auth/admin-session.json'); test.describe('League API Tests', () => { const allResults: TestResult[] = []; let testResults: TestResult[] = []; test.beforeAll(async () => { console.log(`[LEAGUE API] Testing API at: ${API_BASE_URL}`); // Verify auth files exist const userAuthExists = await fs.access(USER_AUTH_FILE).then(() => true).catch(() => false); const adminAuthExists = await fs.access(ADMIN_AUTH_FILE).then(() => true).catch(() => false); if (!userAuthExists || !adminAuthExists) { throw new Error('Auth files not found. Run global setup first.'); } console.log('[LEAGUE API] Auth files verified'); }); test.afterAll(async () => { await generateReport(); }); test('League Discovery Endpoints - Public endpoints', async ({ request }) => { testResults = []; const endpoints = [ { method: 'GET' as const, path: '/leagues/all-with-capacity', name: 'Get all leagues with capacity' }, { method: 'GET' as const, path: '/leagues/all-with-capacity-and-scoring', name: 'Get all leagues with capacity and scoring' }, { method: 'GET' as const, path: '/leagues/total-leagues', name: 'Get total leagues count' }, { method: 'GET' as const, path: '/leagues/all', name: 'Get all leagues (alias)' }, { method: 'GET' as const, path: '/leagues/available', name: 'Get available leagues (alias)' }, ]; console.log(`\n[LEAGUE API] Testing ${endpoints.length} league discovery endpoints...`); for (const endpoint of endpoints) { await testEndpoint(request, endpoint); } // Check for failures const failures = testResults.filter(r => !r.success); if (failures.length > 0) { console.log('\n❌ FAILURES FOUND:'); failures.forEach(r => { console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`); }); } // Assert all endpoints succeeded expect(failures.length).toBe(0); }); test('League Discovery - Response structure validation', async ({ request }) => { testResults = []; // Test /leagues/all-with-capacity const allLeaguesResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`); expect(allLeaguesResponse.ok()).toBe(true); const allLeaguesData = await allLeaguesResponse.json(); expect(allLeaguesData).toHaveProperty('leagues'); expect(allLeaguesData).toHaveProperty('totalCount'); expect(Array.isArray(allLeaguesData.leagues)).toBe(true); expect(typeof allLeaguesData.totalCount).toBe('number'); // Validate league structure if leagues exist if (allLeaguesData.leagues.length > 0) { const league = allLeaguesData.leagues[0]; expect(league).toHaveProperty('id'); expect(league).toHaveProperty('name'); expect(league).toHaveProperty('description'); expect(league).toHaveProperty('ownerId'); expect(league).toHaveProperty('createdAt'); expect(league).toHaveProperty('settings'); expect(league.settings).toHaveProperty('maxDrivers'); expect(league).toHaveProperty('usedSlots'); // Validate data types expect(typeof league.id).toBe('string'); expect(typeof league.name).toBe('string'); expect(typeof league.description).toBe('string'); expect(typeof league.ownerId).toBe('string'); expect(typeof league.createdAt).toBe('string'); expect(typeof league.settings.maxDrivers).toBe('number'); expect(typeof league.usedSlots).toBe('number'); // Validate business logic: usedSlots <= maxDrivers expect(league.usedSlots).toBeLessThanOrEqual(league.settings.maxDrivers); } // Test /leagues/all-with-capacity-and-scoring const scoredLeaguesResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity-and-scoring`); expect(scoredLeaguesResponse.ok()).toBe(true); const scoredLeaguesData = await scoredLeaguesResponse.json(); expect(scoredLeaguesData).toHaveProperty('leagues'); expect(scoredLeaguesData).toHaveProperty('totalCount'); expect(Array.isArray(scoredLeaguesData.leagues)).toBe(true); // Validate scoring structure if leagues exist if (scoredLeaguesData.leagues.length > 0) { const league = scoredLeaguesData.leagues[0]; expect(league).toHaveProperty('scoring'); expect(league.scoring).toHaveProperty('gameId'); expect(league.scoring).toHaveProperty('scoringPresetId'); // Validate data types expect(typeof league.scoring.gameId).toBe('string'); expect(typeof league.scoring.scoringPresetId).toBe('string'); } // Test /leagues/total-leagues const totalResponse = await request.get(`${API_BASE_URL}/leagues/total-leagues`); expect(totalResponse.ok()).toBe(true); const totalData = await totalResponse.json(); expect(totalData).toHaveProperty('totalLeagues'); expect(typeof totalData.totalLeagues).toBe('number'); expect(totalData.totalLeagues).toBeGreaterThanOrEqual(0); // Validate consistency: totalCount from all-with-capacity should match totalLeagues expect(allLeaguesData.totalCount).toBe(totalData.totalLeagues); testResults.push({ endpoint: '/leagues/all-with-capacity', method: 'GET', status: allLeaguesResponse.status(), success: true, hasPresenterError: false, responseTime: 0, }); testResults.push({ endpoint: '/leagues/all-with-capacity-and-scoring', method: 'GET', status: scoredLeaguesResponse.status(), success: true, hasPresenterError: false, responseTime: 0, }); testResults.push({ endpoint: '/leagues/total-leagues', method: 'GET', status: totalResponse.status(), success: true, hasPresenterError: false, responseTime: 0, }); allResults.push(...testResults); }); test('League Detail Endpoints - Public endpoints', async ({ request }) => { testResults = []; // First, get a valid league ID from the discovery endpoint const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`); const discoveryData = await discoveryResponse.json(); if (discoveryData.leagues.length === 0) { console.log('[LEAGUE API] No leagues found, skipping detail endpoint tests'); return; } const leagueId = discoveryData.leagues[0].id; const endpoints = [ { method: 'GET' as const, path: `/leagues/${leagueId}`, name: 'Get league details' }, { method: 'GET' as const, path: `/leagues/${leagueId}/seasons`, name: 'Get league seasons' }, { method: 'GET' as const, path: `/leagues/${leagueId}/stats`, name: 'Get league stats' }, { method: 'GET' as const, path: `/leagues/${leagueId}/memberships`, name: 'Get league memberships' }, ]; console.log(`\n[LEAGUE API] Testing ${endpoints.length} league detail endpoints for league ${leagueId}...`); for (const endpoint of endpoints) { await testEndpoint(request, endpoint); } // Check for failures const failures = testResults.filter(r => !r.success); if (failures.length > 0) { console.log('\n❌ FAILURES FOUND:'); failures.forEach(r => { console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`); }); } // Assert all endpoints succeeded expect(failures.length).toBe(0); }); test('League Detail - Response structure validation', async ({ request }) => { testResults = []; // First, get a valid league ID from the discovery endpoint const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`); const discoveryData = await discoveryResponse.json(); if (discoveryData.leagues.length === 0) { console.log('[LEAGUE API] No leagues found, skipping detail validation tests'); return; } const leagueId = discoveryData.leagues[0].id; // Test /leagues/{id} const leagueResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}`); expect(leagueResponse.ok()).toBe(true); const leagueData = await leagueResponse.json(); expect(leagueData).toHaveProperty('id'); expect(leagueData).toHaveProperty('name'); expect(leagueData).toHaveProperty('description'); expect(leagueData).toHaveProperty('ownerId'); expect(leagueData).toHaveProperty('createdAt'); // Validate data types expect(typeof leagueData.id).toBe('string'); expect(typeof leagueData.name).toBe('string'); expect(typeof leagueData.description).toBe('string'); expect(typeof leagueData.ownerId).toBe('string'); expect(typeof leagueData.createdAt).toBe('string'); // Validate ID matches requested ID expect(leagueData.id).toBe(leagueId); // Test /leagues/{id}/seasons const seasonsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/seasons`); expect(seasonsResponse.ok()).toBe(true); const seasonsData = await seasonsResponse.json(); expect(Array.isArray(seasonsData)).toBe(true); // Validate season structure if seasons exist if (seasonsData.length > 0) { const season = seasonsData[0]; expect(season).toHaveProperty('id'); expect(season).toHaveProperty('name'); expect(season).toHaveProperty('status'); // Validate data types expect(typeof season.id).toBe('string'); expect(typeof season.name).toBe('string'); expect(typeof season.status).toBe('string'); } // Test /leagues/{id}/stats const statsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/stats`); expect(statsResponse.ok()).toBe(true); const statsData = await statsResponse.json(); expect(statsData).toHaveProperty('memberCount'); expect(statsData).toHaveProperty('raceCount'); expect(statsData).toHaveProperty('avgSOF'); // Validate data types expect(typeof statsData.memberCount).toBe('number'); expect(typeof statsData.raceCount).toBe('number'); expect(typeof statsData.avgSOF).toBe('number'); // Validate business logic: counts should be non-negative expect(statsData.memberCount).toBeGreaterThanOrEqual(0); expect(statsData.raceCount).toBeGreaterThanOrEqual(0); expect(statsData.avgSOF).toBeGreaterThanOrEqual(0); // Test /leagues/{id}/memberships const membershipsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/memberships`); expect(membershipsResponse.ok()).toBe(true); const membershipsData = await membershipsResponse.json(); expect(membershipsData).toHaveProperty('members'); expect(Array.isArray(membershipsData.members)).toBe(true); // Validate membership structure if members exist if (membershipsData.members.length > 0) { const member = membershipsData.members[0]; expect(member).toHaveProperty('driverId'); expect(member).toHaveProperty('role'); expect(member).toHaveProperty('joinedAt'); // Validate data types expect(typeof member.driverId).toBe('string'); expect(typeof member.role).toBe('string'); expect(typeof member.joinedAt).toBe('string'); // Validate business logic: at least one owner must exist const hasOwner = membershipsData.members.some((m: any) => m.role === 'owner'); expect(hasOwner).toBe(true); } testResults.push({ endpoint: `/leagues/${leagueId}`, method: 'GET', status: leagueResponse.status(), success: true, hasPresenterError: false, responseTime: 0, }); testResults.push({ endpoint: `/leagues/${leagueId}/seasons`, method: 'GET', status: seasonsResponse.status(), success: true, hasPresenterError: false, responseTime: 0, }); testResults.push({ endpoint: `/leagues/${leagueId}/stats`, method: 'GET', status: statsResponse.status(), success: true, hasPresenterError: false, responseTime: 0, }); testResults.push({ endpoint: `/leagues/${leagueId}/memberships`, method: 'GET', status: membershipsResponse.status(), success: true, hasPresenterError: false, responseTime: 0, }); allResults.push(...testResults); }); test('League Schedule Endpoints - Public endpoints', async ({ request }) => { testResults = []; // First, get a valid league ID from the discovery endpoint const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`); const discoveryData = await discoveryResponse.json(); if (discoveryData.leagues.length === 0) { console.log('[LEAGUE API] No leagues found, skipping schedule endpoint tests'); return; } const leagueId = discoveryData.leagues[0].id; const endpoints = [ { method: 'GET' as const, path: `/leagues/${leagueId}/schedule`, name: 'Get league schedule' }, ]; console.log(`\n[LEAGUE API] Testing ${endpoints.length} league schedule endpoints for league ${leagueId}...`); for (const endpoint of endpoints) { await testEndpoint(request, endpoint); } // Check for failures const failures = testResults.filter(r => !r.success); if (failures.length > 0) { console.log('\n❌ FAILURES FOUND:'); failures.forEach(r => { console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`); }); } // Assert all endpoints succeeded expect(failures.length).toBe(0); }); test('League Schedule - Response structure validation', async ({ request }) => { testResults = []; // First, get a valid league ID from the discovery endpoint const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`); const discoveryData = await discoveryResponse.json(); if (discoveryData.leagues.length === 0) { console.log('[LEAGUE API] No leagues found, skipping schedule validation tests'); return; } const leagueId = discoveryData.leagues[0].id; // Test /leagues/{id}/schedule const scheduleResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/schedule`); expect(scheduleResponse.ok()).toBe(true); const scheduleData = await scheduleResponse.json(); expect(scheduleData).toHaveProperty('seasonId'); expect(scheduleData).toHaveProperty('races'); expect(Array.isArray(scheduleData.races)).toBe(true); // Validate data types expect(typeof scheduleData.seasonId).toBe('string'); // Validate race structure if races exist if (scheduleData.races.length > 0) { const race = scheduleData.races[0]; expect(race).toHaveProperty('id'); expect(race).toHaveProperty('track'); expect(race).toHaveProperty('car'); expect(race).toHaveProperty('scheduledAt'); // Validate data types expect(typeof race.id).toBe('string'); expect(typeof race.track).toBe('string'); expect(typeof race.car).toBe('string'); expect(typeof race.scheduledAt).toBe('string'); // Validate business logic: races should be sorted by scheduledAt const scheduledTimes = scheduleData.races.map((r: any) => new Date(r.scheduledAt).getTime()); const sortedTimes = [...scheduledTimes].sort((a, b) => a - b); expect(scheduledTimes).toEqual(sortedTimes); } testResults.push({ endpoint: `/leagues/${leagueId}/schedule`, method: 'GET', status: scheduleResponse.status(), success: true, hasPresenterError: false, responseTime: 0, }); allResults.push(...testResults); }); test('League Standings Endpoints - Public endpoints', async ({ request }) => { testResults = []; // First, get a valid league ID from the discovery endpoint const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`); const discoveryData = await discoveryResponse.json(); if (discoveryData.leagues.length === 0) { console.log('[LEAGUE API] No leagues found, skipping standings endpoint tests'); return; } const leagueId = discoveryData.leagues[0].id; const endpoints = [ { method: 'GET' as const, path: `/leagues/${leagueId}/standings`, name: 'Get league standings' }, ]; console.log(`\n[LEAGUE API] Testing ${endpoints.length} league standings endpoints for league ${leagueId}...`); for (const endpoint of endpoints) { await testEndpoint(request, endpoint); } // Check for failures const failures = testResults.filter(r => !r.success); if (failures.length > 0) { console.log('\n❌ FAILURES FOUND:'); failures.forEach(r => { console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`); }); } // Assert all endpoints succeeded expect(failures.length).toBe(0); }); test('League Standings - Response structure validation', async ({ request }) => { testResults = []; // First, get a valid league ID from the discovery endpoint const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`); const discoveryData = await discoveryResponse.json(); if (discoveryData.leagues.length === 0) { console.log('[LEAGUE API] No leagues found, skipping standings validation tests'); return; } const leagueId = discoveryData.leagues[0].id; // Test /leagues/{id}/standings const standingsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/standings`); expect(standingsResponse.ok()).toBe(true); const standingsData = await standingsResponse.json(); expect(standingsData).toHaveProperty('standings'); expect(Array.isArray(standingsData.standings)).toBe(true); // Validate standing structure if standings exist if (standingsData.standings.length > 0) { const standing = standingsData.standings[0]; expect(standing).toHaveProperty('position'); expect(standing).toHaveProperty('driverId'); expect(standing).toHaveProperty('points'); expect(standing).toHaveProperty('races'); // Validate data types expect(typeof standing.position).toBe('number'); expect(typeof standing.driverId).toBe('string'); expect(typeof standing.points).toBe('number'); expect(typeof standing.races).toBe('number'); // Validate business logic: position must be sequential starting from 1 const positions = standingsData.standings.map((s: any) => s.position); const expectedPositions = Array.from({ length: positions.length }, (_, i) => i + 1); expect(positions).toEqual(expectedPositions); // Validate business logic: points must be non-negative expect(standing.points).toBeGreaterThanOrEqual(0); // Validate business logic: races count must be non-negative expect(standing.races).toBeGreaterThanOrEqual(0); } testResults.push({ endpoint: `/leagues/${leagueId}/standings`, method: 'GET', status: standingsResponse.status(), success: true, hasPresenterError: false, responseTime: 0, }); allResults.push(...testResults); }); test('Edge Cases - Invalid league IDs', async ({ request }) => { testResults = []; const endpoints = [ { method: 'GET' as const, path: '/leagues/non-existent-league-id', name: 'Get non-existent league' }, { method: 'GET' as const, path: '/leagues/non-existent-league-id/seasons', name: 'Get seasons for non-existent league' }, { method: 'GET' as const, path: '/leagues/non-existent-league-id/stats', name: 'Get stats for non-existent league' }, { method: 'GET' as const, path: '/leagues/non-existent-league-id/schedule', name: 'Get schedule for non-existent league' }, { method: 'GET' as const, path: '/leagues/non-existent-league-id/standings', name: 'Get standings for non-existent league' }, { method: 'GET' as const, path: '/leagues/non-existent-league-id/memberships', name: 'Get memberships for non-existent league' }, ]; console.log(`\n[LEAGUE API] Testing ${endpoints.length} edge case endpoints with invalid IDs...`); for (const endpoint of endpoints) { await testEndpoint(request, endpoint); } // Check for failures const failures = testResults.filter(r => !r.success); if (failures.length > 0) { console.log('\n❌ FAILURES FOUND:'); failures.forEach(r => { console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`); }); } // Assert all endpoints succeeded (404 is acceptable for non-existent resources) expect(failures.length).toBe(0); }); test('Edge Cases - Empty results', async ({ request }) => { testResults = []; // Test discovery endpoints with filters (if available) // Note: The current API doesn't seem to have filter parameters, but we test the base endpoints const endpoints = [ { method: 'GET' as const, path: '/leagues/all-with-capacity', name: 'Get all leagues (empty check)' }, { method: 'GET' as const, path: '/leagues/all-with-capacity-and-scoring', name: 'Get all leagues with scoring (empty check)' }, ]; console.log(`\n[LEAGUE API] Testing ${endpoints.length} endpoints for empty result handling...`); for (const endpoint of endpoints) { await testEndpoint(request, endpoint); } // Check for failures const failures = testResults.filter(r => !r.success); if (failures.length > 0) { console.log('\n❌ FAILURES FOUND:'); failures.forEach(r => { console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`); }); } // Assert all endpoints succeeded expect(failures.length).toBe(0); }); async function testEndpoint( request: import('@playwright/test').APIRequestContext, endpoint: { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; path: string; name?: string; body?: unknown; requiresAuth?: boolean } ): 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; const headers: Record = {}; // Playwright's request context handles cookies automatically // No need to set Authorization header for cookie-based auth switch (endpoint.method) { case 'GET': response = await request.get(fullUrl, { headers }); break; case 'POST': response = await request.post(fullUrl, { data: endpoint.body || {}, headers }); break; case 'PUT': response = await request.put(fullUrl, { data: endpoint.body || {}, headers }); break; case 'DELETE': response = await request.delete(fullUrl, { headers }); break; case 'PATCH': response = await request.patch(fullUrl, { data: endpoint.body || {}, headers }); 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')); // Success is 200-299 status, or 404 for non-existent resources, and no presenter error const isNotFound = status === 404; const success = (status >= 200 && status < 300 || isNotFound) && !hasPresenterError; const result: TestResult = { 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); allResults.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: TestResult = { 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); allResults.push(result); } } async function generateReport(): Promise { const summary = { total: allResults.length, success: allResults.filter(r => r.success).length, failed: allResults.filter(r => !r.success).length, presenterErrors: allResults.filter(r => r.hasPresenterError).length, avgResponseTime: allResults.reduce((sum, r) => sum + r.responseTime, 0) / allResults.length || 0, }; const report = { timestamp: new Date().toISOString(), summary, results: allResults, failures: allResults.filter(r => !r.success), }; // Write JSON report const jsonPath = path.join(__dirname, '../../../league-api-test-report.json'); await fs.writeFile(jsonPath, JSON.stringify(report, null, 2)); // Write Markdown report const mdPath = path.join(__dirname, '../../../league-api-test-report.md'); let md = `# League API 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 = allResults.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 = allResults.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`); } });