test: build comprehensive test suite reaching 80%+ coverage threshold

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>
This commit is contained in:
2026-04-06 21:36:46 -07:00
parent bdf3f247b1
commit ea5acfa9a2
26 changed files with 8010 additions and 3 deletions

View File

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