finished http-client tests

This commit is contained in:
Bojan Kucera 2025-06-04 16:54:11 -04:00
parent 557c157228
commit 1899078523
4 changed files with 204 additions and 46 deletions

View file

@ -1,7 +1,23 @@
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;
@ -34,25 +50,15 @@ describe('HttpClient Error Handling Integration', () => {
console.warn('URL validation test skipped');
}
});
test('should handle server errors (5xx)', async () => {
try {
await expect(
client.get('https://httpbin.org/status/500')
).rejects.toThrow();
} catch (e) {
if ((e as Error).message.includes('ENOTFOUND') ||
(e as Error).message.includes('Failed to fetch')) {
console.warn('Network connectivity issue detected - skipping test');
} else {
throw e;
}
}
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('https://httpbin.org/status/404')
client.get(`${mockServerBaseUrl}/status/404`)
).rejects.toThrow();
});
@ -62,36 +68,27 @@ describe('HttpClient Error Handling Integration', () => {
});
// This should not throw because we're allowing 404
const response = await customClient.get('https://httpbin.org/status/404');
const response = await customClient.get(`${mockServerBaseUrl}/status/404`);
expect(response.status).toBe(404);
});
});
describe('HttpClient Authentication Integration', () => {
test('should handle basic authentication', async () => {
try {
const client = new HttpClient({
timeout: 10000 // Longer timeout for network stability
});
const response = await client.get('https://httpbin.org/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');
} catch (e) {
if ((e as Error).message.includes('ENOTFOUND') ||
(e as Error).message.includes('Failed to fetch')) {
console.warn('Network connectivity issue detected - skipping test');
} else {
throw e;
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 () => {
@ -101,24 +98,23 @@ describe('HttpClient Authentication Integration', () => {
'Authorization': `Bearer ${token}`
}
});
const response = await client.get('https://httpbin.org/headers');
const response = await client.get(`${mockServerBaseUrl}/headers`);
expect(response.status).toBe(200);
expect(response.data.headers).toHaveProperty('Authorization', `Bearer ${token}`);
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
'x-api-key': apiKey
}
});
const response = await client.get('https://httpbin.org/headers');
const response = await client.get(`${mockServerBaseUrl}/headers`);
expect(response.status).toBe(200);
expect(response.data.headers).toHaveProperty('X-Api-Key', apiKey);
expect(response.data.headers).toHaveProperty('x-api-key', apiKey);
});
});

View file

@ -0,0 +1,49 @@
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

@ -0,0 +1,114 @@
/**
* Mock HTTP server for testing the HTTP client
* Replaces external dependency on httpbin.org with a local server
*/
export class MockServer {
private server: ReturnType<typeof Bun.serve> | null = null;
private port: number = 0;
/**
* Start the mock server on a random port
*/
async start(): Promise<void> {
this.server = Bun.serve({
port: 1, // Use any available port
fetch: this.handleRequest.bind(this),
error: this.handleError.bind(this),
});
this.port = this.server.port || 1;
console.log(`Mock server started on port ${this.port}`);
}
/**
* Stop the mock server
*/
async stop(): Promise<void> {
if (this.server) {
this.server.stop(true);
this.server = null;
this.port = 0;
console.log('Mock server stopped');
}
}
/**
* Get the base URL of the mock server
*/
getBaseUrl(): string {
if (!this.server) {
throw new Error('Server not started');
}
return `http://localhost:${this.port}`;
}
/**
* Handle incoming requests
*/ private async handleRequest(req: Request): Promise<Response> {
const url = new URL(req.url);
const path = url.pathname;
console.log(`Mock server handling request: ${req.method} ${path}`);
// Status endpoints
if (path.startsWith('/status/')) {
const status = parseInt(path.replace('/status/', ''), 10);
console.log(`Returning status: ${status}`);
return new Response(null, { status });
} // Headers endpoint
if (path === '/headers') {
const headers = Object.fromEntries([...req.headers.entries()]);
console.log('Headers endpoint called, received headers:', headers);
return Response.json({ headers });
} // Basic auth endpoint
if (path.startsWith('/basic-auth/')) {
const parts = path.split('/').filter(Boolean);
const expectedUsername = parts[1];
const expectedPassword = parts[2];
console.log(`Basic auth endpoint called: expected user=${expectedUsername}, pass=${expectedPassword}`);
const authHeader = req.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Basic ')) {
console.log('Missing or invalid Authorization header');
return new Response('Unauthorized', { status: 401 });
}
const base64Credentials = authHeader.split(' ')[1];
const credentials = atob(base64Credentials);
const [username, password] = credentials.split(':');
if (username === expectedUsername && password === expectedPassword) {
return Response.json({
authenticated: true,
user: username
});
}
return new Response('Unauthorized', { status: 401 });
}
// Echo request body
if (path === '/post' && req.method === 'POST') {
const data = await req.json();
return Response.json({
data,
headers: Object.fromEntries([...req.headers.entries()]),
method: req.method
});
}
// Default response
return Response.json({
url: req.url,
method: req.method,
headers: Object.fromEntries([...req.headers.entries()])
});
}
/**
* Handle errors
*/
private handleError(error: Error): Response {
return new Response('Server error', { status: 500 });
}
}

View file

@ -15,7 +15,6 @@
},
"devDependencies": {
"@types/jest": "^29.5.2",
"jest": "^29.5.0",
"typescript": "^5.4.5"
}
}