package main import ( "os" "path/filepath" "strings" "testing" ) var emptyCfg = &Config{} func TestGenerateBindConf(t *testing.T) { entries := []ZoneEntry{ {Zone: "bitcard.org.", ZoneFile: "data/misc/bitcard.org", DNSSEC: true, ZonesFile: "test", Line: 1}, {Zone: "askask.com.", ZoneFile: "data/ask/askask.com", ZonesFile: "test", Line: 2}, } got := generateBindConf(entries, emptyCfg) // Header assertContains(t, got, "# THIS FILE IS GENERATED BY catalog-zone-gen") assertContains(t, got, "#=============================================") // Zones should be sorted alphabetically (askask.com before bitcard.org) askIdx := strings.Index(got, "askask.com") bitIdx := strings.Index(got, "bitcard.org") if askIdx == -1 || bitIdx == -1 { t.Fatal("expected both zones in output") } if askIdx >= bitIdx { t.Error("expected askask.com before bitcard.org (alphabetical sort)") } // Non-DNSSEC zone assertContains(t, got, `zone "askask.com" {`) assertContains(t, got, ` type master;`) assertContains(t, got, ` file "data/ask/askask.com";`) // DNSSEC zone has directives on file line assertContains(t, got, ` file "data/misc/bitcard.org"; dnssec-policy standard; inline-signing yes;`) // No trailing dot in zone name if strings.Contains(got, `zone "askask.com."`) { t.Error("zone name should not have trailing dot in BIND config") } } func TestGenerateBindConfEmpty(t *testing.T) { got := generateBindConf(nil, emptyCfg) // Should just have the header lines := splitLines(got) if len(lines) != 3 { t.Errorf("expected 3 header lines for empty input, got %d", len(lines)) } } func TestGenerateBindConfNoDNSSEC(t *testing.T) { entries := []ZoneEntry{ {Zone: "example.com.", ZoneFile: "data/example.com", ZonesFile: "test", Line: 1}, } got := generateBindConf(entries, emptyCfg) if strings.Contains(got, "dnssec-policy") { t.Error("non-DNSSEC zone should not have dnssec-policy") } if strings.Contains(got, "inline-signing") { t.Error("non-DNSSEC zone should not have inline-signing") } } func TestValidateBindConf(t *testing.T) { t.Run("all zones have file", func(t *testing.T) { entries := []ZoneEntry{ {Zone: "a.example.com.", ZoneFile: "data/a", ZonesFile: "zones.txt", Line: 1}, {Zone: "b.example.com.", ZoneFile: "data/b", ZonesFile: "zones.txt", Line: 2}, } if err := writeBindConf(filepath.Join(t.TempDir(), "domains.conf"), entries, emptyCfg); err != nil { t.Fatalf("unexpected error: %v", err) } }) t.Run("zones without file are skipped", func(t *testing.T) { entries := []ZoneEntry{ {Zone: "a.example.com.", ZoneFile: "data/a", ZonesFile: "zones.txt", Line: 1}, {Zone: "dyn.example.com.", ZoneFile: "", ZonesFile: "zones.txt", Line: 3}, } dir := t.TempDir() path := filepath.Join(dir, "domains.conf") if err := writeBindConf(path, entries, emptyCfg); err != nil { t.Fatalf("unexpected error: %v", err) } data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } got := string(data) assertContains(t, got, `zone "a.example.com"`) if strings.Contains(got, "dyn.example.com") { t.Error("catalog-only zone without file= should not appear in BIND config") } }) } func TestWriteBindConf(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "domains.conf") entries := []ZoneEntry{ {Zone: "example.com.", ZoneFile: "data/example.com", ZonesFile: "test", Line: 1}, {Zone: "example.org.", ZoneFile: "data/example.org", DNSSEC: true, ZonesFile: "test", Line: 2}, } if err := writeBindConf(path, entries, emptyCfg); err != nil { t.Fatalf("unexpected error: %v", err) } data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } got := string(data) assertContains(t, got, `zone "example.com"`) assertContains(t, got, `zone "example.org"`) assertContains(t, got, "dnssec-policy standard") } func TestWriteBindConfSkipsCatalogOnlyZones(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "domains.conf") entries := []ZoneEntry{ {Zone: "example.com.", ZoneFile: "", ZonesFile: "zones.txt", Line: 5}, } if err := writeBindConf(path, entries, emptyCfg); err != nil { t.Fatalf("unexpected error: %v", err) } data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } got := string(data) // Should have header but no zone blocks assertContains(t, got, "# THIS FILE IS GENERATED BY catalog-zone-gen") if strings.Contains(got, "example.com") { t.Error("zone without file= should not appear in BIND config") } } func TestGenerateBindConfAlsoNotify(t *testing.T) { cfg := &Config{ BindConf: BindConfConfig{ AlsoNotify: map[string][]string{ "pch": {"198.51.100.1", "198.51.100.2"}, }, }, } t.Run("zone in catalog with also-notify", func(t *testing.T) { entries := []ZoneEntry{ {Zone: "example.com.", Catalogs: []string{"pch"}, ZoneFile: "data/example.com", ZonesFile: "test", Line: 1}, } got := generateBindConf(entries, cfg) assertContains(t, got, "also-notify { 198.51.100.1; 198.51.100.2; };") }) t.Run("zone in catalog without also-notify", func(t *testing.T) { entries := []ZoneEntry{ {Zone: "example.com.", Catalogs: []string{"standard"}, ZoneFile: "data/example.com", ZonesFile: "test", Line: 1}, } got := generateBindConf(entries, cfg) if strings.Contains(got, "also-notify") { t.Error("zone in catalog without also-notify config should not have also-notify") } }) t.Run("zone in multiple catalogs combines IPs", func(t *testing.T) { cfg := &Config{ BindConf: BindConfConfig{ AlsoNotify: map[string][]string{ "pch": {"198.51.100.1"}, "extra": {"198.51.100.2"}, }, }, } entries := []ZoneEntry{ {Zone: "example.com.", Catalogs: []string{"pch", "extra"}, ZoneFile: "data/example.com", ZonesFile: "test", Line: 1}, } got := generateBindConf(entries, cfg) assertContains(t, got, "also-notify { 198.51.100.1; 198.51.100.2; };") }) t.Run("duplicate IPs across catalogs are deduplicated", func(t *testing.T) { cfg := &Config{ BindConf: BindConfConfig{ AlsoNotify: map[string][]string{ "pch": {"198.51.100.1", "198.51.100.2"}, "extra": {"198.51.100.2", "198.51.100.3"}, }, }, } entries := []ZoneEntry{ {Zone: "example.com.", Catalogs: []string{"pch", "extra"}, ZoneFile: "data/example.com", ZonesFile: "test", Line: 1}, } got := generateBindConf(entries, cfg) assertContains(t, got, "also-notify { 198.51.100.1; 198.51.100.2; 198.51.100.3; };") }) t.Run("catalog-only zone without file skips also-notify", func(t *testing.T) { entries := []ZoneEntry{ {Zone: "example.com.", Catalogs: []string{"pch"}, ZoneFile: "", ZonesFile: "test", Line: 1}, } got := generateBindConf(entries, cfg) if strings.Contains(got, "also-notify") { t.Error("catalog-only zone should not appear in bind config at all") } }) }