Files
dashcaddy/dashcaddy-api/__tests__/error-handler.test.js
Hermes 7bc2a207f3 DC-005: Fix all 138 broken test paths after src/ refactor
After the DC-005 module reorganization (41 files moved into src/ subdirs),
138 test suites failed because the refactor script's path-rewrite logic
missed three categories:

1. Files inside src/ doing 'require("./src/...")' — should be 'require("../...")'
2. Files in src/X/Y/ doing 'require("../../../src/...")' — should be 'require("../../...")'
3. Test files in __tests__/ with leftover 'require("../../../src/...")' paths

Root cause: the original refactor script ran before all files were moved,
so it computed relative paths against stale filesystem state.

Result:
- 30/30 test suites pass
- 879/879 tests pass (was: 18/30 suites, 614/687 tests)

Also fixed:
- routes/apps/restore.js: wrong responses import path
- routes/*/*.js: '../../src/utilities/X' → '../src/utilities/X' (depth 2 routes)
2026-06-13 12:16:56 -07:00

184 lines
5.2 KiB
JavaScript

// Mock the unified logging module so we can verify logError is called
// without writing to the actual error.log file
jest.mock('../src/utils/logging', () => ({
logError: jest.fn().mockResolvedValue(),
safeErrorMessage: jest.fn((err) => {
if (!err) return 'An internal error occurred';
return err.message || String(err);
}),
createLogger: jest.fn(() => ({
info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn()
})),
LOG_LEVELS: { debug: 0, info: 1, warn: 2, error: 3 }
}));
const { errorMiddleware, notFoundHandler } = require('../src/utilities/error-handler');
const {
AppError,
ValidationError,
AuthenticationError,
NotFoundError,
RateLimitError,
DockerError,
} = require('../src/utilities/errors');
describe('Error Handler', () => {
let req, res, next;
beforeEach(() => {
req = {
method: 'GET',
path: '/api/test',
ip: '127.0.0.1',
user: { id: 'user1' },
body: {},
};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
};
next = jest.fn();
});
describe('errorMiddleware', () => {
it('returns 400 for ValidationError', () => {
const err = new ValidationError('bad input', 'email');
errorMiddleware(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: 'bad input',
code: 'DC-400',
field: 'email',
})
);
});
it('returns 401 for AuthenticationError with requiresTotp', () => {
const err = new AuthenticationError('auth needed', true);
errorMiddleware(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: 'auth needed',
requiresTotp: true,
})
);
});
it('returns 404 for NotFoundError with resource', () => {
const err = new NotFoundError('Service');
errorMiddleware(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
error: 'Service not found',
resource: 'Service',
})
);
});
it('returns 429 for RateLimitError with retryAfter', () => {
const err = new RateLimitError(30);
errorMiddleware(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(429);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
error: 'Rate limit exceeded',
retryAfter: 30,
})
);
});
it('returns 500 with "Internal server error" for generic Error', () => {
const err = new Error('db connection lost');
errorMiddleware(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: 'Internal server error', // NOT the real message
})
);
});
it('includes error code in DC-XXX format', () => {
const err = new AppError('test', 418, 'DC-TEAPOT');
errorMiddleware(err, req, res, next);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({ code: 'DC-TEAPOT' })
);
});
it('includes details for DockerError', () => {
const err = new DockerError('container fail', 'create', { id: '123' });
errorMiddleware(err, req, res, next);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
details: { id: '123' },
})
);
});
it('includes stack trace in development mode', () => {
const origEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
const err = new AppError('test');
errorMiddleware(err, req, res, next);
const response = res.json.mock.calls[0][0];
expect(response.stack).toBeDefined();
process.env.NODE_ENV = origEnv;
});
it('excludes stack trace in production mode', () => {
const origEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const err = new AppError('test');
errorMiddleware(err, req, res, next);
const response = res.json.mock.calls[0][0];
expect(response.stack).toBeUndefined();
process.env.NODE_ENV = origEnv;
});
it('logs non-operational errors as FATAL', () => {
const origError = console.error;
console.error = jest.fn();
const err = new Error('programming bug');
errorMiddleware(err, req, res, next);
expect(console.error).toHaveBeenCalledWith(
'FATAL: Non-operational error detected',
expect.any(Object)
);
console.error = origError;
});
});
describe('notFoundHandler', () => {
it('passes NotFoundError to next()', () => {
notFoundHandler(req, res, next);
expect(next).toHaveBeenCalledWith(expect.any(NotFoundError));
const passedError = next.mock.calls[0][0];
expect(passedError.message).toContain('GET');
expect(passedError.message).toContain('/api/test');
});
});
});