* Increase coverage * Add comprehensive LLM development documentation - Add CLAUDE.md as main documentation index - Create 8 detailed documentation files in docs/: - architecture.md: Tech stack, data models, auth system - directory-structure.md: Complete file tree with paths - backend-patterns.md: Module architecture and patterns - database.md: Models, migrations, and workflows - development-workflow.md: Setup and daily development - code-conventions.md: Style guide and best practices - testing.md: Test organization and patterns - common-tasks.md: How-to guides for frequent tasks - Update .gitignore to allow project-level CLAUDE.md - 4,285 lines of comprehensive documentation - Organized for easy navigation with cross-links - LLM-optimized with absolute paths and code examples * fixup! Add comprehensive LLM development documentation
166 lines
5 KiB
JavaScript
166 lines
5 KiB
JavaScript
const errorHandler = require('../../../shared/middleware/errorHandler');
|
|
const {
|
|
AppError,
|
|
NotFoundError,
|
|
ValidationError,
|
|
ConflictError,
|
|
UnauthorizedError,
|
|
ForbiddenError,
|
|
} = require('../../../shared/errors');
|
|
|
|
describe('errorHandler middleware', () => {
|
|
let req, res, next;
|
|
|
|
beforeEach(() => {
|
|
req = {};
|
|
res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
|
next = jest.fn();
|
|
});
|
|
|
|
// --- AppError subclasses ---
|
|
|
|
it('should handle NotFoundError with 404', () => {
|
|
const err = new NotFoundError('Task not found');
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(404);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Task not found',
|
|
code: 'NOT_FOUND',
|
|
});
|
|
});
|
|
|
|
it('should handle ValidationError with 400', () => {
|
|
const err = new ValidationError('Title is required');
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(400);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Title is required',
|
|
code: 'VALIDATION_ERROR',
|
|
});
|
|
});
|
|
|
|
it('should handle ConflictError with 409', () => {
|
|
const err = new ConflictError();
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(409);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Resource already exists',
|
|
code: 'CONFLICT',
|
|
});
|
|
});
|
|
|
|
it('should handle UnauthorizedError with 401', () => {
|
|
const err = new UnauthorizedError();
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(401);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Unauthorized',
|
|
code: 'UNAUTHORIZED',
|
|
});
|
|
});
|
|
|
|
it('should handle ForbiddenError with 403', () => {
|
|
const err = new ForbiddenError();
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Forbidden',
|
|
code: 'FORBIDDEN',
|
|
});
|
|
});
|
|
|
|
it('should handle generic AppError with custom status', () => {
|
|
const err = new AppError('Rate limited', 429, 'RATE_LIMITED');
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(429);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Rate limited',
|
|
code: 'RATE_LIMITED',
|
|
});
|
|
});
|
|
|
|
// --- Sequelize errors ---
|
|
|
|
it('should handle SequelizeValidationError with 400', () => {
|
|
const err = {
|
|
name: 'SequelizeValidationError',
|
|
errors: [
|
|
{ message: 'email must be unique' },
|
|
{ message: 'name cannot be null' },
|
|
],
|
|
};
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(400);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'email must be unique, name cannot be null',
|
|
code: 'VALIDATION_ERROR',
|
|
});
|
|
});
|
|
|
|
it('should handle SequelizeUniqueConstraintError with 409', () => {
|
|
const err = { name: 'SequelizeUniqueConstraintError' };
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(409);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'A resource with this identifier already exists.',
|
|
code: 'CONFLICT',
|
|
});
|
|
});
|
|
|
|
// --- Unknown errors ---
|
|
|
|
it('should handle unknown errors with 500 in non-production', () => {
|
|
const originalEnv = process.env.NODE_ENV;
|
|
process.env.NODE_ENV = 'test';
|
|
|
|
const err = new Error('Something unexpected');
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(500);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Something unexpected',
|
|
code: 'INTERNAL_ERROR',
|
|
});
|
|
|
|
process.env.NODE_ENV = originalEnv;
|
|
});
|
|
|
|
it('should hide error message in production', () => {
|
|
const originalEnv = process.env.NODE_ENV;
|
|
process.env.NODE_ENV = 'production';
|
|
|
|
const err = new Error('Database credentials leaked');
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(500);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Internal server error',
|
|
code: 'INTERNAL_ERROR',
|
|
});
|
|
|
|
process.env.NODE_ENV = originalEnv;
|
|
});
|
|
|
|
it('should use statusCode from unknown error if present', () => {
|
|
const err = new Error('Bad gateway');
|
|
err.statusCode = 502;
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(502);
|
|
});
|
|
|
|
it('should default to 500 when no statusCode on unknown error', () => {
|
|
const err = new Error('Oops');
|
|
errorHandler(err, req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(500);
|
|
});
|
|
});
|