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>
117 lines
4.2 KiB
JavaScript
117 lines
4.2 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|