renamed http-client to http and fixed tests
This commit is contained in:
parent
e87acb5e18
commit
3d9afd711e
26 changed files with 472 additions and 496 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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" },
|
||||||
|
|
|
||||||
8
bun.lock
8
bun.lock
|
|
@ -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"],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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'
|
||||||
|
|
@ -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",
|
||||||
156
libs/http/test/http-integration.test.ts
Normal file
156
libs/http/test/http-integration.test.ts
Normal 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
161
libs/http/test/http.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
131
libs/http/test/mock-server.test.ts
Normal file
131
libs/http/test/mock-server.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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" },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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" },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue