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

@@ -217,6 +217,96 @@ func TestDefaultSource(t *testing.T) {
}
}
func TestRegisterIdempotent(t *testing.T) {
mgr := NewManager(t.TempDir())
svc := Service{
Domain: "idempotent.example.com",
Source: "test",
Backend: Backend{Address: "127.0.0.1:8080", Type: BackendHTTP},
Reach: ReachInternal,
}
// Register twice with same domain
if err := mgr.Register(svc); err != nil {
t.Fatalf("first Register failed: %v", err)
}
// Update the backend address on second registration
svc.Backend.Address = "127.0.0.1:9090"
if err := mgr.Register(svc); err != nil {
t.Fatalf("second Register failed: %v", err)
}
// Should still have only one service
svcs, err := mgr.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(svcs) != 1 {
t.Errorf("expected 1 service after idempotent register, got %d", len(svcs))
}
// Should have the updated address
got, err := mgr.Get("idempotent.example.com")
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if got.Backend.Address != "127.0.0.1:9090" {
t.Errorf("expected updated backend address 127.0.0.1:9090, got %s", got.Backend.Address)
}
}
func TestListNoDuplicates(t *testing.T) {
mgr := NewManager(t.TempDir())
domains := []string{
"alpha.example.com",
"beta.example.com",
"gamma.example.com",
}
// Register each domain
for _, domain := range domains {
if err := mgr.Register(Service{
Domain: domain,
Source: "test",
Backend: Backend{Address: "127.0.0.1:8080", Type: BackendHTTP},
Reach: ReachInternal,
}); err != nil {
t.Fatalf("Register %s failed: %v", domain, err)
}
}
// Re-register one domain to verify no duplication
if err := mgr.Register(Service{
Domain: "beta.example.com",
Source: "test",
Backend: Backend{Address: "127.0.0.1:9090", Type: BackendHTTP},
Reach: ReachInternal,
}); err != nil {
t.Fatalf("Re-register beta failed: %v", err)
}
svcs, err := mgr.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(svcs) != 3 {
t.Errorf("expected 3 unique services, got %d", len(svcs))
}
// Verify no duplicate domains
seen := make(map[string]bool)
for _, svc := range svcs {
if seen[svc.Domain] {
t.Errorf("duplicate domain in List(): %s", svc.Domain)
}
seen[svc.Domain] = true
}
}
func TestDomainToFilename(t *testing.T) {
tests := []struct {
domain string