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:
2026-07-09 20:55:27 +00:00
parent 1fca2b8032
commit e5cd6583f2
6 changed files with 621 additions and 2 deletions

View File

@@ -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")