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,116 @@
const { paginate, parsePaginationParams, DEFAULT_LIMIT, MAX_LIMIT } = require('../pagination');
describe('Pagination — DashCaddy list endpoints', () => {
describe('parsePaginationParams', () => {
it('returns null when no pagination params (backward compat — full list)', () => {
expect(parsePaginationParams({})).toBeNull();
expect(parsePaginationParams({ search: 'plex' })).toBeNull();
});
it('parses page and limit from query', () => {
const params = parsePaginationParams({ page: '2', limit: '10' });
expect(params).toEqual({ page: 2, limit: 10 });
});
it('defaults page to 1', () => {
expect(parsePaginationParams({ limit: '25' })).toEqual({ page: 1, limit: 25 });
});
it('defaults limit to DEFAULT_LIMIT when only page given', () => {
expect(parsePaginationParams({ page: '3' })).toEqual({ page: 3, limit: DEFAULT_LIMIT });
});
it('clamps page to minimum 1', () => {
expect(parsePaginationParams({ page: '0' }).page).toBe(1);
expect(parsePaginationParams({ page: '-5' }).page).toBe(1);
});
it('treats limit 0 as default (parseInt falsy → DEFAULT_LIMIT)', () => {
expect(parsePaginationParams({ limit: '0' }).limit).toBe(DEFAULT_LIMIT);
});
it('clamps negative limit to minimum 1', () => {
expect(parsePaginationParams({ limit: '-10' }).limit).toBe(1);
});
it('clamps limit to MAX_LIMIT', () => {
expect(parsePaginationParams({ limit: '9999' }).limit).toBe(MAX_LIMIT);
});
it('handles NaN gracefully', () => {
const params = parsePaginationParams({ page: 'abc', limit: 'xyz' });
expect(params.page).toBe(1);
expect(params.limit).toBe(DEFAULT_LIMIT);
});
});
describe('paginate', () => {
const items = Array.from({ length: 55 }, (_, i) => ({ id: `svc-${i + 1}` }));
it('returns all items when params is null (no pagination)', () => {
const result = paginate(items, null);
expect(result.data).toHaveLength(55);
expect(result.pagination).toBeUndefined();
});
it('returns first page correctly', () => {
const result = paginate(items, { page: 1, limit: 10 });
expect(result.data).toHaveLength(10);
expect(result.data[0].id).toBe('svc-1');
expect(result.pagination.page).toBe(1);
expect(result.pagination.total).toBe(55);
expect(result.pagination.totalPages).toBe(6);
expect(result.pagination.hasMore).toBe(true);
});
it('returns last page with fewer items', () => {
const result = paginate(items, { page: 6, limit: 10 });
expect(result.data).toHaveLength(5); // 55 - 50 = 5 remaining
expect(result.data[0].id).toBe('svc-51');
expect(result.pagination.hasMore).toBe(false);
});
it('returns empty array for page beyond total', () => {
const result = paginate(items, { page: 100, limit: 10 });
expect(result.data).toHaveLength(0);
expect(result.pagination.hasMore).toBe(false);
});
it('handles empty list', () => {
const result = paginate([], { page: 1, limit: 10 });
expect(result.data).toHaveLength(0);
expect(result.pagination.total).toBe(0);
expect(result.pagination.totalPages).toBe(0);
});
it('single-page result when limit exceeds total', () => {
const result = paginate(items, { page: 1, limit: 100 });
expect(result.data).toHaveLength(55);
expect(result.pagination.totalPages).toBe(1);
expect(result.pagination.hasMore).toBe(false);
});
});
describe('Real DashCaddy scenario: 52 app templates paginated', () => {
const templates = Array.from({ length: 52 }, (_, i) => ({
id: `app-${i}`,
name: `App ${i}`,
category: i < 10 ? 'Media' : 'Utilities'
}));
it('default limit (50) shows first 50 apps with hasMore', () => {
const params = parsePaginationParams({ page: '1' });
const result = paginate(templates, params);
expect(result.data).toHaveLength(50);
expect(result.pagination.hasMore).toBe(true);
});
it('page 2 shows remaining 2 apps', () => {
const params = parsePaginationParams({ page: '2' });
const result = paginate(templates, params);
expect(result.data).toHaveLength(2);
expect(result.pagination.hasMore).toBe(false);
});
});
});