stock-bot/libs/utils/test/fetch.test.ts

286 lines
No EOL
8.6 KiB
TypeScript

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',
});
});
});
});