import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test'; import type { ExecutionContext, IServiceContainer } from '@stock-bot/types'; import * as utils from '@stock-bot/utils'; import { BaseHandler } from '../src/base/BaseHandler'; // Mock fetch const mockFetch = mock(); class TestHandler extends BaseHandler { async testGet(url: string, options?: any) { return this.http.get(url, options); } async testPost(url: string, data?: any, options?: any) { return this.http.post(url, data, options); } async testPut(url: string, data?: any, options?: any) { return this.http.put(url, data, options); } async testDelete(url: string, options?: any) { return this.http.delete(url, options); } } describe('BaseHandler HTTP Methods', () => { let handler: TestHandler; let mockServices: IServiceContainer; beforeEach(() => { mockServices = { cache: null, globalCache: null, queueManager: null, proxy: null, browser: null, mongodb: null, postgres: null, questdb: null, logger: { info: mock(), debug: mock(), error: mock(), warn: mock(), } as any, } as IServiceContainer; handler = new TestHandler(mockServices, 'TestHandler'); // Mock utils.fetch spyOn(utils, 'fetch').mockImplementation(mockFetch); mockFetch.mockReset(); }); afterEach(() => { // spyOn automatically restores }); describe('GET requests', () => { it('should make GET requests with fetch', async () => { const mockResponse = { ok: true, status: 200, statusText: 'OK', headers: new Headers(), json: async () => ({ data: 'test' }), }; mockFetch.mockResolvedValue(mockResponse); await handler.testGet('https://api.example.com/data'); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/data', expect.objectContaining({ method: 'GET', logger: expect.any(Object), }) ); }); it('should pass custom options to GET requests', async () => { const mockResponse = { ok: true, status: 200, statusText: 'OK', headers: new Headers(), }; mockFetch.mockResolvedValue(mockResponse); await handler.testGet('https://api.example.com/data', { headers: { Authorization: 'Bearer token' }, }); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/data', expect.objectContaining({ headers: { Authorization: 'Bearer token' }, method: 'GET', logger: expect.any(Object), }) ); }); }); describe('POST requests', () => { it('should make POST requests with JSON data', async () => { const mockResponse = { ok: true, status: 200, statusText: 'OK', headers: new Headers(), json: async () => ({ success: true }), }; mockFetch.mockResolvedValue(mockResponse); const data = { name: 'test', value: 123 }; await handler.testPost('https://api.example.com/create', data); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/create', expect.objectContaining({ method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' }, logger: expect.any(Object), }) ); }); it('should merge custom headers in POST requests', async () => { const mockResponse = { ok: true, status: 200, statusText: 'OK', headers: new Headers(), }; mockFetch.mockResolvedValue(mockResponse); await handler.testPost( 'https://api.example.com/create', { test: 'data' }, { headers: { 'X-Custom': 'value' }, } ); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/create', expect.objectContaining({ method: 'POST', body: JSON.stringify({ test: 'data' }), headers: { 'Content-Type': 'application/json', 'X-Custom': 'value', }, logger: expect.any(Object), }) ); }); }); describe('PUT requests', () => { it('should make PUT requests with JSON data', async () => { const mockResponse = { ok: true, status: 200, statusText: 'OK', headers: new Headers(), }; mockFetch.mockResolvedValue(mockResponse); const data = { id: 1, name: 'updated' }; await handler.testPut('https://api.example.com/update/1', data); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/update/1', expect.objectContaining({ method: 'PUT', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' }, logger: expect.any(Object), }) ); }); it('should handle PUT requests with custom options', async () => { const mockResponse = { ok: true, status: 200, statusText: 'OK', headers: new Headers(), }; mockFetch.mockResolvedValue(mockResponse); await handler.testPut( 'https://api.example.com/update', { data: 'test' }, { headers: { 'If-Match': 'etag' }, timeout: 5000, } ); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/update', expect.objectContaining({ method: 'PUT', body: JSON.stringify({ data: 'test' }), headers: { 'Content-Type': 'application/json', 'If-Match': 'etag', }, timeout: 5000, logger: expect.any(Object), }) ); }); }); describe('DELETE requests', () => { it('should make DELETE requests', async () => { const mockResponse = { ok: true, status: 200, statusText: 'OK', headers: new Headers(), }; mockFetch.mockResolvedValue(mockResponse); await handler.testDelete('https://api.example.com/delete/1'); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/delete/1', expect.objectContaining({ method: 'DELETE', logger: expect.any(Object), }) ); }); it('should pass options to DELETE requests', async () => { const mockResponse = { ok: true, status: 200, statusText: 'OK', headers: new Headers(), }; mockFetch.mockResolvedValue(mockResponse); await handler.testDelete('https://api.example.com/delete/1', { headers: { Authorization: 'Bearer token' }, }); expect(mockFetch).toHaveBeenCalledWith( 'https://api.example.com/delete/1', expect.objectContaining({ headers: { Authorization: 'Bearer token' }, method: 'DELETE', logger: expect.any(Object), }) ); }); }); describe('Error handling', () => { it('should propagate fetch errors', async () => { mockFetch.mockRejectedValue(new Error('Network error')); await expect(handler.testGet('https://api.example.com/data')).rejects.toThrow( 'Network error' ); }); it('should handle non-ok responses', async () => { const mockResponse = { ok: false, status: 404, statusText: 'Not Found', headers: new Headers(), }; mockFetch.mockResolvedValue(mockResponse); const response = await handler.testGet('https://api.example.com/missing'); expect(response.ok).toBe(false); expect(response.status).toBe(404); }); }); });