import fs from 'fs'; import path from 'path'; import { describe, expect, it } from 'vitest'; import { makePlatformRegistryFixture } from '../src/test-fixtures/platform-registry.js'; import { detectUpstream, ingressFromConfig, parseResolvConfNameservers, parseUpstreamSpec, renderDnsmasqConfig, writeDnsmasqConfig, type DnsIngressMap, } from './dns.js'; const INGRESS: DnsIngressMap = { controlplane: '10.0.1.2', cms: '10.0.1.4', git: '10.0.1.6', }; describe('setup/dns', () => { it('renders address records for the platform controlplane, shared jails, and every tenant', () => { const registry = makePlatformRegistryFixture(); const result = renderDnsmasqConfig(registry, INGRESS); expect(result.zone).toBe('home.arpa'); expect(result.content).toContain('domain=home.arpa'); expect(result.content).toContain('local=/home.arpa/'); expect(result.content).toContain('address=/ai.home.arpa/10.0.1.2'); expect(result.content).toContain('address=/cms.home.arpa/10.0.1.4'); expect(result.content).toContain('address=/git.home.arpa/10.0.1.6'); expect(result.content).toContain('address=/web.home.arpa/10.0.1.4'); expect(result.content).toContain('address=/alpha.home.arpa/10.0.1.4'); const hosts = result.records.map((record) => record.host); expect(hosts).toEqual([ 'ai.home.arpa', 'cms.home.arpa', 'web.home.arpa', 'git.home.arpa', 'alpha.home.arpa', 'blog.alpha.home.arpa', ]); }); it('skips shared-role records when the role is not declared in shared.jails', () => { const registry = makePlatformRegistryFixture(); const stripped = { ...registry, shared: { ...registry.shared, jails: [] }, }; const result = renderDnsmasqConfig(stripped, INGRESS); expect(result.content).not.toContain('cms.home.arpa'); expect(result.content).not.toContain('git.home.arpa'); expect(result.content).toContain('ai.home.arpa'); expect(result.content).toContain('web.home.arpa'); expect(result.content).toContain('alpha.home.arpa'); }); it('emits an address record per internal/public tenant site and skips disabled sites', () => { const registry = makePlatformRegistryFixture(); const withSites = { ...registry, tenants: { ...registry.tenants, alpha: { ...registry.tenants.alpha!, sites: [ { id: 'blog', exposure: 'internal' as const }, { id: 'docs', exposure: 'public' as const }, { id: 'old', exposure: 'disabled' as const }, ], }, }, }; const result = renderDnsmasqConfig(withSites, INGRESS); const hosts = result.records.map((record) => record.host); expect(hosts).toContain('blog.alpha.home.arpa'); expect(hosts).toContain('docs.alpha.home.arpa'); expect(hosts).not.toContain('old.alpha.home.arpa'); }); it('honors a custom internal_base from the registry', () => { const registry = makePlatformRegistryFixture(); const custom = { ...registry, platform: { ...registry.platform, internalBase: 'lan.example', }, }; const result = renderDnsmasqConfig(custom, INGRESS); expect(result.zone).toBe('lan.example'); expect(result.content).toContain('domain=lan.example'); expect(result.content).toContain('address=/ai.lan.example/10.0.1.2'); expect(result.content).toContain('address=/alpha.lan.example/10.0.1.4'); }); it('emits bind and server directives only when provided', () => { const registry = makePlatformRegistryFixture(); const without = renderDnsmasqConfig(registry, INGRESS); expect(without.content).not.toContain('listen-address='); expect(without.content).not.toContain('bind-interfaces'); expect(without.content).not.toContain('server='); expect(without.listenAddresses).toEqual([]); expect(without.upstream).toEqual([]); const withOptions = renderDnsmasqConfig(registry, INGRESS, { listenAddresses: ['127.0.0.1', '10.0.1.1'], upstream: ['1.1.1.1', '9.9.9.9'], }); expect(withOptions.content).toContain('listen-address=127.0.0.1'); expect(withOptions.content).toContain('listen-address=10.0.1.1'); expect(withOptions.content).toContain('bind-interfaces'); expect(withOptions.content).toContain('server=1.1.1.1'); expect(withOptions.content).toContain('server=9.9.9.9'); expect(withOptions.listenAddresses).toEqual(['127.0.0.1', '10.0.1.1']); expect(withOptions.upstream).toEqual(['1.1.1.1', '9.9.9.9']); }); it('deduplicates bind and upstream entries', () => { const registry = makePlatformRegistryFixture(); const result = renderDnsmasqConfig(registry, INGRESS, { listenAddresses: ['127.0.0.1', '127.0.0.1', '10.0.1.1'], upstream: ['1.1.1.1', '1.1.1.1', '9.9.9.9'], }); expect(result.listenAddresses).toEqual(['127.0.0.1', '10.0.1.1']); expect(result.upstream).toEqual(['1.1.1.1', '9.9.9.9']); }); it('only rewrites the file when content changes', () => { const registry = makePlatformRegistryFixture(); const base = path.join(process.cwd(), 'tmp', 'tests'); fs.mkdirSync(base, { recursive: true }); const dir = fs.mkdtempSync(path.join(base, 'dns-test-')); const outputPath = path.join(dir, 'dnsmasq.conf'); const first = writeDnsmasqConfig(outputPath, registry, INGRESS); expect(first.changed).toBe(true); const firstStat = fs.statSync(outputPath).mtimeMs; const second = writeDnsmasqConfig(outputPath, registry, INGRESS); expect(second.changed).toBe(false); expect(fs.statSync(outputPath).mtimeMs).toBe(firstStat); fs.rmSync(dir, { recursive: true, force: true }); }); describe('ingressFromConfig', () => { it('uses deploy-time addresses from env-file values', () => { expect( ingressFromConfig({ WARDEN_SUBNET_BASE: '192.168.72', WARDEN_GATEWAY: '192.168.72.1', WARDEN_CMS_IP: '192.168.72.3', WARDEN_GIT_IP: '192.168.72.2', }), ).toEqual({ controlplane: '192.168.72.1', cms: '192.168.72.3', git: '192.168.72.2', }); }); }); describe('parseResolvConfNameservers', () => { it('extracts ipv4 nameservers and skips loopback addresses', () => { const raw = [ '# comment line', 'nameserver 1.1.1.1', 'nameserver 127.0.0.1', 'nameserver 8.8.8.8 # trailing comment', 'nameserver ::1', '', ].join('\n'); expect(parseResolvConfNameservers(raw)).toEqual(['1.1.1.1', '8.8.8.8']); }); it('returns an empty list when no usable entries are present', () => { expect(parseResolvConfNameservers('search example\n')).toEqual([]); }); }); describe('parseUpstreamSpec', () => { it('splits comma- and whitespace-separated entries', () => { expect(parseUpstreamSpec('1.1.1.1, 9.9.9.9 8.8.8.8')).toEqual([ '1.1.1.1', '9.9.9.9', '8.8.8.8', ]); }); }); describe('detectUpstream', () => { it('prefers env override when set', () => { const dir = fs.mkdtempSync(path.join(process.cwd(), 'tmp', 'dns-test-resolv-')); const resolv = path.join(dir, 'resolv.conf'); fs.writeFileSync(resolv, 'nameserver 8.8.8.8\n'); try { expect( detectUpstream({ envValue: '1.0.0.1', resolvConfPath: resolv }), ).toEqual(['1.0.0.1']); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); it('falls back to resolv.conf when env is empty', () => { const dir = fs.mkdtempSync(path.join(process.cwd(), 'tmp', 'dns-test-resolv-')); const resolv = path.join(dir, 'resolv.conf'); fs.writeFileSync(resolv, 'nameserver 1.1.1.1\nnameserver 127.0.0.1\n'); try { expect( detectUpstream({ envValue: '', resolvConfPath: resolv }), ).toEqual(['1.1.1.1']); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); it('falls back to public defaults when nothing else is available', () => { const result = detectUpstream({ envValue: '', resolvConfPath: '/nonexistent/resolv.conf', }); expect(result.length).toBeGreaterThan(0); // First entry must be a routable IP (not loopback). expect(result[0]!.startsWith('127.')).toBe(false); }); }); });