import { describe, it, expect, vi, beforeEach } from 'vitest' import type { Request, Response, NextFunction } from 'express' const { mockIsAppError, mockIsNodeError, mockLoggerError, MockAppError, MockValidationError } = vi.hoisted(() => { const mockFn = () => ({}) return { mockIsAppError: vi.fn(), mockIsNodeError: vi.fn(), mockLoggerError: vi.fn(), MockAppError: class MockAppError extends Error { statusCode: number details?: Record code: string constructor( code: string, message: string, statusCode: number = 500, details?: Record ) { super(message) this.name = 'MockAppError' this.code = code this.statusCode = statusCode this.details = details } }, MockValidationError: class MockValidationError extends Error { statusCode: number details?: Record code: string constructor(message: string, details?: Record) { super(message) this.name = 'MockValidationError' this.code = 'VALIDATION_ERROR' this.statusCode = 400 this.details = details } }, } }) vi.mock('@shared/errors', () => ({ isAppError: mockIsAppError, isNodeError: mockIsNodeError, AppError: MockAppError, ValidationError: MockValidationError, })) vi.mock('@/api/utils/logger', () => ({ logger: { error: mockLoggerError, }, })) import { errorHandler } from '../errorHandler' describe('errorHandler', () => { let mockReq: Request let mockRes: Response let mockNext: NextFunction beforeEach(() => { vi.clearAllMocks() mockReq = {} as Request mockRes = { status: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(), } as unknown as Response mockNext = vi.fn() mockIsAppError.mockReturnValue(false) mockIsNodeError.mockReturnValue(false) }) describe('AppError 处理', () => { it('AppError 应发送自定义状态码和错误码', () => { const appError = new MockAppError('VALIDATION_ERROR', '验证失败', 400, { field: 'name' }) mockIsAppError.mockReturnValue(true) errorHandler(appError, mockReq, mockRes, mockNext) expect(mockRes.status).toHaveBeenCalledWith(400) expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: 'VALIDATION_ERROR', message: '验证失败', details: { field: 'name' }, }, }) }) it('ValidationError 应发送正确的错误信息', () => { const validationError = new MockValidationError('字段不能为空', { field: 'email' }) mockIsAppError.mockReturnValue(true) errorHandler(validationError, mockReq, mockRes, mockNext) expect(mockRes.status).toHaveBeenCalledWith(400) expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: 'VALIDATION_ERROR', message: '字段不能为空', details: { field: 'email' }, }, }) }) it('AppError 在生产环境不应包含 details', () => { const originalEnv = process.env.NODE_ENV process.env.NODE_ENV = 'production' try { const appError = new MockAppError('VALIDATION_ERROR', '验证失败', 400, { field: 'name' }) mockIsAppError.mockReturnValue(true) errorHandler(appError, mockReq, mockRes, mockNext) expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: 'VALIDATION_ERROR', message: '验证失败', details: undefined, }, }) } finally { process.env.NODE_ENV = originalEnv } }) }) describe('Node.js 系统错误处理', () => { it('Node.js 系统错误应包含 stack 信息(非生产环境)', () => { const nodeError = new Error('系统错误') as NodeJS.ErrnoException nodeError.code = 'ENOENT' nodeError.stack = 'Error: 系统错误\n at Test.' mockIsAppError.mockReturnValue(false) mockIsNodeError.mockReturnValue(true) errorHandler(nodeError, mockReq, mockRes, mockNext) expect(mockRes.status).toHaveBeenCalledWith(500) expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: 'INTERNAL_ERROR', message: '系统错误', details: { stack: nodeError.stack, nodeErrorCode: 'ENOENT', }, }, }) }) it('普通 Error 应包含 stack 信息(非生产环境)', () => { const error = new Error('普通错误') error.stack = 'Error: 普通错误\n at Test.' errorHandler(error, mockReq, mockRes, mockNext) expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: 'INTERNAL_ERROR', message: '普通错误', details: { stack: error.stack, }, }, }) }) it('在生产环境不包含敏感信息', () => { const originalEnv = process.env.NODE_ENV process.env.NODE_ENV = 'production' try { const error = new Error('错误信息') error.stack = 'Error: 错误信息\n at Test.' errorHandler(error, mockReq, mockRes, mockNext) expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: 'INTERNAL_ERROR', message: '错误信息', details: undefined, }, }) } finally { process.env.NODE_ENV = originalEnv } }) }) })