integration tests cleanup

This commit is contained in:
2026-01-23 12:56:53 +01:00
parent 6df38a462a
commit a00ca4edfd
83 changed files with 0 additions and 5324 deletions

View File

@@ -1,17 +0,0 @@
{
"cookies": [
{
"name": "gp_session",
"value": "gp_9f9c4115-2a02-4be7-9aec-72ddb3c7cdbf",
"domain": "localhost",
"path": "/",
"expires": -1,
"httpOnly": true,
"secure": false,
"sameSite": "Lax"
}
],
"userId": "68fd953d-4f4a-47b6-83b9-ec361238e4f1",
"email": "smoke-test-1767897520573@example.com",
"password": "Password123"
}

View File

@@ -1,244 +0,0 @@
# API Smoke Tests
This directory contains true end-to-end API smoke tests that make direct HTTP requests to the running API server to validate endpoint functionality and detect issues like "presenter not presented" errors.
## Overview
The API smoke tests are designed to:
1. **Test all public API endpoints** - Make requests to discover and validate endpoints
2. **Detect presenter errors** - Identify use cases that return errors without calling `this.output.present()`
3. **Validate response formats** - Ensure endpoints return proper data structures
4. **Test error handling** - Verify graceful handling of invalid inputs
5. **Generate detailed reports** - Create JSON and Markdown reports of findings
## Files
- `api-smoke.test.ts` - Main Playwright test file
- `README.md` - This documentation
## Usage
### Local Testing
Run the API smoke tests against a locally running API:
```bash
# Start the API server (in one terminal)
npm run docker:dev:up
# Run smoke tests (in another terminal)
npm run test:api:smoke
```
### Docker Testing (Recommended)
Run the tests in the full Docker e2e environment:
```bash
# Start the complete e2e environment
npm run docker:e2e:up
# Run smoke tests in Docker
npm run test:api:smoke:docker
# Or use the unified command
npm run test:e2e:website # This runs all e2e tests including API smoke
```
### CI/CD Integration
Add to your CI pipeline:
```yaml
# GitHub Actions example
- name: Start E2E Environment
run: npm run docker:e2e:up
- name: Run API Smoke Tests
run: npm run test:api:smoke:docker
- name: Upload Test Reports
uses: actions/upload-artifact@v3
with:
name: api-smoke-reports
path: |
api-smoke-report.json
api-smoke-report.md
playwright-report/
```
## Test Coverage
The smoke tests cover:
### Race Endpoints
- `/races/all` - Get all races
- `/races/total-races` - Get total count
- `/races/page-data` - Get paginated data
- `/races/reference/penalty-types` - Reference data
- `/races/{id}` - Race details (with invalid IDs)
- `/races/{id}/results` - Race results
- `/races/{id}/sof` - Strength of field
- `/races/{id}/protests` - Protests
- `/races/{id}/penalties` - Penalties
### League Endpoints
- `/leagues/all` - All leagues
- `/leagues/available` - Available leagues
- `/leagues/{id}` - League details
- `/leagues/{id}/standings` - Standings
- `/leagues/{id}/schedule` - Schedule
### Team Endpoints
- `/teams/all` - All teams
- `/teams/{id}` - Team details
- `/teams/{id}/members` - Team members
### Driver Endpoints
- `/drivers/leaderboard` - Leaderboard
- `/drivers/total-drivers` - Total count
- `/drivers/{id}` - Driver details
### Media Endpoints
- `/media/avatar/{id}` - Avatar retrieval
- `/media/{id}` - Media retrieval
### Sponsor Endpoints
- `/sponsors/pricing` - Sponsorship pricing
- `/sponsors/dashboard` - Sponsor dashboard
- `/sponsors/{id}` - Sponsor details
### Auth Endpoints
- `/auth/login` - Login
- `/auth/signup` - Signup
- `/auth/session` - Session info
### Dashboard Endpoints
- `/dashboard/overview` - Overview
- `/dashboard/feed` - Activity feed
### Analytics Endpoints
- `/analytics/metrics` - Metrics
- `/analytics/dashboard` - Dashboard data
### Admin Endpoints
- `/admin/users` - User management
### Protest Endpoints
- `/protests/race/{id}` - Race protests
### Payment Endpoints
- `/payments/wallet` - Wallet info
### Notification Endpoints
- `/notifications/unread` - Unread notifications
### Feature Flags
- `/features` - Feature flag configuration
## Reports
After running tests, three reports are generated:
1. **`api-smoke-report.json`** - Detailed JSON report with all test results
2. **`api-smoke-report.md`** - Human-readable Markdown report
3. **Playwright HTML report** - Interactive test report (in `playwright-report/`)
### Report Structure
```json
{
"timestamp": "2024-01-07T22:00:00Z",
"summary": {
"total": 50,
"success": 45,
"failed": 5,
"presenterErrors": 3,
"avgResponseTime": 45.2
},
"results": [...],
"failures": [...]
}
```
## Detecting Presenter Errors
The test specifically looks for the "Presenter not presented" error pattern:
```typescript
// Detects these patterns:
- "Presenter not presented"
- "presenter not presented"
- Error messages containing these phrases
```
When found, these are flagged as **presenter errors** and require immediate attention.
## Troubleshooting
### API Not Ready
If tests fail because API isn't ready:
```bash
# Check API health
curl http://localhost:3101/health
# Wait longer in test setup (increase timeout in test file)
```
### Port Conflicts
```bash
# Stop conflicting services
npm run docker:e2e:down
# Check what's running
docker-compose -f docker-compose.e2e.yml ps
```
### Missing Data
The tests expect seeded data. If you see 404s:
```bash
# Ensure bootstrap is enabled
export GRIDPILOT_API_BOOTSTRAP=1
# Restart services
npm run docker:e2e:clean && npm run docker:e2e:up
```
## Integration with Existing Tests
This smoke test complements the existing test suite:
- **Unit tests** (`apps/api/src/**/*Service.test.ts`) - Test individual services
- **Integration tests** (`tests/integration/`) - Test component interactions
- **E2E website tests** (`tests/e2e/website/`) - Test website functionality
- **API smoke tests** (this) - Test API endpoints directly
## Best Practices
1. **Run before deployments** - Catch presenter errors before they reach production
2. **Run in CI/CD** - Automated regression testing
3. **Review reports** - Always check the generated reports
4. **Fix presenter errors immediately** - They indicate missing `.present()` calls
5. **Keep tests updated** - Add new endpoints as they're created
## Performance
- Typical runtime: 30-60 seconds
- Parallel execution: Playwright runs tests in parallel by default
- Response time tracking: All requests are timed
- Average response time tracked in reports
## Maintenance
When adding new endpoints:
1. Add them to the test arrays in `api-smoke.test.ts`
2. Test locally first: `npm run test:api:smoke`
3. Verify reports show expected results
4. Commit updated test file
When fixing presenter errors:
1. Run smoke test to identify failing endpoints
2. Check the specific error messages
3. Fix the use case to call `this.output.present()` before returning
4. Re-run smoke test to verify fix

View File

@@ -1,122 +0,0 @@
/**
* API Authentication Setup for E2E Tests
*
* This setup creates authentication sessions for both regular and admin users
* that are persisted across all tests in the suite.
*/
import { test as setup } from '@playwright/test';
import * as fs from 'fs/promises';
import * as path from 'path';
const API_BASE_URL = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
// Define 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');
setup('Authenticate regular user', async ({ request }) => {
console.log(`[AUTH SETUP] Creating regular user session 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(`[AUTH SETUP] 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');
}
// Create test user and establish cookie-based session
const testEmail = `smoke-test-${Date.now()}@example.com`;
const testPassword = 'Password123';
// Signup
const signupResponse = await request.post(`${API_BASE_URL}/auth/signup`, {
data: {
email: testEmail,
password: testPassword,
displayName: 'Smoke Tester',
username: `smokeuser${Date.now()}`
}
});
if (!signupResponse.ok()) {
throw new Error(`Signup failed: ${signupResponse.status()}`);
}
const signupData = await signupResponse.json();
const testUserId = signupData?.user?.userId ?? null;
console.log('[AUTH SETUP] Test user created:', testUserId);
// Login to establish cookie session
const loginResponse = await request.post(`${API_BASE_URL}/auth/login`, {
data: {
email: testEmail,
password: testPassword
}
});
if (!loginResponse.ok()) {
throw new Error(`Login failed: ${loginResponse.status()}`);
}
console.log('[AUTH SETUP] Regular user session established');
// Get cookies and save to auth file
const context = request.context();
const cookies = context.cookies();
// Ensure auth directory exists
await fs.mkdir(path.dirname(USER_AUTH_FILE), { recursive: true });
// Save cookies to file
await fs.writeFile(USER_AUTH_FILE, JSON.stringify({ cookies }, null, 2));
console.log(`[AUTH SETUP] Saved user session to: ${USER_AUTH_FILE}`);
});
setup('Authenticate admin user', async ({ request }) => {
console.log(`[AUTH SETUP] Creating admin user session at: ${API_BASE_URL}`);
// Use seeded admin credentials
const adminEmail = 'demo.admin@example.com';
const adminPassword = 'Demo1234!';
// Login as admin
const loginResponse = await request.post(`${API_BASE_URL}/auth/login`, {
data: {
email: adminEmail,
password: adminPassword
}
});
if (!loginResponse.ok()) {
throw new Error(`Admin login failed: ${loginResponse.status()}`);
}
console.log('[AUTH SETUP] Admin user session established');
// Get cookies and save to auth file
const context = request.context();
const cookies = context.cookies();
// Ensure auth directory exists
await fs.mkdir(path.dirname(ADMIN_AUTH_FILE), { recursive: true });
// Save cookies to file
await fs.writeFile(ADMIN_AUTH_FILE, JSON.stringify({ cookies }, null, 2));
console.log(`[AUTH SETUP] Saved admin session to: ${ADMIN_AUTH_FILE}`);
});

View File

@@ -1,412 +0,0 @@
/**
* 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, request } 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';
// 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('API Smoke Tests', () => {
// Aggregate across the whole suite (used for final report).
const allResults: EndpointTestResult[] = [];
let testResults: EndpointTestResult[] = [];
test.beforeAll(async () => {
console.log(`[API SMOKE] 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('[API SMOKE] Auth files verified');
});
test.afterAll(async () => {
await generateReport();
});
test('all public GET endpoints respond correctly', async ({ request }) => {
testResults = [];
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-with-capacity', 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' },
// Sponsor endpoints
{ method: 'GET' as const, path: '/sponsors/pricing', name: 'Get sponsorship pricing' },
// Features endpoint
{ method: 'GET' as const, path: '/features', name: 'Get feature flags' },
// Hello endpoint
{ method: 'GET' as const, path: '/hello', name: 'Hello World' },
// Media endpoints
{ method: 'GET' as const, path: '/media/avatar/non-existent-id', name: 'Get non-existent avatar' },
// Driver by ID
{ method: 'GET' as const, path: '/drivers/non-existent-id', name: 'Get non-existent driver' },
];
console.log(`\n[API SMOKE] Testing ${endpoints.length} public GET 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('POST endpoints handle requests gracefully', async ({ request }) => {
testResults = [];
const endpoints = [
// Auth endpoints (no auth required)
{ method: 'POST' as const, path: '/auth/signup', name: 'Signup', requiresAuth: false, body: { email: `test-smoke-${Date.now()}@example.com`, password: 'Password123', displayName: 'Smoke Test', username: 'smoketest' } },
{ method: 'POST' as const, path: '/auth/login', name: 'Login', requiresAuth: false, body: { email: 'demo.driver@example.com', password: 'Demo1234!' } },
// Protected endpoints (require auth)
{ method: 'POST' as const, path: '/races/123/register', name: 'Register for race', requiresAuth: true, body: { driverId: 'test-driver' } },
{ method: 'POST' as const, path: '/races/protests/file', name: 'File protest', requiresAuth: true, body: { raceId: '123', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', incident: { lap: 1, description: 'Test protest' } } },
{ method: 'POST' as const, path: '/leagues/league-1/join', name: 'Join league', requiresAuth: true, body: {} },
{ method: 'POST' as const, path: '/teams/123/join', name: 'Join team', requiresAuth: true, body: { teamId: '123' } },
];
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 }) => {
testResults = [];
const endpoints = [
{ method: 'GET' as const, path: '/races/non-existent-id', name: 'Get non-existent race', requiresAuth: false },
{ method: 'GET' as const, path: '/races/non-existent-id/results', name: 'Get non-existent race results', requiresAuth: false },
{ method: 'GET' as const, path: '/leagues/non-existent-id', name: 'Get non-existent league', requiresAuth: false },
{ method: 'GET' as const, path: '/teams/non-existent-id', name: 'Get non-existent team', requiresAuth: false },
{ method: 'GET' as const, path: '/drivers/non-existent-id', name: 'Get non-existent driver', requiresAuth: false },
{ method: 'GET' as const, path: '/media/avatar/non-existent-id', name: 'Get non-existent avatar', requiresAuth: false },
];
console.log(`\n[API SMOKE] Testing ${endpoints.length} parameterized endpoints with invalid IDs...`);
for (const endpoint of endpoints) {
await testEndpoint(request, endpoint);
}
// Check for failures
const failures = testResults.filter(r => !r.success);
expect(failures.length).toBe(0);
});
test('authenticated endpoints respond correctly', async () => {
testResults = [];
// Load user auth cookies
const userAuthData = await fs.readFile(USER_AUTH_FILE, 'utf-8');
const userCookies = JSON.parse(userAuthData).cookies;
// Create new API request context with user auth
const userContext = await request.newContext({
storageState: {
cookies: userCookies,
origins: [{ origin: API_BASE_URL, localStorage: [] }]
}
});
const endpoints = [
// Dashboard
{ method: 'GET' as const, path: '/dashboard/overview', name: 'Dashboard Overview' },
// Analytics
{ method: 'GET' as const, path: '/analytics/metrics', name: 'Analytics Metrics' },
// Notifications
{ method: 'GET' as const, path: '/notifications/unread', name: 'Unread Notifications' },
];
console.log(`\n[API SMOKE] Testing ${endpoints.length} authenticated endpoints...`);
for (const endpoint of endpoints) {
await testEndpoint(userContext, endpoint);
}
// Check for presenter errors
const presenterErrors = testResults.filter(r => r.hasPresenterError);
expect(presenterErrors.length).toBe(0);
// Clean up
await userContext.dispose();
});
test('admin endpoints respond correctly', async () => {
testResults = [];
// Load admin auth cookies
const adminAuthData = await fs.readFile(ADMIN_AUTH_FILE, 'utf-8');
const adminCookies = JSON.parse(adminAuthData).cookies;
// Create new API request context with admin auth
const adminContext = await request.newContext({
storageState: {
cookies: adminCookies,
origins: [{ origin: API_BASE_URL, localStorage: [] }]
}
});
const endpoints = [
// Payments (requires admin capability)
{ method: 'GET' as const, path: '/payments/wallets?leagueId=league-1', name: 'Wallets' },
];
console.log(`\n[API SMOKE] Testing ${endpoints.length} admin endpoints...`);
for (const endpoint of endpoints) {
await testEndpoint(adminContext, endpoint);
}
// Check for presenter errors
const presenterErrors = testResults.filter(r => r.hasPresenterError);
expect(presenterErrors.length).toBe(0);
// Clean up
await adminContext.dispose();
});
async function testEndpoint(
request: import('@playwright/test').APIRequestContext,
endpoint: { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; path: string; name?: string; body?: unknown; requiresAuth?: boolean }
): Promise<void> {
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<string, string> = {};
// 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: 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);
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: 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);
allResults.push(result);
}
}
async function generateReport(): Promise<void> {
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: TestReport = {
timestamp: new Date().toISOString(),
summary,
results: allResults,
failures: allResults.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 = 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`);
}
});

View File

@@ -1,782 +0,0 @@
/**
* 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<void> {
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<string, string> = {};
// 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<void> {
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`);
}
});