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:
122
dashcaddy-api/__tests__/url-resolver.test.js
Normal file
122
dashcaddy-api/__tests__/url-resolver.test.js
Normal file
@@ -0,0 +1,122 @@
|
||||
const { resolveServiceUrl } = require('../url-resolver');
|
||||
|
||||
describe('URL Resolver — DashCaddy service URL resolution', () => {
|
||||
const buildServiceUrl = jest.fn(id => `https://${id}.sami`);
|
||||
|
||||
beforeEach(() => {
|
||||
buildServiceUrl.mockClear();
|
||||
});
|
||||
|
||||
describe('Internet connectivity check', () => {
|
||||
it('always resolves "internet" to google.com regardless of config', () => {
|
||||
expect(resolveServiceUrl('internet', null, null, buildServiceUrl))
|
||||
.toBe('https://www.google.com');
|
||||
expect(buildServiceUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores service object for internet ID', () => {
|
||||
const service = { url: 'http://custom.test', isExternal: true, externalUrl: 'http://ext.test' };
|
||||
expect(resolveServiceUrl('internet', service, {}, buildServiceUrl))
|
||||
.toBe('https://www.google.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('External services (seedhost, cloud-hosted)', () => {
|
||||
it('uses externalUrl for services marked isExternal', () => {
|
||||
const service = { isExternal: true, externalUrl: 'https://usw123.seedhost.eu/sami/radarr' };
|
||||
expect(resolveServiceUrl('radarr', service, {}, buildServiceUrl))
|
||||
.toBe('https://usw123.seedhost.eu/sami/radarr');
|
||||
});
|
||||
|
||||
it('ignores isExternal if externalUrl is missing', () => {
|
||||
const service = { isExternal: true };
|
||||
expect(resolveServiceUrl('plex', service, {}, buildServiceUrl))
|
||||
.toBe('https://plex.sami');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom URL override on service', () => {
|
||||
it('uses service.url with http prefix as-is', () => {
|
||||
const service = { url: 'http://192.168.1.100:32400' };
|
||||
expect(resolveServiceUrl('plex', service, {}, buildServiceUrl))
|
||||
.toBe('http://192.168.1.100:32400');
|
||||
});
|
||||
|
||||
it('uses service.url with https prefix as-is', () => {
|
||||
const service = { url: 'https://plex.mydomain.com' };
|
||||
expect(resolveServiceUrl('plex', service, {}, buildServiceUrl))
|
||||
.toBe('https://plex.mydomain.com');
|
||||
});
|
||||
|
||||
it('prepends https:// to bare hostnames', () => {
|
||||
const service = { url: 'plex.sami' };
|
||||
expect(resolveServiceUrl('plex', service, {}, buildServiceUrl))
|
||||
.toBe('https://plex.sami');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DNS server resolution (Technitium, Pi-hole)', () => {
|
||||
it('resolves DNS server by ID from siteConfig', () => {
|
||||
const siteConfig = {
|
||||
dnsServers: {
|
||||
dns1: { ip: '192.168.254.204', port: 5380 },
|
||||
dns2: { ip: '100.74.102.61', port: 5380 },
|
||||
}
|
||||
};
|
||||
expect(resolveServiceUrl('dns1', null, siteConfig, buildServiceUrl))
|
||||
.toBe('http://192.168.254.204:5380');
|
||||
expect(resolveServiceUrl('dns2', null, siteConfig, buildServiceUrl))
|
||||
.toBe('http://100.74.102.61:5380');
|
||||
});
|
||||
|
||||
it('defaults to port 5380 when port is omitted', () => {
|
||||
const siteConfig = { dnsServers: { dns1: { ip: '10.0.0.1' } } };
|
||||
expect(resolveServiceUrl('dns1', null, siteConfig, buildServiceUrl))
|
||||
.toBe('http://10.0.0.1:5380');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fallback to buildServiceUrl (Caddy subdomain/subdirectory)', () => {
|
||||
it('falls back for local services with no special config', () => {
|
||||
resolveServiceUrl('radarr', { name: 'Radarr' }, {}, buildServiceUrl);
|
||||
expect(buildServiceUrl).toHaveBeenCalledWith('radarr');
|
||||
});
|
||||
|
||||
it('works when service is null (top-card items)', () => {
|
||||
expect(resolveServiceUrl('sonarr', null, {}, buildServiceUrl))
|
||||
.toBe('https://sonarr.sami');
|
||||
});
|
||||
|
||||
it('works when siteConfig is null', () => {
|
||||
expect(resolveServiceUrl('jellyfin', null, null, buildServiceUrl))
|
||||
.toBe('https://jellyfin.sami');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Priority chain — higher priority wins', () => {
|
||||
const fullService = {
|
||||
isExternal: true,
|
||||
externalUrl: 'https://external.test',
|
||||
url: 'http://custom.test',
|
||||
};
|
||||
const siteConfig = {
|
||||
dnsServers: { myservice: { ip: '10.0.0.1', port: 5380 } }
|
||||
};
|
||||
|
||||
it('externalUrl wins over service.url and DNS', () => {
|
||||
expect(resolveServiceUrl('myservice', fullService, siteConfig, buildServiceUrl))
|
||||
.toBe('https://external.test');
|
||||
});
|
||||
|
||||
it('service.url wins over DNS and fallback', () => {
|
||||
const service = { url: 'http://custom.test' };
|
||||
expect(resolveServiceUrl('myservice', service, siteConfig, buildServiceUrl))
|
||||
.toBe('http://custom.test');
|
||||
});
|
||||
|
||||
it('DNS wins over fallback', () => {
|
||||
expect(resolveServiceUrl('myservice', null, siteConfig, buildServiceUrl))
|
||||
.toBe('http://10.0.0.1:5380');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user