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:
2026-07-09 21:07:12 +00:00
parent 112f7c7f55
commit 234a8f8462
5 changed files with 459 additions and 5 deletions

View File

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

View File

@@ -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 != "" {

View File

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