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.
317 lines
7.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|