Files
catz/catalog_test.go
Ask Bjørn Hansen 0eddb9fcfe Add --bind-conf flag for BIND domains.conf generation
Generate a BIND-format domains.conf file alongside catalog zones.
New input properties: file= (zone data path) and dnssec (bare flag).
When --bind-conf is set, every zone must have file= or it errors.

Renames ZoneEntry.File to ZonesFile (input path for error messages)
and adds ZoneFile (BIND file path) and DNSSEC (bool) fields.
2026-03-28 11:15:06 -07:00

317 lines
7.1 KiB
Go

package main
import (
"path/filepath"
"strings"
"testing"
)
func TestHashZoneName(t *testing.T) {
tests := []struct {
zone string
want string
}{
{"example.com.", "lq2vr60"},
{"example.org.", "an5oaj8"},
// Same input should always produce same output
{"example.com.", "lq2vr60"},
}
for _, tt := range tests {
got := hashZoneName(tt.zone)
if got != tt.want {
t.Errorf("hashZoneName(%q) = %q, want %q", tt.zone, got, tt.want)
}
}
}
func TestHashZoneNameUniqueness(t *testing.T) {
zones := []string{
"example.com.",
"example.org.",
"example.net.",
"test.example.com.",
"app.example.org.",
"zone.example.org.",
"zone.example.com.",
"test.example.net.",
}
hashes := make(map[string]string)
for _, zone := range zones {
h := hashZoneName(zone)
if existing, ok := hashes[h]; ok {
t.Errorf("hash collision: %q and %q both hash to %q", existing, zone, h)
}
hashes[h] = zone
}
}
func TestBumpSerial(t *testing.T) {
tests := []struct {
name string
old uint32
year int
month int
day int
want uint32
wantErr bool
}{
{
name: "new serial from zero",
old: 0,
year: 2026, month: 3, day: 2,
want: 2026030201,
},
{
name: "bump same date",
old: 2026030201,
year: 2026, month: 3, day: 2,
want: 2026030202,
},
{
name: "new date",
old: 2026030205,
year: 2026, month: 3, day: 3,
want: 2026030301,
},
{
name: "old serial different format",
old: 12345,
year: 2026, month: 3, day: 2,
want: 2026030201,
},
{
name: "overflow at 99",
old: 2026030299,
year: 2026, month: 3, day: 2,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now := fixedTime(tt.year, tt.month, tt.day)
got, err := bumpSerial(tt.old, now)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("bumpSerial(%d) = %d, want %d", tt.old, got, tt.want)
}
})
}
}
func TestDefaultSerial(t *testing.T) {
now := fixedTime(2026, 3, 2)
got := defaultSerial(now)
want := uint32(2026030201)
if got != want {
t.Errorf("defaultSerial() = %d, want %d", got, want)
}
}
func TestGenerateCatalogZone(t *testing.T) {
cfg := &Config{
Catalogs: map[string]CatalogConfig{
"cat1": {Zone: "catalog.example.com."},
},
SOA: SOAConfig{
Mname: "ns1.example.com.",
Rname: "hostmaster.example.com.",
},
}
members := []ZoneEntry{
{Zone: "b.example.com.", Catalogs: []string{"cat1"}, ZonesFile: "test", Line: 1},
{Zone: "a.example.com.", Catalogs: []string{"cat1"}, Group: "mygroup", ZonesFile: "test", Line: 2},
}
content, err := generateCatalogZone("cat1", cfg, members, 2026030201)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify SOA is first line
lines := splitLines(content)
if len(lines) < 4 {
t.Fatalf("expected at least 4 lines, got %d", len(lines))
}
// SOA should contain the serial
assertContains(t, lines[0], "2026030201")
assertContains(t, lines[0], "SOA")
// NS should be second
assertContains(t, lines[1], "NS")
assertContains(t, lines[1], "invalid.")
// Version TXT should be third
assertContains(t, lines[2], "TXT")
assertContains(t, lines[2], "\"2\"")
// a.example.com should come before b.example.com (sorted)
aIdx := -1
bIdx := -1
for i, line := range lines {
if strings.Contains(line, "a.example.com.") {
if aIdx == -1 {
aIdx = i
}
}
if strings.Contains(line, "b.example.com.") {
if bIdx == -1 {
bIdx = i
}
}
}
if aIdx == -1 || bIdx == -1 {
t.Fatal("expected both a.example.com and b.example.com in output")
}
if aIdx >= bIdx {
t.Errorf("a.example.com (line %d) should come before b.example.com (line %d)", aIdx, bIdx)
}
// a.example.com should have a group TXT
foundGroup := false
for _, line := range lines {
if strings.Contains(line, "group.") && strings.Contains(line, "\"mygroup\"") {
foundGroup = true
}
}
if !foundGroup {
t.Error("expected group TXT record for a.example.com")
}
}
func TestGenerateCatalogZoneCOO(t *testing.T) {
cfg := &Config{
Catalogs: map[string]CatalogConfig{
"cat1": {Zone: "catalog.example.com."},
},
SOA: SOAConfig{
Mname: "ns1.example.com.",
Rname: "hostmaster.example.com.",
},
}
members := []ZoneEntry{
{Zone: "a.example.com.", Catalogs: []string{"cat1"}, COO: "old.example.com.", ZonesFile: "test", Line: 1},
}
content, err := generateCatalogZone("cat1", cfg, members, 2026030201)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
foundCOO := false
for _, line := range splitLines(content) {
if strings.Contains(line, "coo.") && strings.Contains(line, "old.example.com.") {
foundCOO = true
}
}
if !foundCOO {
t.Error("expected coo PTR record")
}
}
func TestReadExisting(t *testing.T) {
t.Run("file does not exist", func(t *testing.T) {
content, serial, err := readExisting("/nonexistent/path.zone")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if serial != 0 {
t.Errorf("serial = %d, want 0", serial)
}
if content != "" {
t.Errorf("content = %q, want empty", content)
}
})
t.Run("valid zone file", func(t *testing.T) {
dir := t.TempDir()
zoneContent := "example.com. 0 IN SOA ns1.example.com. hostmaster.example.com. 2026030205 900 600 2147483646 0\nexample.com. 0 IN NS invalid.\n"
writeTestFile(t, dir, "test.zone", zoneContent)
content, serial, err := readExisting(filepath.Join(dir, "test.zone"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if serial != 2026030205 {
t.Errorf("serial = %d, want 2026030205", serial)
}
if content != zoneContent {
t.Errorf("content mismatch: got %q", content)
}
})
t.Run("zone file with no SOA", func(t *testing.T) {
dir := t.TempDir()
zoneContent := "example.com. 0 IN NS invalid.\n"
writeTestFile(t, dir, "test.zone", zoneContent)
content, serial, err := readExisting(filepath.Join(dir, "test.zone"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if serial != 0 {
t.Errorf("serial = %d, want 0 (no SOA found)", serial)
}
if content != zoneContent {
t.Errorf("content mismatch: got %q", content)
}
})
}
func TestGenerateCatalogZoneEmpty(t *testing.T) {
cfg := &Config{
Catalogs: map[string]CatalogConfig{
"cat1": {Zone: "catalog.example.com."},
},
SOA: SOAConfig{
Mname: "ns1.example.com.",
Rname: "hostmaster.example.com.",
},
}
content, err := generateCatalogZone("cat1", cfg, nil, 2026030201)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
lines := splitLines(content)
if len(lines) != 3 {
t.Fatalf("expected 3 lines (SOA, NS, version TXT), got %d", len(lines))
}
assertContains(t, lines[0], "SOA")
assertContains(t, lines[1], "NS")
assertContains(t, lines[2], "TXT")
}
func TestNormalizeFQDN(t *testing.T) {
tests := []struct {
in, want string
}{
{"example.com.", "example.com."},
{"example.com", "example.com."},
{"Example.COM.", "example.com."},
{" example.com ", "example.com."},
{"", ""},
}
for _, tt := range tests {
got := normalizeFQDN(tt.in)
if got != tt.want {
t.Errorf("normalizeFQDN(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}