Initial commit

This commit is contained in:
2026-03-08 01:34:54 +08:00
commit 1f104f73c8
441 changed files with 64911 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
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
}
})
})
})

View File

@@ -0,0 +1,42 @@
import type { NextFunction, Request, Response } from 'express'
import type { ApiResponse } from '../../shared/types.js'
import { ERROR_CODES } from '../../shared/constants/errors.js'
import { isAppError, isNodeError } from '../../shared/errors/index.js'
import { logger } from '../utils/logger.js'
export const errorHandler = (err: unknown, _req: Request, res: Response, _next: NextFunction) => {
let statusCode: number = 500
let code: string = ERROR_CODES.INTERNAL_ERROR
let message: string = 'Server internal error'
let details: unknown = undefined
if (isAppError(err)) {
statusCode = err.statusCode
code = err.code
message = err.message
details = err.details
} else if (isNodeError(err)) {
message = err.message
if (process.env.NODE_ENV !== 'production') {
details = { stack: err.stack, nodeErrorCode: err.code }
}
} else if (err instanceof Error) {
message = err.message
if (process.env.NODE_ENV !== 'production') {
details = { stack: err.stack }
}
}
logger.error(err)
const response: ApiResponse<null> = {
success: false,
error: {
code,
message,
details: process.env.NODE_ENV === 'production' ? undefined : details,
},
}
res.status(statusCode).json(response)
}

View File

@@ -0,0 +1,33 @@
import type { Request, Response, NextFunction } from 'express'
import { ZodSchema, ZodError } from 'zod'
import { ValidationError } from '../../shared/errors/index.js'
export const validateBody = (schema: ZodSchema) => {
return (req: Request, _res: Response, next: NextFunction) => {
try {
req.body = schema.parse(req.body)
next()
} catch (error) {
if (error instanceof ZodError) {
next(new ValidationError('Request validation failed', { issues: error.issues }))
} else {
next(error)
}
}
}
}
export const validateQuery = (schema: ZodSchema) => {
return (req: Request, _res: Response, next: NextFunction) => {
try {
req.query = schema.parse(req.query) as typeof req.query
next()
} catch (error) {
if (error instanceof ZodError) {
next(new ValidationError('Query validation failed', { issues: error.issues }))
} else {
next(error)
}
}
}
}