Extend catz.yaml config with a bind-conf section mapping catalog names to also-notify IP lists. Zones in catalogs with also-notify configured get an also-notify directive in the generated domains.conf. IPs are deduplicated and sorted when a zone belongs to multiple catalogs.
223 lines
6.7 KiB
Go
223 lines
6.7 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|