feat: Add comprehensive tests + fix startup reconciliation
Tests added (13 new): - HAProxy ACL ordering: L7 before L4 wildcard, L4 exact before L4 wildcard - HAProxy subdomains: false=exact only, true=wildcard+exact - Reconciliation integration: HAProxy config from services, DNS config from services, Central config domain injection, correct DNS target IPs - dnsmasq: reach:internal has local=/, reach:public does not - Services: idempotent registration, no duplicates in list Bug fix: - Run initial reconciliation on startup so Central's own domain (from config) gets HAProxy + DNS routes even with zero registered services. Previously, Central only got routes after the first service was registered, causing 503 on fresh restart. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -352,6 +352,66 @@ func TestGenerateInstanceConfig_ServiceWithoutInternalDomain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test: reach:internal → local=/ directive present (prevents upstream DNS forwarding)
|
||||
func TestGenerate_ReachInternal_HasLocal(t *testing.T) {
|
||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||
globalCfg := &config.GlobalConfig{}
|
||||
|
||||
// Simulate reconciliation for an internal-reach service:
|
||||
// InternalDomain is set (reach:internal), Domain is empty (reach:public)
|
||||
inst := instanceWith("", "my-api.payne.io", "192.168.8.151")
|
||||
|
||||
out := g.Generate(globalCfg, []config.InstanceConfig{inst})
|
||||
|
||||
if !strings.Contains(out, "local=/my-api.payne.io/") {
|
||||
t.Errorf("reach:internal must produce local=/ directive, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "address=/my-api.payne.io/192.168.8.151") {
|
||||
t.Errorf("reach:internal must produce address entry, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: reach:public → no local=/ directive (allows upstream DNS forwarding)
|
||||
func TestGenerate_ReachPublic_NoLocal(t *testing.T) {
|
||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||
globalCfg := &config.GlobalConfig{}
|
||||
|
||||
// Simulate reconciliation for a public-reach service:
|
||||
// Domain is set (reach:public), InternalDomain is empty
|
||||
inst := instanceWith("cloud.payne.io", "", "192.168.8.240")
|
||||
|
||||
out := g.Generate(globalCfg, []config.InstanceConfig{inst})
|
||||
|
||||
if !strings.Contains(out, "address=/cloud.payne.io/192.168.8.240") {
|
||||
t.Errorf("reach:public must produce address entry, got:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "local=/cloud.payne.io/") {
|
||||
t.Errorf("reach:public must NOT produce local=/ directive, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: InternalDomain field → local=/ entry for LAN-only resolution
|
||||
func TestGenerate_InternalDomainOnly_HasLocal(t *testing.T) {
|
||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||
globalCfg := &config.GlobalConfig{}
|
||||
|
||||
// Instance with only an internal domain (no external domain)
|
||||
inst := instanceWith("", "internal.cloud.payne.io", "192.168.8.240")
|
||||
|
||||
out := g.Generate(globalCfg, []config.InstanceConfig{inst})
|
||||
|
||||
if !strings.Contains(out, "local=/internal.cloud.payne.io/") {
|
||||
t.Errorf("InternalDomain must produce local=/ entry, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "address=/internal.cloud.payne.io/192.168.8.240") {
|
||||
t.Errorf("InternalDomain must produce address entry, got:\n%s", out)
|
||||
}
|
||||
// No external domain → no address for empty domain
|
||||
if strings.Contains(out, "address=//") {
|
||||
t.Errorf("no external domain must not produce empty address=// entry, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Mix of instances with and without internal domains
|
||||
func TestGenerate_MixedServicesAndInstances(t *testing.T) {
|
||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||
|
||||
Reference in New Issue
Block a user