clawdie-ai/setup/dns.test.ts
Operator & Codex 149d90196f Fix dnsmasq deploy address handling (C&C)
---
Build: pass | Tests: pass — 2247 passed (666 files)
2026-05-09 13:03:15 +02:00

233 lines
8.2 KiB
TypeScript

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);
});
});
});