/** * Unit tests for error handling and Result types. * Tests APIError, InvalidDTOError, InvalidVOError, and Result wrapper functionality. */ import { describe, it, expect } from 'vitest' import { ZodError, z } from 'zod' import { APIError } from '../src/APIError' import { InvalidDTOError } from '../src/InvalidDTOError' import { InvalidVOError } from '../src/InvalidVOError' import { ofSuccess, ofError, type Result, type SuccessResult, type FailedResult } from '../src/Result' describe('Error Classes', () => { describe('APIError', () => { it('should create APIError with code and message', () => { const error = new APIError(404, 'User not found') expect(error).toBeInstanceOf(Error) expect(error).toBeInstanceOf(APIError) expect(error.code).toBe(404) expect(error.message).toBe('User not found') expect(error.name).toBe('Error') }) it('should create APIError with different HTTP status codes', () => { const errors = [ { code: 400, message: 'Bad Request' }, { code: 401, message: 'Unauthorized' }, { code: 403, message: 'Forbidden' }, { code: 500, message: 'Internal Server Error' } ] errors.forEach(({ code, message }) => { const error = new APIError(code, message) expect(error.code).toBe(code) expect(error.message).toBe(message) }) }) it('should be catchable as Error', () => { const error = new APIError(500, 'Server error') try { throw error } catch (caught) { expect(caught).toBeInstanceOf(Error) expect(caught).toBeInstanceOf(APIError) if (caught instanceof APIError) { expect(caught.code).toBe(500) expect(caught.message).toBe('Server error') } } }) }) describe('InvalidDTOError', () => { it('should create InvalidDTOError without ZodError', () => { const error = new InvalidDTOError() expect(error).toBeInstanceOf(Error) expect(error).toBeInstanceOf(APIError) expect(error).toBeInstanceOf(InvalidDTOError) expect(error.code).toBe(403) expect(error.message).toBe('Invalid input error') expect(error.zodError).toBeUndefined() }) it('should create InvalidDTOError with ZodError', () => { const schema = z.object({ name: z.string(), age: z.number() }) const result = schema.safeParse({ name: 123, age: 'invalid' }) expect(result.success).toBe(false) if (!result.success) { const error = new InvalidDTOError(result.error) expect(error.code).toBe(403) expect(error.message).toBe('Invalid input error') expect(error.zodError).toBe(result.error) expect(error.zodError).toBeInstanceOf(ZodError) expect(error.zodError?.issues).toBeDefined() expect(error.zodError?.issues.length).toBeGreaterThan(0) } }) it('should preserve ZodError details', () => { const schema = z.object({ email: z.string().email(), age: z.number().min(18) }) const result = schema.safeParse({ email: 'invalid-email', age: 15 }) if (!result.success) { const error = new InvalidDTOError(result.error) expect(error.zodError?.issues).toHaveLength(2) expect(error.zodError?.issues[0].path).toEqual(['email']) expect(error.zodError?.issues[1].path).toEqual(['age']) } }) }) describe('InvalidVOError', () => { it('should create InvalidVOError without ZodError', () => { const error = new InvalidVOError() expect(error).toBeInstanceOf(Error) expect(error).toBeInstanceOf(APIError) expect(error).toBeInstanceOf(InvalidVOError) expect(error.code).toBe(500) expect(error.message).toBe('Invalid server result') expect(error.zodError).toBeUndefined() }) it('should create InvalidVOError with ZodError', () => { const schema = z.object({ id: z.string().uuid(), status: z.enum(['active', 'inactive']) }) const result = schema.safeParse({ id: 'invalid-uuid', status: 'unknown' }) expect(result.success).toBe(false) if (!result.success) { const error = new InvalidVOError(result.error) expect(error.code).toBe(500) expect(error.message).toBe('Invalid server result') expect(error.zodError).toBe(result.error) expect(error.zodError).toBeInstanceOf(ZodError) } }) it('should indicate server-side validation failure', () => { const schema = z.object({ data: z.array(z.string()), count: z.number().int().positive() }) const result = schema.safeParse({ data: ['valid'], count: -1 }) if (!result.success) { const error = new InvalidVOError(result.error) // Should have 500 code indicating server issue expect(error.code).toBe(500) expect(error.zodError?.issues.some(issue => issue.path.includes('count') && issue.code === 'too_small' )).toBe(true) } }) }) }) describe('Result Types', () => { describe('ofSuccess', () => { it('should create SuccessResult with data', () => { const data = { id: '123', name: 'John Doe' } const result = ofSuccess(data) expect(result.success).toBe(true) expect(result.data).toBe(data) expect((result as SuccessResult).data.id).toBe('123') expect((result as SuccessResult).data.name).toBe('John Doe') }) it('should work with different data types', () => { // String data const stringResult = ofSuccess('Hello World') expect(stringResult.success).toBe(true) expect(stringResult.data).toBe('Hello World') // Number data const numberResult = ofSuccess(42) expect(numberResult.success).toBe(true) expect(numberResult.data).toBe(42) // Array data const arrayResult = ofSuccess([1, 2, 3]) expect(arrayResult.success).toBe(true) expect(arrayResult.data).toEqual([1, 2, 3]) // Null data const nullResult = ofSuccess(null) expect(nullResult.success).toBe(true) expect(nullResult.data).toBeNull() // Undefined data const undefinedResult = ofSuccess(undefined) expect(undefinedResult.success).toBe(true) expect(undefinedResult.data).toBeUndefined() }) it('should preserve object references', () => { const originalData = { nested: { value: 'test' } } const result = ofSuccess(originalData) expect(result.data).toBe(originalData) expect(result.data.nested).toBe(originalData.nested) }) }) describe('ofError', () => { it('should convert APIError to FailedResult', () => { const apiError = new APIError(404, 'Not found') const result = ofError(apiError) expect(result.success).toBe(false) const failedResult = result as FailedResult expect(failedResult.code).toBe(404) expect(failedResult.msg).toBe('Not found') }) it('should convert InvalidDTOError to FailedResult', () => { const dtoError = new InvalidDTOError() const result = ofError(dtoError) expect(result.success).toBe(false) const failedResult = result as FailedResult expect(failedResult.code).toBe(403) expect(failedResult.msg).toBe('Invalid input error') }) it('should convert InvalidVOError to FailedResult', () => { const voError = new InvalidVOError() const result = ofError(voError) expect(result.success).toBe(false) const failedResult = result as FailedResult expect(failedResult.code).toBe(500) expect(failedResult.msg).toBe('Invalid server result') }) it('should convert standard Error to FailedResult', () => { const error = new Error('Something went wrong') const result = ofError(error) expect(result.success).toBe(false) const failedResult = result as FailedResult expect(failedResult.code).toBe(1) expect(failedResult.msg).toBe('Something went wrong') }) it('should convert string error to FailedResult', () => { const result = ofError('String error message') expect(result.success).toBe(false) const failedResult = result as FailedResult expect(failedResult.code).toBe(1) expect(failedResult.msg).toBe('String error message') }) it('should convert unknown error to FailedResult', () => { const unknownErrors = [ 123, { error: 'object error' }, null, undefined, Symbol('test') ] unknownErrors.forEach(unknownError => { const result = ofError(unknownError) expect(result.success).toBe(false) const failedResult = result as FailedResult expect(failedResult.code).toBe(1) expect(failedResult.msg).toBe('Unknown error') }) }) it('should handle errors with different properties', () => { class CustomError extends Error { public customProperty = 'custom' constructor(message: string) { super(message) this.name = 'CustomError' } } const customError = new CustomError('Custom error message') const result = ofError(customError) expect(result.success).toBe(false) const failedResult = result as FailedResult expect(failedResult.code).toBe(1) expect(failedResult.msg).toBe('Custom error message') }) }) describe('Result type discrimination', () => { it('should allow type-safe access to success results', () => { const successResult: Result = ofSuccess('test data') if (successResult.success) { // TypeScript should know this is SuccessResult expect(successResult.data).toBe('test data') // @ts-expect-error - code should not exist on success result // expect(successResult.code).toBeUndefined() } }) it('should allow type-safe access to failed results', () => { const failedResult: Result = ofError(new APIError(500, 'Server error')) if (!failedResult.success) { // TypeScript should know this is FailedResult expect(failedResult.code).toBe(500) expect(failedResult.msg).toBe('Server error') // @ts-expect-error - data should not exist on failed result // expect(failedResult.data).toBeUndefined() } }) it('should work with generic Result types', () => { function processResult(result: Result): T | null { return result.success ? result.data : null } const successResult = ofSuccess({ id: 1, name: 'test' }) const failedResult = ofError('Failed') expect(processResult(successResult)).toEqual({ id: 1, name: 'test' }) expect(processResult(failedResult)).toBeNull() }) }) })