import { describe, test, expect, beforeEach } from 'bun:test'; import { HttpClient, RateLimiter, ProxyManager } from '../src/index.js'; import type { RateLimitConfig, ProxyConfig } from '../src/types.js'; import { RateLimitError } from '../src/types.js'; describe('HttpClient', () => { let client: HttpClient; beforeEach(() => { client = new HttpClient(); }); test('should create client with default config', () => { expect(client).toBeInstanceOf(HttpClient); }); test('should make GET request', async () => { // Using a mock endpoint that returns JSON const response = await client.get('https://jsonplaceholder.typicode.com/posts/1'); expect(response.status).toBe(200); expect(response.data).toHaveProperty('id'); expect(response.data).toHaveProperty('title'); }); test('should handle timeout', async () => { const clientWithTimeout = new HttpClient({ timeout: 1 }); // 1ms timeout await expect( clientWithTimeout.get('https://jsonplaceholder.typicode.com/posts/1') ).rejects.toThrow('timeout'); }); test('should handle 404 error', async () => { await expect( client.get('https://jsonplaceholder.typicode.com/posts/999999') ).rejects.toThrow(); }); test('should make POST request with body', async () => { const response = await client.post('https://jsonplaceholder.typicode.com/posts', { title: 'Test Post', body: 'Test body', userId: 1, }); expect(response.status).toBe(201); expect(response.data).toHaveProperty('id'); }); test('should use base URL', async () => { const clientWithBase = new HttpClient({ baseURL: 'https://jsonplaceholder.typicode.com' }); const response = await clientWithBase.get('/posts/1'); expect(response.status).toBe(200); }); test('should merge headers', async () => { const clientWithHeaders = new HttpClient({ defaultHeaders: { 'X-API-Key': 'test-key', } }); const response = await clientWithHeaders.get('https://jsonplaceholder.typicode.com/posts/1', { headers: { 'X-Custom': 'custom-value', } }); expect(response.status).toBe(200); }); test('should make PUT request', async () => { const response = await client.put('https://jsonplaceholder.typicode.com/posts/1', { id: 1, title: 'Updated Post', body: 'Updated content', userId: 1, }); expect(response.status).toBe(200); expect(response.data).toHaveProperty('id'); expect(response.data).toHaveProperty('title', 'Updated Post'); }); test('should make DELETE request', async () => { const response = await client.delete('https://jsonplaceholder.typicode.com/posts/1'); expect(response.status).toBe(200); // JSONPlaceholder typically returns an empty object for DELETE expect(response.data).toEqual({}); }); test('should make PATCH request', async () => { const response = await client.patch('https://jsonplaceholder.typicode.com/posts/1', { title: 'Patched Title', }); expect(response.status).toBe(200); expect(response.data).toHaveProperty('title', 'Patched Title'); }); test('should handle HTTP redirect', async () => { // Using httpbin.org to test redirect const response = await client.get('https://httpbin.org/redirect-to?url=https://httpbin.org/get'); expect(response.status).toBe(200); expect(response.data).toBeDefined(); }); test('should handle custom status validation', async () => { const customClient = new HttpClient({ validateStatus: (status) => status < 500, }); // This 404 should not throw because we're allowing any status < 500 const response = await customClient.get('https://jsonplaceholder.typicode.com/posts/999999'); expect(response.status).toBe(404); }); test('should retry failed requests', async () => { const retryClient = new HttpClient({ retries: 2, retryDelay: 100, }); // Using a deliberately wrong URL that will fail await expect( retryClient.get('https://nonexistent-domain-123456789.com') ).rejects.toThrow(); // We can't easily test a successful retry without mocking, // but at least we can confirm it handles failures gracefully }); test('should send query parameters', async () => { const params = { userId: 1, completed: false, }; const response = await client.get('https://jsonplaceholder.typicode.com/todos', { params, }); expect(response.status).toBe(200); expect(Array.isArray(response.data)).toBe(true); if (response.data.length > 0) { expect(response.data[0].userId).toBe(1); } }); test('should handle form data', async () => { const formData = new FormData(); formData.append('title', 'Form Data Test'); formData.append('body', 'This is a test with form data'); formData.append('userId', '1'); const response = await client.post('https://jsonplaceholder.typicode.com/posts', formData); expect(response.status).toBe(201); expect(response.data).toHaveProperty('id'); }); test('should handle request timeout configuration', async () => { const client1 = new HttpClient(); const client2 = new HttpClient({ timeout: 5000 }); // Just ensure they can both make a request without errors await expect(client1.get('https://jsonplaceholder.typicode.com/posts/1')).resolves.toBeDefined(); await expect(client2.get('https://jsonplaceholder.typicode.com/posts/1')).resolves.toBeDefined(); }); }); describe('RateLimiter', () => { let rateLimiter: RateLimiter; const config: RateLimitConfig = { maxRequests: 2, windowMs: 1000, // 1 second }; beforeEach(() => { rateLimiter = new RateLimiter(config); }); test('should allow requests within limit', async () => { await expect(rateLimiter.checkRateLimit()).resolves.toBeUndefined(); rateLimiter.recordRequest(true); await expect(rateLimiter.checkRateLimit()).resolves.toBeUndefined(); rateLimiter.recordRequest(true); }); test('should reject requests exceeding limit', async () => { // Fill up the rate limit await rateLimiter.checkRateLimit(); rateLimiter.recordRequest(true); await rateLimiter.checkRateLimit(); rateLimiter.recordRequest(true); // This should throw await expect(rateLimiter.checkRateLimit()).rejects.toThrow(RateLimitError); }); test('should reset after window', async () => { const shortConfig: RateLimitConfig = { maxRequests: 1, windowMs: 100, // 100ms }; const shortRateLimiter = new RateLimiter(shortConfig); await shortRateLimiter.checkRateLimit(); shortRateLimiter.recordRequest(true); // Should be at limit await expect(shortRateLimiter.checkRateLimit()).rejects.toThrow(RateLimitError); // Wait for window to reset await new Promise(resolve => setTimeout(resolve, 150)); // Should be allowed again await expect(shortRateLimiter.checkRateLimit()).resolves.toBeUndefined(); }); test('should get current count', () => { expect(rateLimiter.getCurrentCount()).toBe(0); rateLimiter.recordRequest(true); expect(rateLimiter.getCurrentCount()).toBe(1); rateLimiter.recordRequest(false); expect(rateLimiter.getCurrentCount()).toBe(2); }); }); describe('ProxyManager', () => { test('should validate proxy config', () => { const validConfig: ProxyConfig = { type: 'http', host: 'proxy.example.com', port: 8080, }; expect(() => ProxyManager.validateConfig(validConfig)).not.toThrow(); }); test('should reject invalid proxy config', () => { const invalidConfig = { type: 'http', host: '', port: 8080, } as ProxyConfig; expect(() => ProxyManager.validateConfig(invalidConfig)).toThrow('Proxy host is required'); }); test('should reject invalid port', () => { const invalidConfig = { type: 'http', host: 'proxy.example.com', port: 70000, } as ProxyConfig; expect(() => ProxyManager.validateConfig(invalidConfig)).toThrow('Invalid proxy port'); }); test('should create HTTP proxy agent', () => { const config: ProxyConfig = { type: 'http', host: 'proxy.example.com', port: 8080, }; const agent = ProxyManager.createAgent(config); expect(agent).toBeDefined(); expect(agent).toBeDefined(); }); test('should create SOCKS proxy agent', () => { const config: ProxyConfig = { type: 'socks5', host: 'proxy.example.com', port: 1080, }; const agent = ProxyManager.createAgent(config); expect(agent).toBeDefined(); }); test('should handle authenticated proxy', () => { const config: ProxyConfig = { type: 'http', host: 'proxy.example.com', port: 8080, username: 'user', password: 'pass' }; const agent = ProxyManager.createAgent(config); expect(agent).toBeDefined(); }); });