/** * Integration Test: ApiClient * * Tests the ApiClient infrastructure for making HTTP requests * - Validates request/response handling * - Tests error handling and timeouts * - Verifies health check functionality * * Focus: Infrastructure testing, NOT business logic */ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; import { ApiClient } from './api-client'; describe('ApiClient - Infrastructure Tests', () => { let apiClient: ApiClient; let mockServer: { close: () => void; port: number }; beforeAll(async () => { // Create a mock HTTP server for testing const http = require('http'); const server = http.createServer((req: any, res: any) => { if (req.url === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok' })); } else if (req.url === '/api/data') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: 'success', data: { id: 1, name: 'test' } })); } else if (req.url === '/api/error') { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Internal Server Error' })); } else if (req.url === '/api/slow') { // Simulate slow response setTimeout(() => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: 'slow response' })); }, 2000); } else { res.writeHead(404); res.end('Not Found'); } }); await new Promise((resolve) => { server.listen(0, () => { const port = (server.address() as any).port; mockServer = { close: () => server.close(), port }; apiClient = new ApiClient({ baseUrl: `http://localhost:${port}`, timeout: 5000 }); resolve(); }); }); }); afterAll(() => { if (mockServer) { mockServer.close(); } }); describe('GET Requests', () => { it('should successfully make a GET request', async () => { // Given: An API client configured with a mock server // When: Making a GET request to /api/data const result = await apiClient.get<{ message: string; data: { id: number; name: string } }>('/api/data'); // Then: The response should contain the expected data expect(result).toBeDefined(); expect(result.message).toBe('success'); expect(result.data.id).toBe(1); expect(result.data.name).toBe('test'); }); it('should handle GET request with custom headers', async () => { // Given: An API client configured with a mock server // When: Making a GET request with custom headers const result = await apiClient.get<{ message: string }>('/api/data', { 'X-Custom-Header': 'test-value', 'Authorization': 'Bearer token123', }); // Then: The request should succeed expect(result).toBeDefined(); expect(result.message).toBe('success'); }); }); describe('POST Requests', () => { it('should successfully make a POST request with body', async () => { // Given: An API client configured with a mock server const requestBody = { name: 'test', value: 123 }; // When: Making a POST request to /api/data const result = await apiClient.post<{ message: string; data: any }>('/api/data', requestBody); // Then: The response should contain the expected data expect(result).toBeDefined(); expect(result.message).toBe('success'); }); it('should handle POST request with custom headers', async () => { // Given: An API client configured with a mock server const requestBody = { test: 'data' }; // When: Making a POST request with custom headers const result = await apiClient.post<{ message: string }>('/api/data', requestBody, { 'X-Request-ID': 'test-123', }); // Then: The request should succeed expect(result).toBeDefined(); expect(result.message).toBe('success'); }); }); describe('PUT Requests', () => { it('should successfully make a PUT request with body', async () => { // Given: An API client configured with a mock server const requestBody = { id: 1, name: 'updated' }; // When: Making a PUT request to /api/data const result = await apiClient.put<{ message: string }>('/api/data', requestBody); // Then: The response should contain the expected data expect(result).toBeDefined(); expect(result.message).toBe('success'); }); }); describe('PATCH Requests', () => { it('should successfully make a PATCH request with body', async () => { // Given: An API client configured with a mock server const requestBody = { name: 'patched' }; // When: Making a PATCH request to /api/data const result = await apiClient.patch<{ message: string }>('/api/data', requestBody); // Then: The response should contain the expected data expect(result).toBeDefined(); expect(result.message).toBe('success'); }); }); describe('DELETE Requests', () => { it('should successfully make a DELETE request', async () => { // Given: An API client configured with a mock server // When: Making a DELETE request to /api/data const result = await apiClient.delete<{ message: string }>('/api/data'); // Then: The response should contain the expected data expect(result).toBeDefined(); expect(result.message).toBe('success'); }); }); describe('Error Handling', () => { it('should handle HTTP errors gracefully', async () => { // Given: An API client configured with a mock server // When: Making a request to an endpoint that returns an error // Then: Should throw an error with status code await expect(apiClient.get('/api/error')).rejects.toThrow('API Error 500'); }); it('should handle 404 errors', async () => { // Given: An API client configured with a mock server // When: Making a request to a non-existent endpoint // Then: Should throw an error await expect(apiClient.get('/non-existent')).rejects.toThrow(); }); it('should handle timeout errors', async () => { // Given: An API client with a short timeout const shortTimeoutClient = new ApiClient({ baseUrl: `http://localhost:${mockServer.port}`, timeout: 100, // 100ms timeout }); // When: Making a request to a slow endpoint // Then: Should throw a timeout error await expect(shortTimeoutClient.get('/api/slow')).rejects.toThrow('Request timeout after 100ms'); }); }); describe('Health Check', () => { it('should successfully check health endpoint', async () => { // Given: An API client configured with a mock server // When: Checking health const isHealthy = await apiClient.health(); // Then: Should return true if healthy expect(isHealthy).toBe(true); }); it('should return false when health check fails', async () => { // Given: An API client configured with a non-existent server const unhealthyClient = new ApiClient({ baseUrl: 'http://localhost:9999', // Non-existent server timeout: 100, }); // When: Checking health const isHealthy = await unhealthyClient.health(); // Then: Should return false expect(isHealthy).toBe(false); }); }); describe('Wait For Ready', () => { it('should wait for API to be ready', async () => { // Given: An API client configured with a mock server // When: Waiting for the API to be ready await apiClient.waitForReady(5000); // Then: Should complete without throwing // (This test passes if waitForReady completes successfully) expect(true).toBe(true); }); it('should timeout if API never becomes ready', async () => { // Given: An API client configured with a non-existent server const unhealthyClient = new ApiClient({ baseUrl: 'http://localhost:9999', timeout: 100, }); // When: Waiting for the API to be ready with a short timeout // Then: Should throw a timeout error await expect(unhealthyClient.waitForReady(500)).rejects.toThrow('API failed to become ready within 500ms'); }); }); describe('Request Configuration', () => { it('should use custom timeout', async () => { // Given: An API client with a custom timeout const customTimeoutClient = new ApiClient({ baseUrl: `http://localhost:${mockServer.port}`, timeout: 10000, // 10 seconds }); // When: Making a request const result = await customTimeoutClient.get<{ message: string }>('/api/data'); // Then: The request should succeed expect(result).toBeDefined(); expect(result.message).toBe('success'); }); it('should handle trailing slash in base URL', async () => { // Given: An API client with a base URL that has a trailing slash const clientWithTrailingSlash = new ApiClient({ baseUrl: `http://localhost:${mockServer.port}/`, timeout: 5000, }); // When: Making a request const result = await clientWithTrailingSlash.get<{ message: string }>('/api/data'); // Then: The request should succeed expect(result).toBeDefined(); expect(result.message).toBe('success'); }); }); });