// src/services/db/errors.db.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { Logger } from 'pino'; import { RepositoryError, UniqueConstraintError, ForeignKeyConstraintError, NotFoundError, ForbiddenError, ValidationError, FileUploadError, NotNullConstraintError, CheckConstraintError, InvalidTextRepresentationError, NumericValueOutOfRangeError, handleDbError, } from './errors.db'; vi.mock('./logger.server'); describe('Custom Database and Application Errors', () => { describe('RepositoryError', () => { it('should create a generic database error with a message and status', () => { const message = 'Generic DB Error'; const status = 500; const error = new RepositoryError(message, status); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(RepositoryError); expect(error.message).toBe(message); expect(error.status).toBe(status); expect(error.name).toBe('RepositoryError'); }); }); describe('UniqueConstraintError', () => { it('should create an error with a default message and status 409', () => { const error = new UniqueConstraintError(); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(RepositoryError); expect(error).toBeInstanceOf(UniqueConstraintError); expect(error.message).toBe('The record already exists.'); expect(error.status).toBe(409); expect(error.name).toBe('UniqueConstraintError'); }); it('should create an error with a custom message', () => { const message = 'This email is already taken.'; const error = new UniqueConstraintError(message); expect(error.message).toBe(message); }); }); describe('ForeignKeyConstraintError', () => { it('should create an error with a default message and status 400', () => { const error = new ForeignKeyConstraintError(); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(RepositoryError); expect(error).toBeInstanceOf(ForeignKeyConstraintError); expect(error.message).toBe('The referenced record does not exist.'); expect(error.status).toBe(400); expect(error.name).toBe('ForeignKeyConstraintError'); }); it('should create an error with a custom message', () => { const message = 'The specified user does not exist.'; const error = new ForeignKeyConstraintError(message); expect(error.message).toBe(message); }); }); describe('NotFoundError', () => { it('should create an error with a default message and status 404', () => { const error = new NotFoundError(); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(RepositoryError); expect(error).toBeInstanceOf(NotFoundError); expect(error.message).toBe('The requested resource was not found.'); expect(error.status).toBe(404); expect(error.name).toBe('NotFoundError'); }); it('should create an error with a custom message', () => { const message = 'Flyer with ID 999 not found.'; const error = new NotFoundError(message); expect(error.message).toBe(message); }); }); describe('ForbiddenError', () => { it('should create an error with a default message and status 403', () => { const error = new ForbiddenError(); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(RepositoryError); expect(error).toBeInstanceOf(ForbiddenError); expect(error.message).toBe('Access denied.'); expect(error.status).toBe(403); expect(error.name).toBe('ForbiddenError'); }); it('should create an error with a custom message', () => { const message = 'You shall not pass.'; const error = new ForbiddenError(message); expect(error.message).toBe(message); }); }); describe('ValidationError', () => { it('should create an error with a default message, status 400, and validation errors array', () => { const validationIssues = [{ path: ['email'], message: 'Invalid email' }]; const error = new ValidationError(validationIssues); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(RepositoryError); expect(error).toBeInstanceOf(ValidationError); expect(error.message).toBe('The request data is invalid.'); expect(error.status).toBe(400); expect(error.name).toBe('ValidationError'); expect(error.validationErrors).toEqual(validationIssues); }); it('should create an error with a custom message', () => { const message = 'Your input has some issues.'; const error = new ValidationError([], message); expect(error.message).toBe(message); }); }); describe('FileUploadError', () => { it('should create an error with the correct message, name, and status 400', () => { const message = 'No file was uploaded.'; const error = new FileUploadError(message); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(FileUploadError); expect(error.message).toBe(message); expect(error.status).toBe(400); expect(error.name).toBe('FileUploadError'); }); }); describe('NotNullConstraintError', () => { it('should create an error with a default message and status 400', () => { const error = new NotNullConstraintError(); expect(error).toBeInstanceOf(RepositoryError); expect(error.message).toBe('A required field was left null.'); expect(error.status).toBe(400); expect(error.name).toBe('NotNullConstraintError'); }); it('should create an error with a custom message', () => { const message = 'Email cannot be null.'; const error = new NotNullConstraintError(message); expect(error.message).toBe(message); }); }); describe('CheckConstraintError', () => { it('should create an error with a default message and status 400', () => { const error = new CheckConstraintError(); expect(error).toBeInstanceOf(RepositoryError); expect(error.message).toBe('A check constraint was violated.'); expect(error.status).toBe(400); expect(error.name).toBe('CheckConstraintError'); }); it('should create an error with a custom message', () => { const message = 'Price must be positive.'; const error = new CheckConstraintError(message); expect(error.message).toBe(message); }); }); describe('InvalidTextRepresentationError', () => { it('should create an error with a default message and status 400', () => { const error = new InvalidTextRepresentationError(); expect(error).toBeInstanceOf(RepositoryError); expect(error.message).toBe('A value has an invalid format for its data type.'); expect(error.status).toBe(400); expect(error.name).toBe('InvalidTextRepresentationError'); }); it('should create an error with a custom message', () => { const message = 'Invalid input syntax for type integer: "abc"'; const error = new InvalidTextRepresentationError(message); expect(error.message).toBe(message); }); }); describe('NumericValueOutOfRangeError', () => { it('should create an error with a default message and status 400', () => { const error = new NumericValueOutOfRangeError(); expect(error).toBeInstanceOf(RepositoryError); expect(error.message).toBe('A numeric value is out of the allowed range.'); expect(error.status).toBe(400); expect(error.name).toBe('NumericValueOutOfRangeError'); }); it('should create an error with a custom message', () => { const message = 'Value too large for type smallint.'; const error = new NumericValueOutOfRangeError(message); expect(error.message).toBe(message); }); }); describe('handleDbError', () => { const mockLogger = { error: vi.fn(), } as unknown as Logger; beforeEach(() => { vi.clearAllMocks(); }); it('should re-throw existing RepositoryError instances without logging', () => { const notFound = new NotFoundError('Test not found'); expect(() => handleDbError(notFound, mockLogger, 'msg', {})).toThrow(notFound); expect(mockLogger.error).not.toHaveBeenCalled(); }); it('should throw UniqueConstraintError for code 23505', () => { const dbError = new Error('duplicate key'); (dbError as any).code = '23505'; expect(() => handleDbError(dbError, mockLogger, 'msg', {}, { uniqueMessage: 'custom unique' }), ).toThrow('custom unique'); }); it('should throw ForeignKeyConstraintError for code 23503', () => { const dbError = new Error('fk violation'); (dbError as any).code = '23503'; expect(() => handleDbError(dbError, mockLogger, 'msg', {}, { fkMessage: 'custom fk' }), ).toThrow('custom fk'); }); it('should throw NotNullConstraintError for code 23502', () => { const dbError = new Error('not null violation'); (dbError as any).code = '23502'; expect(() => handleDbError(dbError, mockLogger, 'msg', {}, { notNullMessage: 'custom not null' }), ).toThrow('custom not null'); }); it('should throw CheckConstraintError for code 23514', () => { const dbError = new Error('check violation'); (dbError as any).code = '23514'; expect(() => handleDbError(dbError, mockLogger, 'msg', {}, { checkMessage: 'custom check' }), ).toThrow('custom check'); }); it('should throw InvalidTextRepresentationError for code 22P02', () => { const dbError = new Error('invalid text'); (dbError as any).code = '22P02'; expect(() => handleDbError( dbError, mockLogger, 'msg', {}, { invalidTextMessage: 'custom invalid text' }, ), ).toThrow('custom invalid text'); }); it('should throw NumericValueOutOfRangeError for code 22003', () => { const dbError = new Error('out of range'); (dbError as any).code = '22003'; expect(() => handleDbError( dbError, mockLogger, 'msg', {}, { numericOutOfRangeMessage: 'custom out of range' }, ), ).toThrow('custom out of range'); }); it('should throw a generic Error with a default message', () => { const genericError = new Error('Something else happened'); expect(() => handleDbError(genericError, mockLogger, 'msg', {}, { defaultMessage: 'Oops' }), ).toThrow('Oops'); expect(mockLogger.error).toHaveBeenCalledWith({ err: genericError }, 'msg'); }); it('should throw a generic Error with a constructed message using entityName', () => { const genericError = new Error('Something else happened'); expect(() => handleDbError(genericError, mockLogger, 'msg', {}, { entityName: 'User' }), ).toThrow('Failed to perform operation on User.'); }); it('should throw a generic Error with a constructed message using "database" as a fallback', () => { const genericError = new Error('Something else happened'); // No defaultMessage or entityName provided expect(() => handleDbError(genericError, mockLogger, 'msg', {}, {})).toThrow( 'Failed to perform operation on database.', ); }); it('should fall through to generic error for unhandled Postgres error codes', () => { const dbError = new Error('some other db error'); // Set an unhandled Postgres error code (e.g., 42P01 - undefined_table) (dbError as any).code = '42P01'; (dbError as any).constraint = 'some_constraint'; (dbError as any).detail = 'Table does not exist'; expect(() => handleDbError( dbError, mockLogger, 'Unknown DB error', { table: 'users' }, { defaultMessage: 'Operation failed' }, ), ).toThrow('Operation failed'); // Verify logger.error was called with enhanced context including Postgres-specific fields expect(mockLogger.error).toHaveBeenCalledWith( expect.objectContaining({ err: dbError, code: '42P01', constraint: 'some_constraint', detail: 'Table does not exist', table: 'users', }), 'Unknown DB error', ); }); }); });