This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286// SPDX-License-Identifier: AGPL-3.0-or-later
import {HttpStatus} from '@fluxer/constants/src/HttpConstants';import {createErrorHandler, type ErrorHandlerOptions} from '@fluxer/errors/src/ErrorHandler';import {FluxerError} from '@fluxer/errors/src/FluxerError';import {Hono} from 'hono';import {HTTPException} from 'hono/http-exception';import {describe, expect, it, vi} from 'vitest';
interface ErrorResponse { code: string; message: string; stack?: string; [key: string]: unknown;}
function createTestApp(options: ErrorHandlerOptions = {}) { const app = new Hono(); app.onError(createErrorHandler(options)); return app;}
describe('createErrorHandler', () => { describe('FluxerError handling', () => { it('should return FluxerError response directly', async () => { const app = createTestApp(); app.get('/test', () => { throw new FluxerError({ code: 'TEST_ERROR', message: 'Test error message', status: 400, }); }); const response = await app.request('/test'); expect(response.status).toBe(400); const body = (await response.json()) as ErrorResponse; expect(body).toEqual({ code: 'TEST_ERROR', message: 'Test error message', }); }); it('should include FluxerError data in response', async () => { const app = createTestApp(); app.get('/test', () => { throw new FluxerError({ code: 'VALIDATION_ERROR', message: 'Validation failed', status: 400, data: {field: 'email'}, }); }); const response = await app.request('/test'); const body = (await response.json()) as ErrorResponse; expect(body).toEqual({ code: 'VALIDATION_ERROR', message: 'Validation failed', field: 'email', }); }); it('should include FluxerError custom headers', async () => { const app = createTestApp(); app.get('/test', () => { throw new FluxerError({ code: 'RATE_LIMITED', status: 429, headers: {'Retry-After': '60'}, }); }); const response = await app.request('/test'); expect(response.headers.get('Retry-After')).toBe('60'); }); }); describe('HTTPException handling', () => { it('should handle HTTPException with JSON response', async () => { const app = createTestApp(); app.get('/test', () => { throw new HTTPException(403, {message: 'Access denied'}); }); const response = await app.request('/test'); expect(response.status).toBe(403); const body = (await response.json()) as ErrorResponse; expect(body.code).toBe('FORBIDDEN'); expect(body.message).toBe('Access denied'); }); it('should use default message for HTTPException without message', async () => { const app = createTestApp(); app.get('/test', () => { throw new HTTPException(500); }); const response = await app.request('/test'); expect(response.status).toBe(500); const body = (await response.json()) as ErrorResponse; expect(body.message).toBe('An error occurred'); }); }); describe('generic Error handling', () => { it('should return 500 for generic errors', async () => { const app = createTestApp(); app.get('/test', () => { throw new Error('Something went wrong'); }); const response = await app.request('/test'); expect(response.status).toBe(HttpStatus.INTERNAL_SERVER_ERROR); const body = (await response.json()) as ErrorResponse; expect(body.code).toBe('INTERNAL_SERVER_ERROR'); expect(body.message).toBe('Something went wrong. Please try again later.'); }); it('should not expose error message without includeStack option', async () => { const app = createTestApp({includeStack: false}); app.get('/test', () => { throw new Error('Sensitive error details'); }); const response = await app.request('/test'); const body = (await response.json()) as ErrorResponse; expect(body.message).toBe('Something went wrong. Please try again later.'); expect(body.message).not.toContain('Sensitive'); }); it('should expose error message with includeStack option', async () => { const app = createTestApp({includeStack: true}); app.get('/test', () => { throw new Error('Error details for debugging'); }); const response = await app.request('/test'); const body = (await response.json()) as ErrorResponse; expect(body.message).toBe('Error details for debugging'); }); it('should include stack trace with includeStack option', async () => { const app = createTestApp({includeStack: true}); app.get('/test', () => { throw new Error('Test error'); }); const response = await app.request('/test'); const body = (await response.json()) as ErrorResponse; expect(body).toHaveProperty('stack'); expect(body.stack).toContain('Error: Test error'); }); }); describe('logError callback', () => { it('should call logError with error and context', async () => { const logError = vi.fn(); const app = createTestApp({logError}); app.get('/test', () => { throw new Error('Logged error'); }); await app.request('/test'); expect(logError).toHaveBeenCalledTimes(1); expect(logError.mock.calls[0][0]).toBeInstanceOf(Error); expect((logError.mock.calls[0][0] as Error).message).toBe('Logged error'); }); it('should call logError for FluxerError', async () => { const logError = vi.fn(); const app = createTestApp({logError}); app.get('/test', () => { throw new FluxerError({code: 'TEST', status: 400}); }); await app.request('/test'); expect(logError).toHaveBeenCalledTimes(1); expect(logError.mock.calls[0][0]).toBeInstanceOf(FluxerError); }); }); describe('customHandler callback', () => { it('should use customHandler response when provided', async () => { const customHandler = vi.fn().mockReturnValue( new Response(JSON.stringify({custom: true}), { status: 418, headers: {'Content-Type': 'application/json'}, }), ); const app = createTestApp({customHandler}); app.get('/test', () => { throw new Error('Custom handled'); }); const response = await app.request('/test'); expect(response.status).toBe(418); const body = (await response.json()) as { custom: boolean; }; expect(body).toEqual({custom: true}); expect(customHandler).toHaveBeenCalledTimes(1); }); it('should fall back to default handling when customHandler returns undefined', async () => { const customHandler = vi.fn().mockReturnValue(undefined); const app = createTestApp({customHandler}); app.get('/test', () => { throw new FluxerError({code: 'FALLBACK', status: 400}); }); const response = await app.request('/test'); expect(response.status).toBe(400); const body = (await response.json()) as ErrorResponse; expect(body.code).toBe('FALLBACK'); }); it('should support async customHandler', async () => { const customHandler = vi.fn().mockResolvedValue( new Response(JSON.stringify({async: true}), { status: 202, headers: {'Content-Type': 'application/json'}, }), ); const app = createTestApp({customHandler}); app.get('/test', () => { throw new Error('Async handled'); }); const response = await app.request('/test'); expect(response.status).toBe(202); const body = (await response.json()) as { async: boolean; }; expect(body).toEqual({async: true}); }); }); describe('responseFormat option', () => { it('should return JSON by default', async () => { const app = createTestApp(); app.get('/test', () => { throw new HTTPException(400); }); const response = await app.request('/test'); expect(response.headers.get('Content-Type')).toBe('application/json'); }); it('should return XML when responseFormat is xml', async () => { const app = createTestApp({responseFormat: 'xml'}); app.get('/test', () => { throw new HTTPException(400, {message: 'Bad request'}); }); const response = await app.request('/test'); expect(response.status).toBe(400); expect(response.headers.get('Content-Type')).toBe('application/xml'); const body = await response.text(); expect(body).toContain('<?xml version="1.0"'); expect(body).toContain('<Error>'); expect(body).toContain('<Code>BAD_REQUEST</Code>'); expect(body).toContain('<Message>Bad request</Message>'); }); it('should escape XML special characters', async () => { const app = createTestApp({responseFormat: 'xml'}); app.get('/test', () => { throw new HTTPException(400, {message: 'Error with <special> & "chars"'}); }); const response = await app.request('/test'); const body = await response.text(); expect(body).toContain('<special>'); expect(body).toContain('&'); expect(body).toContain('"chars"'); }); it('should return XML for internal errors when responseFormat is xml', async () => { const app = createTestApp({responseFormat: 'xml'}); app.get('/test', () => { throw new Error('Internal error'); }); const response = await app.request('/test'); expect(response.status).toBe(500); expect(response.headers.get('Content-Type')).toBe('application/xml'); const body = await response.text(); expect(body).toContain('<Code>INTERNAL_SERVER_ERROR</Code>'); }); }); describe('combined options', () => { it('should support logError and includeStack together', async () => { const logError = vi.fn(); const app = createTestApp({logError, includeStack: true}); app.get('/test', () => { throw new Error('Combined test'); }); const response = await app.request('/test'); const body = (await response.json()) as ErrorResponse; expect(logError).toHaveBeenCalledTimes(1); expect(body.message).toBe('Combined test'); expect(body).toHaveProperty('stack'); }); it('should call logError before customHandler', async () => { const callOrder: Array<string> = []; const logError = vi.fn(() => callOrder.push('log')); const customHandler = vi.fn(() => { callOrder.push('custom'); return undefined; }); const app = createTestApp({logError, customHandler}); app.get('/test', () => { throw new Error('Order test'); }); await app.request('/test'); expect(callOrder).toEqual(['log', 'custom']); }); });});