Add 22 test files (~700 tests) covering security-critical modules, core infrastructure, API routes, and error handling. Final coverage: 86.73% statements / 80.57% branches / 85.57% functions / 87.42% lines, all above the 80% threshold enforced by jest.config.js. Highlights: - Unit tests for crypto-utils, credential-manager, auth-manager, csrf, input-validator, state-manager, health-checker, backup-manager, update-manager, resource-monitor, app-templates, platform-paths, port-lock-manager, errors, error-handler, pagination, url-resolver - Route tests for health, services, and containers (supertest + mocked deps) - Shared test-utils helper for mock factories and Express app builder - npm scripts for CI: test:ci, test:unit, test:routes, test:security, test:changed, test:debug - jest.config.js: expand coverage targets, add 80% threshold gate - routes/services.js: import ValidationError and NotFoundError from errors - .gitignore: exclude coverage/, *.bak, *.log Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
158 lines
4.9 KiB
JavaScript
158 lines
4.9 KiB
JavaScript
const {
|
|
AppError,
|
|
ValidationError,
|
|
AuthenticationError,
|
|
ForbiddenError,
|
|
NotFoundError,
|
|
ConflictError,
|
|
RateLimitError,
|
|
DockerError,
|
|
CaddyError,
|
|
DNSError,
|
|
ServiceUnavailableError
|
|
} = require('../errors');
|
|
|
|
describe('Error Classes', () => {
|
|
describe('AppError', () => {
|
|
it('has default statusCode 500 and auto-generated code', () => {
|
|
const err = new AppError('something broke');
|
|
expect(err.message).toBe('something broke');
|
|
expect(err.statusCode).toBe(500);
|
|
expect(err.code).toBe('APP_ERROR');
|
|
expect(err.isOperational).toBe(true);
|
|
expect(err).toBeInstanceOf(Error);
|
|
});
|
|
|
|
it('accepts custom statusCode and code', () => {
|
|
const err = new AppError('custom', 418, 'DC-TEAPOT');
|
|
expect(err.statusCode).toBe(418);
|
|
expect(err.code).toBe('DC-TEAPOT');
|
|
});
|
|
});
|
|
|
|
describe('ValidationError', () => {
|
|
it('has statusCode 400, code DC-400, and optional field', () => {
|
|
const err = new ValidationError('bad input', 'email');
|
|
expect(err.statusCode).toBe(400);
|
|
expect(err.code).toBe('DC-400');
|
|
expect(err.field).toBe('email');
|
|
expect(err).toBeInstanceOf(AppError);
|
|
});
|
|
|
|
it('field defaults to null', () => {
|
|
const err = new ValidationError('bad');
|
|
expect(err.field).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('AuthenticationError', () => {
|
|
it('has statusCode 401 and requiresTotp flag', () => {
|
|
const err = new AuthenticationError('need auth', true);
|
|
expect(err.statusCode).toBe(401);
|
|
expect(err.code).toBe('DC-401');
|
|
expect(err.requiresTotp).toBe(true);
|
|
expect(err).toBeInstanceOf(AppError);
|
|
});
|
|
|
|
it('has sensible defaults', () => {
|
|
const err = new AuthenticationError();
|
|
expect(err.message).toBe('Authentication required');
|
|
expect(err.requiresTotp).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('ForbiddenError', () => {
|
|
it('has statusCode 403', () => {
|
|
const err = new ForbiddenError();
|
|
expect(err.statusCode).toBe(403);
|
|
expect(err.code).toBe('DC-403');
|
|
expect(err.message).toBe('Forbidden');
|
|
expect(err).toBeInstanceOf(AppError);
|
|
});
|
|
});
|
|
|
|
describe('NotFoundError', () => {
|
|
it('has statusCode 404 and resource in message', () => {
|
|
const err = new NotFoundError('Service');
|
|
expect(err.statusCode).toBe(404);
|
|
expect(err.code).toBe('DC-404');
|
|
expect(err.message).toBe('Service not found');
|
|
expect(err.resource).toBe('Service');
|
|
expect(err).toBeInstanceOf(AppError);
|
|
});
|
|
|
|
it('defaults to "Resource"', () => {
|
|
const err = new NotFoundError();
|
|
expect(err.message).toBe('Resource not found');
|
|
});
|
|
});
|
|
|
|
describe('ConflictError', () => {
|
|
it('has statusCode 409 and optional conflictingResource', () => {
|
|
const err = new ConflictError('already exists', 'service-x');
|
|
expect(err.statusCode).toBe(409);
|
|
expect(err.code).toBe('DC-409');
|
|
expect(err.conflictingResource).toBe('service-x');
|
|
expect(err).toBeInstanceOf(AppError);
|
|
});
|
|
});
|
|
|
|
describe('RateLimitError', () => {
|
|
it('has statusCode 429 and retryAfter', () => {
|
|
const err = new RateLimitError(30);
|
|
expect(err.statusCode).toBe(429);
|
|
expect(err.code).toBe('DC-429');
|
|
expect(err.retryAfter).toBe(30);
|
|
expect(err.message).toBe('Rate limit exceeded');
|
|
expect(err).toBeInstanceOf(AppError);
|
|
});
|
|
|
|
it('defaults retryAfter to 60', () => {
|
|
const err = new RateLimitError();
|
|
expect(err.retryAfter).toBe(60);
|
|
});
|
|
});
|
|
|
|
describe('DockerError', () => {
|
|
it('has statusCode 500, operation, and details', () => {
|
|
const err = new DockerError('container failed', 'create', { containerId: '123' });
|
|
expect(err.statusCode).toBe(500);
|
|
expect(err.code).toBe('DC-500-DOCKER');
|
|
expect(err.operation).toBe('create');
|
|
expect(err.details).toEqual({ containerId: '123' });
|
|
expect(err).toBeInstanceOf(AppError);
|
|
});
|
|
});
|
|
|
|
describe('CaddyError', () => {
|
|
it('has statusCode 502', () => {
|
|
const err = new CaddyError('reload failed', 'reload');
|
|
expect(err.statusCode).toBe(502);
|
|
expect(err.code).toBe('DC-502-CADDY');
|
|
expect(err.operation).toBe('reload');
|
|
expect(err).toBeInstanceOf(AppError);
|
|
});
|
|
});
|
|
|
|
describe('DNSError', () => {
|
|
it('has statusCode 502', () => {
|
|
const err = new DNSError('zone create failed', 'create-zone');
|
|
expect(err.statusCode).toBe(502);
|
|
expect(err.code).toBe('DC-502-DNS');
|
|
expect(err).toBeInstanceOf(AppError);
|
|
});
|
|
});
|
|
|
|
describe('ServiceUnavailableError', () => {
|
|
it('has statusCode 503, service name, and optional retryAfter', () => {
|
|
const err = new ServiceUnavailableError('plex', 120);
|
|
expect(err.statusCode).toBe(503);
|
|
expect(err.code).toBe('DC-503');
|
|
expect(err.message).toBe('Service unavailable: plex');
|
|
expect(err.service).toBe('plex');
|
|
expect(err.retryAfter).toBe(120);
|
|
expect(err).toBeInstanceOf(AppError);
|
|
});
|
|
});
|
|
});
|