fluxer/packages/errors/src/__tests__/FluxerError.test.tsx
2026-02-17 12:22:36 +00:00

274 lines
7.0 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {FluxerError} from '@fluxer/errors/src/FluxerError';
import {HTTPException} from 'hono/http-exception';
import {describe, expect, it} from 'vitest';
describe('FluxerError', () => {
describe('constructor', () => {
it('should create an error with required options', () => {
const error = new FluxerError({
code: 'TEST_ERROR',
status: 400,
});
expect(error.code).toBe('TEST_ERROR');
expect(error.status).toBe(400);
expect(error.message).toBe('TEST_ERROR');
expect(error.name).toBe('FluxerError');
});
it('should use provided message instead of code', () => {
const error = new FluxerError({
code: 'TEST_ERROR',
message: 'Custom error message',
status: 400,
});
expect(error.code).toBe('TEST_ERROR');
expect(error.message).toBe('Custom error message');
});
it('should include optional data', () => {
const error = new FluxerError({
code: 'TEST_ERROR',
status: 400,
data: {field: 'username', reason: 'invalid'},
});
expect(error.data).toEqual({field: 'username', reason: 'invalid'});
});
it('should include optional headers', () => {
const error = new FluxerError({
code: 'TEST_ERROR',
status: 400,
headers: {'X-Custom-Header': 'value'},
});
expect(error.headers).toEqual({'X-Custom-Header': 'value'});
});
it('should include message variables for i18n', () => {
const error = new FluxerError({
code: 'RATE_LIMITED',
status: 429,
messageVariables: {retryAfter: 60},
});
expect(error.messageVariables).toEqual({retryAfter: 60});
});
it('should include cause for error chaining', () => {
const cause = new Error('Original error');
const error = new FluxerError({
code: 'WRAPPED_ERROR',
status: 500,
cause,
});
expect(error.cause).toBe(cause);
});
it('should be an instance of HTTPException', () => {
const error = new FluxerError({
code: 'TEST_ERROR',
status: 400,
});
expect(error).toBeInstanceOf(HTTPException);
});
});
describe('getResponse', () => {
it('should return a JSON Response with correct status', async () => {
const error = new FluxerError({
code: 'TEST_ERROR',
message: 'Test message',
status: 400,
});
const response = error.getResponse();
expect(response.status).toBe(400);
expect(response.headers.get('Content-Type')).toBe('application/json');
const body = await response.json();
expect(body).toEqual({
code: 'TEST_ERROR',
message: 'Test message',
});
});
it('should include data in response body', async () => {
const error = new FluxerError({
code: 'VALIDATION_ERROR',
message: 'Validation failed',
status: 400,
data: {errors: [{field: 'email', message: 'Invalid email'}]},
});
const response = error.getResponse();
const body = await response.json();
expect(body).toEqual({
code: 'VALIDATION_ERROR',
message: 'Validation failed',
errors: [{field: 'email', message: 'Invalid email'}],
});
});
it('should include custom headers in response', async () => {
const error = new FluxerError({
code: 'RATE_LIMITED',
status: 429,
headers: {'Retry-After': '60', 'X-RateLimit-Reset': '1234567890'},
});
const response = error.getResponse();
expect(response.headers.get('Retry-After')).toBe('60');
expect(response.headers.get('X-RateLimit-Reset')).toBe('1234567890');
expect(response.headers.get('Content-Type')).toBe('application/json');
});
it('should handle empty data', async () => {
const error = new FluxerError({
code: 'SIMPLE_ERROR',
status: 403,
});
const response = error.getResponse();
const body = await response.json();
expect(body).toEqual({
code: 'SIMPLE_ERROR',
message: 'SIMPLE_ERROR',
});
});
});
describe('toJSON', () => {
it('should serialize to JSON object', () => {
const error = new FluxerError({
code: 'TEST_ERROR',
message: 'Test message',
status: 400,
});
const json = error.toJSON();
expect(json).toEqual({
code: 'TEST_ERROR',
message: 'Test message',
});
});
it('should include data in JSON output', () => {
const error = new FluxerError({
code: 'VALIDATION_ERROR',
message: 'Validation failed',
status: 400,
data: {field: 'username'},
});
const json = error.toJSON();
expect(json).toEqual({
code: 'VALIDATION_ERROR',
message: 'Validation failed',
field: 'username',
});
});
it('should not include status or headers in JSON output', () => {
const error = new FluxerError({
code: 'TEST_ERROR',
status: 500,
headers: {'X-Custom': 'value'},
});
const json = error.toJSON();
expect(json).not.toHaveProperty('status');
expect(json).not.toHaveProperty('headers');
});
});
describe('status codes', () => {
it('should handle 4xx client error status codes', () => {
const testCases = [
{status: 400, name: 'Bad Request'},
{status: 401, name: 'Unauthorized'},
{status: 403, name: 'Forbidden'},
{status: 404, name: 'Not Found'},
{status: 409, name: 'Conflict'},
{status: 429, name: 'Too Many Requests'},
] as const;
for (const {status} of testCases) {
const error = new FluxerError({code: 'TEST', status});
expect(error.status).toBe(status);
}
});
it('should handle 5xx server error status codes', () => {
const testCases = [
{status: 500, name: 'Internal Server Error'},
{status: 501, name: 'Not Implemented'},
{status: 502, name: 'Bad Gateway'},
{status: 503, name: 'Service Unavailable'},
{status: 504, name: 'Gateway Timeout'},
] as const;
for (const {status} of testCases) {
const error = new FluxerError({code: 'TEST', status});
expect(error.status).toBe(status);
}
});
});
describe('error properties', () => {
it('should have correct name property', () => {
const error = new FluxerError({code: 'TEST', status: 400});
expect(error.name).toBe('FluxerError');
});
it('should be throwable', () => {
const error = new FluxerError({code: 'THROWN_ERROR', status: 400});
expect(() => {
throw error;
}).toThrow(FluxerError);
});
it('should be catchable as Error', () => {
const error = new FluxerError({code: 'CAUGHT_ERROR', status: 400});
try {
throw error;
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect(e).toBeInstanceOf(FluxerError);
}
});
});
});