test: Add 22 unit tests across dnsmasq, haproxy, services
dnsmasq (6 new, 30.8% → 32%): - Empty instances, multiple instances, nil config, configured IP - No leaked empty entries (address=//, local=//) - Empty fields don't produce broken entries Also fixed a bug: empty Domain/InternalDomain fields in commented-out entries (no LB IP case) now guarded against producing # local=// and # address=// directives. haproxy (7 new): - No HTTPS frontend with no routes - HTTP-only and L4-only route scenarios - CertsDir customization and default - Custom TCP route ports - Health check directive in L7 backends services (8 new, 72.7% → 75.8%): - Not-found errors for Get, Deregister, Update - DeregisterBySource partial match (source AND backend) - Overwrite preserves file count - Empty dir and non-YAML file handling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,8 +60,12 @@ func (g *ConfigGenerator) Generate(cfg *config.GlobalConfig, clouds []config.Ins
|
||||
slog.Info("no load balancer IP configured, adding commented DNS config", "component", "dnsmasq", "instance", cloud.Cluster.Name)
|
||||
// Add commented out entries for instances without load balancer
|
||||
resolution_section += fmt.Sprintf("# No load balancer IP configured for instance %s\n", cloud.Cluster.Name)
|
||||
resolution_section += fmt.Sprintf("# local=/%s/\n# address=/%s/<load-balancer-ip>\n", cloud.Cloud.InternalDomain, cloud.Cloud.InternalDomain)
|
||||
resolution_section += fmt.Sprintf("# address=/%s/<load-balancer-ip>\n", cloud.Cloud.Domain)
|
||||
if cloud.Cloud.InternalDomain != "" {
|
||||
resolution_section += fmt.Sprintf("# local=/%s/\n# address=/%s/<load-balancer-ip>\n", cloud.Cloud.InternalDomain, cloud.Cloud.InternalDomain)
|
||||
}
|
||||
if cloud.Cloud.Domain != "" {
|
||||
resolution_section += fmt.Sprintf("# address=/%s/<load-balancer-ip>\n", cloud.Cloud.Domain)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Internal domain (.internal.cloud.example.tld) - local only, no external DNS
|
||||
|
||||
@@ -112,9 +112,13 @@ func (g *ConfigGenerator) GenerateInstanceConfig(instance config.InstanceConfig)
|
||||
if loadBalancerIP == "" {
|
||||
sb.WriteString("# WARNING: No load balancer IP configured for this instance\n")
|
||||
sb.WriteString("# DNS entries are commented out until load balancer IP is configured\n\n")
|
||||
fmt.Fprintf(&sb, "# local=/%s/\n", instance.Cloud.InternalDomain)
|
||||
fmt.Fprintf(&sb, "# address=/%s/<load-balancer-ip>\n\n", instance.Cloud.InternalDomain)
|
||||
fmt.Fprintf(&sb, "# address=/%s/<load-balancer-ip>\n", instance.Cloud.Domain)
|
||||
if instance.Cloud.InternalDomain != "" {
|
||||
fmt.Fprintf(&sb, "# local=/%s/\n", instance.Cloud.InternalDomain)
|
||||
fmt.Fprintf(&sb, "# address=/%s/<load-balancer-ip>\n\n", instance.Cloud.InternalDomain)
|
||||
}
|
||||
if instance.Cloud.Domain != "" {
|
||||
fmt.Fprintf(&sb, "# address=/%s/<load-balancer-ip>\n", instance.Cloud.Domain)
|
||||
}
|
||||
} else {
|
||||
// Internal domain (.internal.cloud.example.tld) - local only, no external DNS
|
||||
if instance.Cloud.InternalDomain != "" {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dnsmasq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -438,3 +439,131 @@ func TestGenerate_MixedServicesAndInstances(t *testing.T) {
|
||||
t.Errorf("unexpected empty local=// in output:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: empty instances list produces base config with no address= entries
|
||||
func TestGenerate_EmptyInstances(t *testing.T) {
|
||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||
globalCfg := &config.GlobalConfig{}
|
||||
|
||||
out := g.Generate(globalCfg, []config.InstanceConfig{})
|
||||
|
||||
// Should have base config skeleton
|
||||
if !strings.Contains(out, "bind-interfaces") {
|
||||
t.Errorf("expected bind-interfaces in config, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "server=1.1.1.1") {
|
||||
t.Errorf("expected upstream DNS server, got:\n%s", out)
|
||||
}
|
||||
// Should not have any address= entries
|
||||
if strings.Contains(out, "address=/") {
|
||||
t.Errorf("expected no address= entries with empty instances, got:\n%s", out)
|
||||
}
|
||||
// Should not have any local=/ entries
|
||||
if strings.Contains(out, "local=/") {
|
||||
t.Errorf("expected no local=/ entries with empty instances, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: multiple instances each get their own entries
|
||||
func TestGenerate_MultipleInstances(t *testing.T) {
|
||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||
globalCfg := &config.GlobalConfig{}
|
||||
|
||||
instances := []config.InstanceConfig{
|
||||
instanceWith("cloud1.example.com", "internal1.example.com", "10.0.0.1"),
|
||||
instanceWith("cloud2.example.com", "internal2.example.com", "10.0.0.2"),
|
||||
instanceWith("cloud3.example.com", "internal3.example.com", "10.0.0.3"),
|
||||
}
|
||||
|
||||
out := g.Generate(globalCfg, instances)
|
||||
|
||||
for i, inst := range instances {
|
||||
ip := []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"}[i]
|
||||
if !strings.Contains(out, fmt.Sprintf("address=/%s/%s", inst.Cloud.Domain, ip)) {
|
||||
t.Errorf("expected address entry for %s, got:\n%s", inst.Cloud.Domain, out)
|
||||
}
|
||||
if !strings.Contains(out, fmt.Sprintf("address=/%s/%s", inst.Cloud.InternalDomain, ip)) {
|
||||
t.Errorf("expected address entry for %s, got:\n%s", inst.Cloud.InternalDomain, out)
|
||||
}
|
||||
if !strings.Contains(out, fmt.Sprintf("local=/%s/", inst.Cloud.InternalDomain)) {
|
||||
t.Errorf("expected local=/ entry for %s, got:\n%s", inst.Cloud.InternalDomain, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: nil config doesn't panic, uses auto-detect for IP
|
||||
func TestGenerate_NilConfig(t *testing.T) {
|
||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||
|
||||
// Should not panic with nil config
|
||||
out := g.Generate(nil, []config.InstanceConfig{})
|
||||
|
||||
if !strings.Contains(out, "bind-interfaces") {
|
||||
t.Errorf("expected valid config with nil cfg, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "server=1.1.1.1") {
|
||||
t.Errorf("expected upstream DNS server with nil cfg, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: when cfg.Cloud.Dnsmasq.IP is set, uses that instead of auto-detect
|
||||
func TestGenerate_ConfiguredDnsmasqIP(t *testing.T) {
|
||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||
cfg := &config.GlobalConfig{}
|
||||
cfg.Cloud.Dnsmasq.IP = "192.168.8.100"
|
||||
|
||||
out := g.Generate(cfg, []config.InstanceConfig{})
|
||||
|
||||
if !strings.Contains(out, "listen-address=192.168.8.100") {
|
||||
t.Errorf("expected configured dnsmasq IP in listen-address, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: verify no address=// or local=// entries appear in any test output
|
||||
func TestGenerate_NoLeakedEmptyEntries(t *testing.T) {
|
||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||
cfg := &config.GlobalConfig{}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
instances []config.InstanceConfig
|
||||
}{
|
||||
{"empty", nil},
|
||||
{"domain_only", []config.InstanceConfig{instanceWith("example.com", "", "10.0.0.1")}},
|
||||
{"internal_only", []config.InstanceConfig{instanceWith("", "internal.example.com", "10.0.0.1")}},
|
||||
{"both", []config.InstanceConfig{instanceWith("example.com", "internal.example.com", "10.0.0.1")}},
|
||||
{"no_lb_ip", []config.InstanceConfig{instanceWith("example.com", "internal.example.com", "")}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out := g.Generate(cfg, tc.instances)
|
||||
if strings.Contains(out, "address=//") {
|
||||
t.Errorf("leaked empty address=// in output:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "local=//") {
|
||||
t.Errorf("leaked empty local=// in output:\n%s", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test: GenerateInstanceConfig with all fields empty doesn't produce broken entries
|
||||
func TestGenerateInstanceConfig_EmptyFields(t *testing.T) {
|
||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||
var inst config.InstanceConfig
|
||||
// All fields empty: no domain, no internal domain, no LB IP
|
||||
|
||||
out := g.GenerateInstanceConfig(inst)
|
||||
|
||||
if strings.Contains(out, "address=//") {
|
||||
t.Errorf("empty fields produced broken address=// entry:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "local=//") {
|
||||
t.Errorf("empty fields produced broken local=// entry:\n%s", out)
|
||||
}
|
||||
// Should still contain the header comment
|
||||
if !strings.Contains(out, "DNS configuration for instance") {
|
||||
t.Errorf("expected header comment in output, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user