renamed http-client to http and fixed tests

This commit is contained in:
Bojan Kucera 2025-06-07 13:20:58 -04:00
parent e87acb5e18
commit 3d9afd711e
26 changed files with 472 additions and 496 deletions

View file

@ -113,7 +113,7 @@ stock-bot/
├── libs/ # ✅ Your existing shared libraries ├── libs/ # ✅ Your existing shared libraries
│ ├── config/ # ✅ Environment configuration │ ├── config/ # ✅ Environment configuration
│ ├── http-client/ # ✅ HTTP utilities │ ├── http/ # ✅ HTTP utilities
│ ├── logger/ # ✅ Loki-integrated logging │ ├── logger/ # ✅ Loki-integrated logging
│ ├── mongodb-client/ # ✅ MongoDB operations │ ├── mongodb-client/ # ✅ MongoDB operations
│ ├── postgres-client/ # ✅ PostgreSQL operations │ ├── postgres-client/ # ✅ PostgreSQL operations

View file

@ -18,7 +18,7 @@
"@stock-bot/questdb-client": "*", "@stock-bot/questdb-client": "*",
"@stock-bot/mongodb-client": "*", "@stock-bot/mongodb-client": "*",
"@stock-bot/event-bus": "*", "@stock-bot/event-bus": "*",
"@stock-bot/http-client": "*", "@stock-bot/http": "*",
"@stock-bot/cache": "*", "@stock-bot/cache": "*",
"hono": "^4.0.0", "hono": "^4.0.0",
"ws": "^8.0.0" "ws": "^8.0.0"

View file

@ -1,6 +1,6 @@
# Proxy Service # Proxy Service
A comprehensive proxy management service for the Stock Bot platform that integrates with existing libraries (Redis cache, logger, http-client) to provide robust proxy scraping, validation, and management capabilities. A comprehensive proxy management service for the Stock Bot platform that integrates with existing libraries (Redis cache, logger, http) to provide robust proxy scraping, validation, and management capabilities.
## Features ## Features
@ -9,7 +9,7 @@ A comprehensive proxy management service for the Stock Bot platform that integra
- **Redis Caching**: Stores proxy data with TTL and working status in Redis - **Redis Caching**: Stores proxy data with TTL and working status in Redis
- **Health Monitoring**: Periodic health checks for working proxies - **Health Monitoring**: Periodic health checks for working proxies
- **Structured Logging**: Comprehensive logging with the platform's logger - **Structured Logging**: Comprehensive logging with the platform's logger
- **HTTP Client Integration**: Seamless integration with the existing http-client library - **HTTP Client Integration**: Seamless integration with the existing http library
- **Background Processing**: Non-blocking proxy validation and refresh jobs - **Background Processing**: Non-blocking proxy validation and refresh jobs
## Quick Start ## Quick Start
@ -25,7 +25,7 @@ await proxyService.startHealthChecks(15 * 60 * 1000); // Health check every 15
const proxy = await proxyService.getWorkingProxy(); const proxy = await proxyService.getWorkingProxy();
// Use the proxy with HttpClient // Use the proxy with HttpClient
import { HttpClient } from '@stock-bot/http-client'; import { HttpClient } from '@stock-bot/http';
const client = new HttpClient({ proxy }); const client = new HttpClient({ proxy });
const response = await client.get('https://api.example.com/data'); const response = await client.get('https://api.example.com/data');
``` ```
@ -132,7 +132,7 @@ const customSource = {
```typescript ```typescript
import { proxyService } from './services/proxy.service.js'; import { proxyService } from './services/proxy.service.js';
import { HttpClient } from '@stock-bot/http-client'; import { HttpClient } from '@stock-bot/http';
async function fetchMarketDataWithProxy(symbol: string) { async function fetchMarketDataWithProxy(symbol: string) {
const proxy = await proxyService.getWorkingProxy(); const proxy = await proxyService.getWorkingProxy();
@ -235,7 +235,7 @@ All errors are logged with context and don't crash the service.
- `@stock-bot/cache`: Redis caching with TTL support - `@stock-bot/cache`: Redis caching with TTL support
- `@stock-bot/logger`: Structured logging with Loki integration - `@stock-bot/logger`: Structured logging with Loki integration
- `@stock-bot/http-client`: HTTP client with built-in proxy support - `@stock-bot/http`: HTTP client with built-in proxy support
- `ioredis`: Redis client (via cache library) - `ioredis`: Redis client (via cache library)
- `pino`: High-performance logging (via logger library) - `pino`: High-performance logging (via logger library)

View file

@ -69,7 +69,7 @@ const logger = getLogger('proxy-demo');
// if (workingProxies.length > 0) { // if (workingProxies.length > 0) {
// console.log('🔄 Example: Using proxy with HttpClient...'); // console.log('🔄 Example: Using proxy with HttpClient...');
// try { // try {
// const { HttpClient } = await import('@stock-bot/http-client'); // const { HttpClient } = await import('@stock-bot/http');
// const proxyClient = new HttpClient({ // const proxyClient = new HttpClient({
// proxy: workingProxies[0], // proxy: workingProxies[0],
// timeout: 10000 // timeout: 10000

View file

@ -1,6 +1,6 @@
import { Logger } from '@stock-bot/logger'; import { Logger } from '@stock-bot/logger';
import createCache, { type CacheProvider } from '@stock-bot/cache'; import createCache, { type CacheProvider } from '@stock-bot/cache';
import { HttpClient, HttpClientConfig, ProxyConfig , RequestConfig } from '@stock-bot/http-client'; import { HttpClient, HttpClientConfig, ProxyConfig , RequestConfig } from '@stock-bot/http';
export interface ProxySource { export interface ProxySource {
url: string; url: string;

View file

@ -20,7 +20,7 @@
"references": [ "references": [
{ "path": "../../libs/config" }, { "path": "../../libs/config" },
{ "path": "../../libs/logger" }, { "path": "../../libs/logger" },
{ "path": "../../libs/http-client" }, { "path": "../../libs/http" },
{ "path": "../../libs/types" }, { "path": "../../libs/types" },
{ "path": "../../libs/cache" }, { "path": "../../libs/cache" },
{ "path": "../../libs/utils" }, { "path": "../../libs/utils" },

View file

@ -83,13 +83,13 @@
"bun-types": "*", "bun-types": "*",
}, },
}, },
"libs/http-client": { "libs/http": {
"name": "@stock-bot/http-client", "name": "@stock-bot/http",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@stock-bot/logger": "*", "@stock-bot/logger": "*",
"@stock-bot/types": "*", "@stock-bot/types": "*",
"got": "^14.4.2", "got": "^14.4.7",
"http-proxy-agent": "^7.0.2", "http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"socks-proxy-agent": "^8.0.5", "socks-proxy-agent": "^8.0.5",
@ -294,7 +294,7 @@
"@stock-bot/event-bus": ["@stock-bot/event-bus@workspace:libs/event-bus"], "@stock-bot/event-bus": ["@stock-bot/event-bus@workspace:libs/event-bus"],
"@stock-bot/http-client": ["@stock-bot/http-client@workspace:libs/http-client"], "@stock-bot/http": ["@stock-bot/http@workspace:libs/http"],
"@stock-bot/logger": ["@stock-bot/logger@workspace:libs/logger"], "@stock-bot/logger": ["@stock-bot/logger@workspace:libs/logger"],

View file

@ -1,120 +0,0 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { HttpClient } from '../src/index.js';
import type { RateLimitConfig } from '../src/types.js';
import { MockServer } from './mock-server.js';
// Global mock server instance
let mockServer: MockServer;
let mockServerBaseUrl: string;
beforeAll(async () => {
// Start mock server
mockServer = new MockServer();
await mockServer.start();
mockServerBaseUrl = mockServer.getBaseUrl();
});
afterAll(async () => {
// Stop mock server
await mockServer.stop();
});
describe('HttpClient Error Handling Integration', () => {
let client: HttpClient;
beforeAll(() => {
client = new HttpClient({
timeout: 10000, // Increased timeout for network reliability
retries: 1
});
});
test('should handle network errors gracefully', async () => {
try {
await expect(
client.get('https://nonexistent-domain-that-will-fail-12345.test')
).rejects.toThrow();
} catch (e) {
console.warn('Network connectivity issue detected - skipping test');
}
});
test('should handle invalid URLs', async () => {
try {
// Note: with our improved URL handling, this might actually succeed now
// if the client auto-prepends https://, so we use a clearly invalid URL
await expect(
client.get('not:/a:valid/url')
).rejects.toThrow();
} catch (e) {
console.warn('URL validation test skipped');
}
});
test('should handle server errors (5xx)', async () => {
await expect(
client.get(`${mockServerBaseUrl}/status/500`)
).rejects.toThrow();
});
test('should treat 404 as error by default', async () => {
await expect(
client.get(`${mockServerBaseUrl}/status/404`)
).rejects.toThrow();
});
test('should respect custom validateStatus', async () => {
const customClient = new HttpClient({
validateStatus: (status) => status < 500
});
// This should not throw because we're allowing 404
const response = await customClient.get(`${mockServerBaseUrl}/status/404`);
expect(response.status).toBe(404);
});
});
describe('HttpClient Authentication Integration', () => {
test('should handle basic authentication', async () => {
const client = new HttpClient({
timeout: 10000 // Longer timeout for network stability
});
const response = await client.get(`${mockServerBaseUrl}/basic-auth/user/passwd`, {
auth: {
username: 'user',
password: 'passwd'
}
});
expect(response.status).toBe(200);
expect(response.data).toHaveProperty('authenticated', true);
expect(response.data).toHaveProperty('user', 'user');
});
test('should handle bearer token authentication', async () => {
const token = 'test-token-123';
const client = new HttpClient({
defaultHeaders: {
'authorization': `bearer ${token}`
}
});
const response = await client.get(`${mockServerBaseUrl}/headers`);
expect(response.status).toBe(200);
expect(response.data.headers).toHaveProperty('authorization', `bearer ${token}`);
});
test('should handle custom authentication headers', async () => {
const apiKey = 'api-key-123456';
const client = new HttpClient({
defaultHeaders: {
'x-api-key': apiKey
}
});
const response = await client.get(`${mockServerBaseUrl}/headers`);
expect(response.status).toBe(200);
expect(response.data.headers).toHaveProperty('x-api-key', apiKey);
});
});

View file

@ -1,302 +0,0 @@
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();
});
});

View file

@ -1,49 +0,0 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { MockServer } from './mock-server.js';
describe('Mock Server Tests', () => {
let mockServer: MockServer;
let baseUrl: string;
beforeAll(async () => {
mockServer = new MockServer();
await mockServer.start();
baseUrl = mockServer.getBaseUrl();
});
afterAll(async () => {
await mockServer.stop();
});
test('should return status codes', async () => {
const response = await fetch(`${baseUrl}/status/200`);
expect(response.status).toBe(200);
const response404 = await fetch(`${baseUrl}/status/404`);
expect(response404.status).toBe(404);
});
test('should handle headers endpoint', async () => {
const response = await fetch(`${baseUrl}/headers`, {
headers: {
'X-Test-Header': 'test-value'
}
});
expect(response.status).toBe(200);
const data = await response.json();
expect(data.headers['x-test-header']).toBe('test-value');
});
test('should handle basic auth', async () => {
const credentials = btoa('user:pass');
const response = await fetch(`${baseUrl}/basic-auth/user/pass`, {
headers: {
'Authorization': `Basic ${credentials}`
}
});
expect(response.status).toBe(200);
const data = await response.json();
expect(data.authenticated).toBe(true);
expect(data.user).toBe('user');
});
});

View file

@ -13,13 +13,13 @@ A comprehensive HTTP client library for the Stock Bot platform with built-in sup
## Installation ## Installation
```bash ```bash
bun add @stock-bot/http-client bun add @stock-bot/http
``` ```
## Basic Usage ## Basic Usage
```typescript ```typescript
import { HttpClient } from '@stock-bot/http-client'; import { HttpClient } from '@stock-bot/http';
// Create a client with default configuration // Create a client with default configuration
const client = new HttpClient(); const client = new HttpClient();
@ -38,7 +38,7 @@ const postResponse = await client.post('https://api.example.com/users', {
## Advanced Configuration ## Advanced Configuration
```typescript ```typescript
import { HttpClient } from '@stock-bot/http-client'; import { HttpClient } from '@stock-bot/http';
import { logger } from '@stock-bot/logger'; import { logger } from '@stock-bot/logger';
const client = new HttpClient({ const client = new HttpClient({
@ -135,7 +135,7 @@ const customResponse = await client.request({
## Error Handling ## Error Handling
```typescript ```typescript
import { HttpError, TimeoutError, RateLimitError } from '@stock-bot/http-client'; import { HttpError, TimeoutError, RateLimitError } from '@stock-bot/http';
try { try {
const response = await client.get('/api/data'); const response = await client.get('/api/data');
@ -224,7 +224,7 @@ const config: RequestConfig = {
```typescript ```typescript
import { logger } from '@stock-bot/logger'; import { logger } from '@stock-bot/logger';
import { HttpClient } from '@stock-bot/http-client'; import { HttpClient } from '@stock-bot/http';
const client = new HttpClient({ const client = new HttpClient({
baseURL: 'https://api.example.com' baseURL: 'https://api.example.com'

View file

@ -1,5 +1,5 @@
{ {
"name": "@stock-bot/http-client", "name": "@stock-bot/http",
"version": "1.0.0", "version": "1.0.0",
"description": "HTTP client library with proxy support, rate limiting, and timeout for Stock Bot platform", "description": "HTTP client library with proxy support, rate limiting, and timeout for Stock Bot platform",
"main": "src/index.ts", "main": "src/index.ts",

View file

@ -0,0 +1,156 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { HttpClient, HttpError } from '../src/index.js';
import { MockServer } from './mock-server.js';
/**
* Integration tests for HTTP client with real network scenarios
* These tests use external services and may be affected by network conditions
*/
let mockServer: MockServer;
let mockServerBaseUrl: string;
beforeAll(async () => {
mockServer = new MockServer();
await mockServer.start();
mockServerBaseUrl = mockServer.getBaseUrl();
});
afterAll(async () => {
await mockServer.stop();
});
describe('HTTP Integration Tests', () => {
let client: HttpClient;
beforeAll(() => {
client = new HttpClient({
timeout: 10000,
retries: 1
});
});
describe('Real-world scenarios', () => {
test('should handle JSON API responses', async () => {
try {
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');
expect(response.data).toHaveProperty('body');
} catch (error) {
console.warn('External API test skipped due to network issues:', error.message);
}
});
test('should handle large responses', async () => {
try {
const response = await client.get('https://jsonplaceholder.typicode.com/posts');
expect(response.status).toBe(200);
expect(Array.isArray(response.data)).toBe(true);
expect(response.data.length).toBeGreaterThan(0);
} catch (error) {
console.warn('Large response test skipped due to network issues:', error.message);
}
});
test('should handle POST with JSON data', async () => {
try {
const postData = {
title: 'Integration Test Post',
body: 'This is a test post from integration tests',
userId: 1
};
const response = await client.post('https://jsonplaceholder.typicode.com/posts', postData);
expect(response.status).toBe(201);
expect(response.data).toHaveProperty('id');
expect(response.data.title).toBe(postData.title);
} catch (error) {
console.warn('POST integration test skipped due to network issues:', error.message);
}
});
});
describe('Error scenarios with mock server', () => { test('should handle various HTTP status codes', async () => {
const successCodes = [200, 201];
const errorCodes = [400, 401, 403, 404, 500, 503];
// Test success codes
for (const statusCode of successCodes) {
const response = await client.get(`${mockServerBaseUrl}/status/${statusCode}`);
expect(response.status).toBe(statusCode);
}
// Test error codes (should throw HttpError)
for (const statusCode of errorCodes) {
await expect(
client.get(`${mockServerBaseUrl}/status/${statusCode}`)
).rejects.toThrow(HttpError);
}
});
test('should handle malformed responses gracefully', async () => {
// Mock server returns valid JSON, so this test verifies our client handles it properly
const response = await client.get(`${mockServerBaseUrl}/`);
expect(response.status).toBe(200);
expect(typeof response.data).toBe('object');
});
test('should handle concurrent requests', async () => {
const requests = Array.from({ length: 5 }, (_, i) =>
client.get(`${mockServerBaseUrl}/`, {
headers: { 'X-Request-ID': `req-${i}` }
})
);
const responses = await Promise.all(requests);
responses.forEach((response, index) => {
expect(response.status).toBe(200);
expect(response.data.headers).toHaveProperty('x-request-id', `req-${index}`);
});
});
});
describe('Performance and reliability', () => {
test('should handle rapid sequential requests', async () => {
const startTime = Date.now();
const requests = [];
for (let i = 0; i < 10; i++) {
requests.push(client.get(`${mockServerBaseUrl}/`));
}
const responses = await Promise.all(requests);
const endTime = Date.now();
expect(responses).toHaveLength(10);
responses.forEach(response => {
expect(response.status).toBe(200);
});
console.log(`Completed 10 requests in ${endTime - startTime}ms`);
});
test('should maintain connection efficiency', async () => {
const clientWithKeepAlive = new HttpClient({
timeout: 5000,
keepAlive: true
});
const requests = Array.from({ length: 3 }, () =>
clientWithKeepAlive.get(`${mockServerBaseUrl}/`)
);
const responses = await Promise.all(requests);
responses.forEach(response => {
expect(response.status).toBe(200);
});
});
});
});

161
libs/http/test/http.test.ts Normal file
View file

@ -0,0 +1,161 @@
import { describe, test, expect, beforeEach, beforeAll, afterAll } from 'bun:test';
import { HttpClient, HttpError, ProxyManager } from '../src/index.js';
import type { ProxyConfig } from '../src/types.js';
import { MockServer } from './mock-server.js';
// Global mock server instance
let mockServer: MockServer;
let mockServerBaseUrl: string;
beforeAll(async () => {
// Start mock server for all tests
mockServer = new MockServer();
await mockServer.start();
mockServerBaseUrl = mockServer.getBaseUrl();
});
afterAll(async () => {
// Stop mock server
await mockServer.stop();
});
describe('HttpClient', () => {
let client: HttpClient;
beforeEach(() => {
client = new HttpClient();
});
describe('Basic functionality', () => {
test('should create client with default config', () => {
expect(client).toBeInstanceOf(HttpClient);
});
test('should make GET request', async () => {
const response = await client.get(`${mockServerBaseUrl}/`);
expect(response.status).toBe(200);
expect(response.data).toHaveProperty('url');
expect(response.data).toHaveProperty('method', 'GET');
});
test('should make POST request with body', async () => {
const testData = {
title: 'Test Post',
body: 'Test body',
userId: 1,
};
const response = await client.post(`${mockServerBaseUrl}/post`, testData);
expect(response.status).toBe(200);
expect(response.data).toHaveProperty('data');
expect(response.data.data).toEqual(testData);
});
test('should handle custom headers', async () => {
const customHeaders = {
'X-Custom-Header': 'test-value',
'User-Agent': 'StockBot-HTTP-Client/1.0'
};
const response = await client.get(`${mockServerBaseUrl}/headers`, {
headers: customHeaders
});
expect(response.status).toBe(200);
expect(response.data.headers).toHaveProperty('x-custom-header', 'test-value');
expect(response.data.headers).toHaveProperty('user-agent', 'StockBot-HTTP-Client/1.0');
});
test('should handle timeout', async () => {
const clientWithTimeout = new HttpClient({ timeout: 1 }); // 1ms timeout
await expect(
clientWithTimeout.get('https://httpbin.org/delay/1')
).rejects.toThrow();
});
});
describe('Error handling', () => {
test('should handle HTTP errors', async () => {
await expect(
client.get(`${mockServerBaseUrl}/status/404`)
).rejects.toThrow(HttpError);
});
test('should handle network errors gracefully', async () => {
await expect(
client.get('https://nonexistent-domain-that-will-fail-12345.test')
).rejects.toThrow();
});
test('should handle invalid URLs', async () => {
await expect(
client.get('not:/a:valid/url')
).rejects.toThrow();
});
});
describe('HTTP methods', () => {
test('should make PUT request', async () => {
const testData = { id: 1, name: 'Updated' };
const response = await client.put(`${mockServerBaseUrl}/post`, testData);
expect(response.status).toBe(200);
});
test('should make DELETE request', async () => {
const response = await client.del(`${mockServerBaseUrl}/`);
expect(response.status).toBe(200);
expect(response.data.method).toBe('DELETE');
});
test('should make PATCH request', async () => {
const testData = { name: 'Patched' };
const response = await client.patch(`${mockServerBaseUrl}/post`, testData);
expect(response.status).toBe(200);
});
});
});
describe('ProxyManager', () => {
test('should determine when to use Bun fetch', () => {
const httpProxy: ProxyConfig = {
protocol: 'http',
host: 'proxy.example.com',
port: 8080
};
const socksProxy: ProxyConfig = {
protocol: 'socks5',
host: 'proxy.example.com',
port: 1080
};
expect(ProxyManager.shouldUseBunFetch(httpProxy)).toBe(true);
expect(ProxyManager.shouldUseBunFetch(socksProxy)).toBe(false);
});
test('should create proxy URL for Bun fetch', () => {
const proxy: ProxyConfig = {
protocol: 'http',
host: 'proxy.example.com',
port: 8080,
username: 'user',
password: 'pass'
};
const proxyUrl = ProxyManager.createBunProxyUrl(proxy);
expect(proxyUrl).toBe('http://user:pass@proxy.example.com:8080');
});
test('should create proxy URL without credentials', () => {
const proxy: ProxyConfig = {
protocol: 'https',
host: 'proxy.example.com',
port: 8080
};
const proxyUrl = ProxyManager.createBunProxyUrl(proxy);
expect(proxyUrl).toBe('https://proxy.example.com:8080');
});
});

View file

@ -0,0 +1,131 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { MockServer } from './mock-server.js';
/**
* Tests for the MockServer utility
* Ensures our test infrastructure works correctly
*/
describe('MockServer', () => {
let mockServer: MockServer;
let baseUrl: string;
beforeAll(async () => {
mockServer = new MockServer();
await mockServer.start();
baseUrl = mockServer.getBaseUrl();
});
afterAll(async () => {
await mockServer.stop();
});
describe('Server lifecycle', () => {
test('should start and provide base URL', () => {
expect(baseUrl).toMatch(/^http:\/\/localhost:\d+$/);
expect(mockServer.getBaseUrl()).toBe(baseUrl);
});
test('should be reachable', async () => {
const response = await fetch(`${baseUrl}/`);
expect(response.ok).toBe(true);
});
});
describe('Status endpoints', () => {
test('should return correct status codes', async () => {
const statusCodes = [200, 201, 400, 401, 403, 404, 500, 503];
for (const status of statusCodes) {
const response = await fetch(`${baseUrl}/status/${status}`);
expect(response.status).toBe(status);
}
});
});
describe('Headers endpoint', () => {
test('should echo request headers', async () => {
const response = await fetch(`${baseUrl}/headers`, {
headers: {
'X-Test-Header': 'test-value',
'User-Agent': 'MockServer-Test'
} });
expect(response.ok).toBe(true);
const data = await response.json();
expect(data.headers).toHaveProperty('x-test-header', 'test-value');
expect(data.headers).toHaveProperty('user-agent', 'MockServer-Test');
});
});
describe('Basic auth endpoint', () => {
test('should authenticate valid credentials', async () => {
const username = 'testuser';
const password = 'testpass';
const credentials = btoa(`${username}:${password}`);
const response = await fetch(`${baseUrl}/basic-auth/${username}/${password}`, {
headers: {
'Authorization': `Basic ${credentials}`
}
});
expect(response.ok).toBe(true);
const data = await response.json();
expect(data.authenticated).toBe(true);
expect(data.user).toBe(username);
});
test('should reject invalid credentials', async () => {
const credentials = btoa('wrong:credentials');
const response = await fetch(`${baseUrl}/basic-auth/user/pass`, {
headers: {
'Authorization': `Basic ${credentials}`
}
});
expect(response.status).toBe(401);
});
test('should reject missing auth header', async () => {
const response = await fetch(`${baseUrl}/basic-auth/user/pass`);
expect(response.status).toBe(401);
});
});
describe('POST endpoint', () => {
test('should echo POST data', async () => {
const testData = {
message: 'Hello, MockServer!',
timestamp: Date.now()
};
const response = await fetch(`${baseUrl}/post`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(testData)
});
expect(response.ok).toBe(true);
const data = await response.json();
expect(data.data).toEqual(testData);
expect(data.method).toBe('POST');
expect(data.headers).toHaveProperty('content-type', 'application/json');
});
});
describe('Default endpoint', () => {
test('should return request information', async () => {
const response = await fetch(`${baseUrl}/unknown-endpoint`);
expect(response.ok).toBe(true);
const data = await response.json();
expect(data.url).toBe(`${baseUrl}/unknown-endpoint`);
expect(data.method).toBe('GET');
expect(data.headers).toBeDefined();
});
});
});

View file

@ -14,6 +14,6 @@
{ "path": "../types" }, { "path": "../types" },
{ "path": "../event-bus" }, { "path": "../event-bus" },
{ "path": "../data-frame" }, { "path": "../data-frame" },
{ "path": "../http-client" }, { "path": "../http" },
] ]
} }

View file

@ -7,9 +7,8 @@ $libs = @(
"types", # Base types - no dependencies "types", # Base types - no dependencies
"logger", # Logging utilities - depends on types "logger", # Logging utilities - depends on types
"config", # Configuration - depends on types "config", # Configuration - depends on types
"utils", # Utilities - depends on types and config "utils", # Utilities - depends on types and config "cache", # Cache - depends on types and logger
"cache", # Cache - depends on types and logger "http", # HTTP client - depends on types, config, logger
"http-client", # HTTP client - depends on types, config, logger
"postgres-client", # PostgreSQL client - depends on types, config, logger "postgres-client", # PostgreSQL client - depends on types, config, logger
"mongodb-client", # MongoDB client - depends on types, config, logger "mongodb-client", # MongoDB client - depends on types, config, logger
"questdb-client" # QuestDB client - depends on types, config, logger "questdb-client" # QuestDB client - depends on types, config, logger

View file

@ -41,7 +41,7 @@
"dist" "dist"
], "references": [ ], "references": [
{ "path": "./libs/config" }, { "path": "./libs/config" },
{ "path": "./libs/http-client" }, { "path": "./libs/http" },
{ "path": "./libs/logger" }, { "path": "./libs/logger" },
{ "path": "./libs/mongodb-client" }, { "path": "./libs/mongodb-client" },
{ "path": "./libs/postgres-client" }, { "path": "./libs/postgres-client" },