200 lines
5.6 KiB
TypeScript
200 lines
5.6 KiB
TypeScript
|
|
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<string, unknown>
|
||
|
|
code: string
|
||
|
|
constructor(
|
||
|
|
code: string,
|
||
|
|
message: string,
|
||
|
|
statusCode: number = 500,
|
||
|
|
details?: Record<string, unknown>
|
||
|
|
) {
|
||
|
|
super(message)
|
||
|
|
this.name = 'MockAppError'
|
||
|
|
this.code = code
|
||
|
|
this.statusCode = statusCode
|
||
|
|
this.details = details
|
||
|
|
}
|
||
|
|
},
|
||
|
|
MockValidationError: class MockValidationError extends Error {
|
||
|
|
statusCode: number
|
||
|
|
details?: Record<string, unknown>
|
||
|
|
code: string
|
||
|
|
constructor(message: string, details?: Record<string, unknown>) {
|
||
|
|
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.<anonymous>'
|
||
|
|
|
||
|
|
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.<anonymous>'
|
||
|
|
|
||
|
|
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.<anonymous>'
|
||
|
|
|
||
|
|
errorHandler(error, mockReq, mockRes, mockNext)
|
||
|
|
|
||
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
||
|
|
success: false,
|
||
|
|
error: {
|
||
|
|
code: 'INTERNAL_ERROR',
|
||
|
|
message: '错误信息',
|
||
|
|
details: undefined,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
} finally {
|
||
|
|
process.env.NODE_ENV = originalEnv
|
||
|
|
}
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|