264 lines
9.3 KiB
TypeScript
264 lines
9.3 KiB
TypeScript
/**
|
|
* 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<void>((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');
|
|
});
|
|
});
|
|
});
|