From 18990785238f788f1cf7814a60806eabe975cdb3 Mon Sep 17 00:00:00 2001 From: Bojan Kucera Date: Wed, 4 Jun 2025 16:54:11 -0400 Subject: [PATCH] finished http-client tests --- .../test/http-client-integration.test.ts | 86 +++++++------ libs/http-client/test/mock-server.test.ts | 49 ++++++++ libs/http-client/test/mock-server.ts | 114 ++++++++++++++++++ libs/utils/package.json | 1 - 4 files changed, 204 insertions(+), 46 deletions(-) create mode 100644 libs/http-client/test/mock-server.test.ts create mode 100644 libs/http-client/test/mock-server.ts diff --git a/libs/http-client/test/http-client-integration.test.ts b/libs/http-client/test/http-client-integration.test.ts index 2b348e2..3775d24 100644 --- a/libs/http-client/test/http-client-integration.test.ts +++ b/libs/http-client/test/http-client-integration.test.ts @@ -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); }); }); diff --git a/libs/http-client/test/mock-server.test.ts b/libs/http-client/test/mock-server.test.ts new file mode 100644 index 0000000..f6ef929 --- /dev/null +++ b/libs/http-client/test/mock-server.test.ts @@ -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'); + }); +}); diff --git a/libs/http-client/test/mock-server.ts b/libs/http-client/test/mock-server.ts new file mode 100644 index 0000000..e5c3d03 --- /dev/null +++ b/libs/http-client/test/mock-server.ts @@ -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 | null = null; + private port: number = 0; + + /** + * Start the mock server on a random port + */ + async start(): Promise { + 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 { + 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 { + 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 }); + } +} diff --git a/libs/utils/package.json b/libs/utils/package.json index e23d452..48cf390 100644 --- a/libs/utils/package.json +++ b/libs/utils/package.json @@ -15,7 +15,6 @@ }, "devDependencies": { "@types/jest": "^29.5.2", - "jest": "^29.5.0", "typescript": "^5.4.5" } }