302 lines
8.9 KiB
TypeScript
302 lines
8.9 KiB
TypeScript
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();
|
|
});
|
|
});
|