import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import { fetch } from '../src/fetch'; describe('Enhanced Fetch', () => { let originalFetch: typeof globalThis.fetch; let mockFetch: any; let mockLogger: any; beforeEach(() => { originalFetch = globalThis.fetch; mockFetch = mock(() => Promise.resolve(new Response('test'))); globalThis.fetch = mockFetch; mockLogger = { debug: mock(() => {}), info: mock(() => {}), error: mock(() => {}), }; }); afterEach(() => { globalThis.fetch = originalFetch; }); describe('basic fetch', () => { it('should make simple GET request', async () => { const mockResponse = new Response('test data', { status: 200 }); mockFetch.mockResolvedValue(mockResponse); const response = await fetch('https://api.example.com/data'); expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', { method: 'GET', headers: {}, }); expect(response).toBe(mockResponse); }); it('should make POST request with body', async () => { const mockResponse = new Response('created', { status: 201 }); mockFetch.mockResolvedValue(mockResponse); const body = JSON.stringify({ name: 'test' }); const response = await fetch('https://api.example.com/data', { method: 'POST', body, headers: { 'Content-Type': 'application/json' }, }); expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', { method: 'POST', body, headers: { 'Content-Type': 'application/json' }, }); expect(response).toBe(mockResponse); }); it('should handle URL objects', async () => { const mockResponse = new Response('test'); mockFetch.mockResolvedValue(mockResponse); const url = new URL('https://api.example.com/data'); await fetch(url); expect(mockFetch).toHaveBeenCalledWith(url, expect.any(Object)); }); it('should handle Request objects', async () => { const mockResponse = new Response('test'); mockFetch.mockResolvedValue(mockResponse); const request = new Request('https://api.example.com/data', { method: 'PUT', }); await fetch(request); expect(mockFetch).toHaveBeenCalledWith(request, expect.any(Object)); }); }); describe('proxy support', () => { it('should add proxy to request options', async () => { const mockResponse = new Response('proxy test'); mockFetch.mockResolvedValue(mockResponse); await fetch('https://api.example.com/data', { proxy: 'http://proxy.example.com:8080', }); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/data', expect.objectContaining({ proxy: 'http://proxy.example.com:8080', }) ); }); it('should handle null proxy', async () => { const mockResponse = new Response('no proxy'); mockFetch.mockResolvedValue(mockResponse); await fetch('https://api.example.com/data', { proxy: null, }); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/data', expect.not.objectContaining({ proxy: expect.anything(), }) ); }); }); describe('timeout support', () => { it('should handle timeout', async () => { mockFetch.mockImplementation((url, options) => { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => resolve(new Response('delayed')), 100); // Listen for abort signal if (options?.signal) { options.signal.addEventListener('abort', () => { clearTimeout(timeoutId); reject(new DOMException('The operation was aborted', 'AbortError')); }); } }); }); await expect(fetch('https://api.example.com/data', { timeout: 50 })).rejects.toThrow( 'The operation was aborted' ); }); it('should clear timeout on success', async () => { const mockResponse = new Response('quick response'); mockFetch.mockResolvedValue(mockResponse); const response = await fetch('https://api.example.com/data', { timeout: 1000, }); expect(response).toBe(mockResponse); }); it('should clear timeout on error', async () => { mockFetch.mockRejectedValue(new Error('Network error')); await expect(fetch('https://api.example.com/data', { timeout: 1000 })).rejects.toThrow( 'Network error' ); }); }); describe('logging', () => { it('should log request details', async () => { const mockResponse = new Response('test', { status: 200, statusText: 'OK', headers: new Headers({ 'content-type': 'text/plain' }), }); mockFetch.mockResolvedValue(mockResponse); await fetch('https://api.example.com/data', { logger: mockLogger, method: 'POST', headers: { Authorization: 'Bearer token' }, }); expect(mockLogger.debug).toHaveBeenCalledWith('HTTP request', { method: 'POST', url: 'https://api.example.com/data', headers: { Authorization: 'Bearer token' }, proxy: null, }); expect(mockLogger.debug).toHaveBeenCalledWith('HTTP response', { url: 'https://api.example.com/data', status: 200, statusText: 'OK', ok: true, headers: { 'content-type': 'text/plain' }, }); }); it('should log errors', async () => { const error = new Error('Connection failed'); mockFetch.mockRejectedValue(error); await expect(fetch('https://api.example.com/data', { logger: mockLogger })).rejects.toThrow( 'Connection failed' ); expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', { url: 'https://api.example.com/data', error: 'Connection failed', name: 'Error', }); }); it('should use console as default logger', async () => { const consoleSpy = mock(console.debug); console.debug = consoleSpy; const mockResponse = new Response('test'); mockFetch.mockResolvedValue(mockResponse); await fetch('https://api.example.com/data'); expect(consoleSpy).toHaveBeenCalledTimes(2); // Request and response console.debug = originalFetch as any; }); }); describe('request options', () => { it('should forward all standard RequestInit options', async () => { const mockResponse = new Response('test'); mockFetch.mockResolvedValue(mockResponse); const controller = new AbortController(); const options = { method: 'PATCH' as const, headers: { 'X-Custom': 'value' }, body: 'data', signal: controller.signal, credentials: 'include' as const, cache: 'no-store' as const, redirect: 'manual' as const, referrer: 'https://referrer.com', referrerPolicy: 'no-referrer' as const, integrity: 'sha256-hash', keepalive: true, mode: 'cors' as const, }; await fetch('https://api.example.com/data', options); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/data', expect.objectContaining(options) ); }); it('should handle undefined options', async () => { const mockResponse = new Response('test'); mockFetch.mockResolvedValue(mockResponse); await fetch('https://api.example.com/data', undefined); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/data', expect.objectContaining({ method: 'GET', headers: {}, }) ); }); }); describe('error handling', () => { it('should propagate fetch errors', async () => { const error = new TypeError('Failed to fetch'); mockFetch.mockRejectedValue(error); await expect(fetch('https://api.example.com/data')).rejects.toThrow('Failed to fetch'); }); it('should handle non-Error objects', async () => { mockFetch.mockRejectedValue('string error'); await expect(fetch('https://api.example.com/data', { logger: mockLogger })).rejects.toBe( 'string error' ); expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', { url: 'https://api.example.com/data', error: 'string error', name: 'Unknown', }); }); }); });