package main import ( "os" "os/exec" "path/filepath" "strings" "testing" ) func TestIntegrationEndToEnd(t *testing.T) { dir := t.TempDir() // Write config configContent := `catalogs: catalog1: zone: catalog1.example.com. catalog2: zone: catalog2.example.com. soa: mname: ns1.example.com. rname: hostmaster.example.com. ` writeTestFile(t, dir, "catz.yaml", configContent) // Write input inputContent := `# Test zones zone.example.org catalog1, catalog2 zone.example.com catalog2, coo=old-catalog.example.com. test.example.net catalog1, group=internal app.example.org catalog1, group=external, coo=migrated.example.com. ` writeTestFile(t, dir, "zones.txt", inputContent) cfg, err := loadConfig(filepath.Join(dir, "catz.yaml")) if err != nil { t.Fatalf("loadConfig: %v", err) } members, err := parseInput(filepath.Join(dir, "zones.txt"), cfg) if err != nil { t.Fatalf("parseInput: %v", err) } now := fixedTime(2026, 1, 15) // First run: should create files for catName, catMembers := range members { changed, err := processCatalog(catName, cfg, catMembers, dir, now) if err != nil { t.Fatalf("processCatalog(%s): %v", catName, err) } if !changed { t.Errorf("expected %s to be changed on first run", catName) } } // Verify files exist cat1Path := filepath.Join(dir, "catalog1.example.com.zone") cat2Path := filepath.Join(dir, "catalog2.example.com.zone") cat1Content := readTestFile(t, cat1Path) cat2Content := readTestFile(t, cat2Path) // Verify catalog1 content assertContains(t, cat1Content, "catalog1.example.com.\t0\tIN\tSOA") assertContains(t, cat1Content, "catalog1.example.com.\t0\tIN\tNS\tinvalid.") assertContains(t, cat1Content, "version.catalog1.example.com.\t0\tIN\tTXT\t\"2\"") assertContains(t, cat1Content, "IN\tPTR\tapp.example.org.") assertContains(t, cat1Content, "IN\tPTR\ttest.example.net.") assertContains(t, cat1Content, "IN\tPTR\tzone.example.org.") assertContains(t, cat1Content, "IN\tTXT\t\"internal\"") assertContains(t, cat1Content, "IN\tTXT\t\"external\"") assertContains(t, cat1Content, "IN\tPTR\tmigrated.example.com.") // Verify catalog2 content assertContains(t, cat2Content, "catalog2.example.com.\t0\tIN\tSOA") assertContains(t, cat2Content, "IN\tPTR\tzone.example.com.") assertContains(t, cat2Content, "IN\tPTR\tzone.example.org.") assertContains(t, cat2Content, "IN\tPTR\told-catalog.example.com.") // Verify serial format assertContains(t, cat1Content, "2026011501") // Second run (same time): should be unchanged for catName, catMembers := range members { changed, err := processCatalog(catName, cfg, catMembers, dir, now) if err != nil { t.Fatalf("processCatalog(%s) second run: %v", catName, err) } if changed { t.Errorf("expected %s to be unchanged on second run", catName) } } // Verify content hasn't changed cat1After := readTestFile(t, cat1Path) if cat1After != cat1Content { t.Error("catalog1 content changed on idempotent run") } } func TestIntegrationSerialBump(t *testing.T) { dir := t.TempDir() configContent := `catalogs: cat1: zone: cat1.example.com. soa: mname: ns1.example.com. rname: hostmaster.example.com. ` writeTestFile(t, dir, "catz.yaml", configContent) cfg, err := loadConfig(filepath.Join(dir, "catz.yaml")) if err != nil { t.Fatal(err) } members1 := []ZoneEntry{ {Zone: "a.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 1}, } now := fixedTime(2026, 1, 15) // First run changed, err := processCatalog("cat1", cfg, members1, dir, now) if err != nil { t.Fatal(err) } if !changed { t.Error("expected changed on first run") } content1 := readTestFile(t, filepath.Join(dir, "cat1.example.com.zone")) assertContains(t, content1, "2026011501") // Second run with different input (same day) members2 := []ZoneEntry{ {Zone: "a.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 1}, {Zone: "b.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 2}, } changed, err = processCatalog("cat1", cfg, members2, dir, now) if err != nil { t.Fatal(err) } if !changed { t.Error("expected changed when input differs") } content2 := readTestFile(t, filepath.Join(dir, "cat1.example.com.zone")) assertContains(t, content2, "2026011502") // Third run with same input: unchanged changed, err = processCatalog("cat1", cfg, members2, dir, now) if err != nil { t.Fatal(err) } if changed { t.Error("expected unchanged on third run") } // Fourth run next day with same input: new serial nextDay := fixedTime(2026, 1, 16) changed, err = processCatalog("cat1", cfg, members2, dir, nextDay) if err != nil { t.Fatal(err) } // Content hasn't changed, so this should be unchanged even though date differs if changed { t.Error("expected unchanged when content hasn't changed") } } func TestIntegrationSortOrder(t *testing.T) { dir := t.TempDir() configContent := `catalogs: cat1: zone: cat1.example.com. soa: mname: ns1.example.com. rname: hostmaster.example.com. ` writeTestFile(t, dir, "catz.yaml", configContent) cfg, err := loadConfig(filepath.Join(dir, "catz.yaml")) if err != nil { t.Fatal(err) } // Input in reverse order members := []ZoneEntry{ {Zone: "z.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 1}, {Zone: "m.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 2}, {Zone: "a.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 3}, } now := fixedTime(2026, 1, 15) _, err = processCatalog("cat1", cfg, members, dir, now) if err != nil { t.Fatal(err) } content := readTestFile(t, filepath.Join(dir, "cat1.example.com.zone")) lines := splitLines(content) // Find PTR lines and verify order var ptrZones []string for _, line := range lines { if strings.Contains(line, "\tPTR\t") && !strings.Contains(line, "coo.") { // Extract the PTR target parts := strings.Split(line, "\t") ptrZones = append(ptrZones, parts[len(parts)-1]) } } if len(ptrZones) != 3 { t.Fatalf("expected 3 PTR records, got %d", len(ptrZones)) } if ptrZones[0] != "a.example.com." || ptrZones[1] != "m.example.com." || ptrZones[2] != "z.example.com." { t.Errorf("PTR records not sorted: %v", ptrZones) } } func TestIntegrationCLI(t *testing.T) { // Build the binary binary := filepath.Join(t.TempDir(), "catalog-zone-gen") cmd := exec.Command("go", "build", "-o", binary, ".") cmd.Dir = projectDir(t) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("go build failed: %v\n%s", err, out) } dir := t.TempDir() configContent := `catalogs: catalog1: zone: catalog1.example.com. soa: mname: ns1.example.com. rname: hostmaster.example.com. ` writeTestFile(t, dir, "catz.yaml", configContent) inputContent := "a.example.com. catalog1\n" writeTestFile(t, dir, "zones.txt", inputContent) // Run the binary cmd = exec.Command(binary, "--config", filepath.Join(dir, "catz.yaml"), "--output-dir", dir, filepath.Join(dir, "zones.txt")) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("catalog-zone-gen failed: %v\n%s", err, out) } // Verify output file exists zonePath := filepath.Join(dir, "catalog1.example.com.zone") if _, err := os.Stat(zonePath); err != nil { t.Fatalf("output file not created: %v", err) } content := readTestFile(t, zonePath) assertContains(t, content, "catalog1.example.com.") assertContains(t, content, "a.example.com.") } func TestIntegrationCLIErrors(t *testing.T) { binary := filepath.Join(t.TempDir(), "catalog-zone-gen") cmd := exec.Command("go", "build", "-o", binary, ".") cmd.Dir = projectDir(t) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("go build failed: %v\n%s", err, out) } t.Run("no arguments", func(t *testing.T) { cmd := exec.Command(binary) err := cmd.Run() if err == nil { t.Error("expected error with no arguments") } }) t.Run("missing input file", func(t *testing.T) { cmd := exec.Command(binary, "/nonexistent/zones.txt") err := cmd.Run() if err == nil { t.Error("expected error with missing input file") } }) t.Run("unknown catalog in input", func(t *testing.T) { dir := t.TempDir() writeTestFile(t, dir, "catz.yaml", `catalogs: cat1: zone: cat1.example.com. soa: mname: ns1.example.com. rname: hostmaster.example.com. `) writeTestFile(t, dir, "zones.txt", "a.example.com. unknown_catalog\n") cmd := exec.Command(binary, "--config", filepath.Join(dir, "catz.yaml"), "--output-dir", dir, filepath.Join(dir, "zones.txt")) out, err := cmd.CombinedOutput() if err == nil { t.Error("expected error for unknown catalog") } assertContains(t, string(out), "unknown catalog") }) } func writeTestFile(t *testing.T, dir, name, content string) { t.Helper() if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { t.Fatal(err) } } func readTestFile(t *testing.T, path string) string { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } return string(data) } func projectDir(t *testing.T) string { t.Helper() // Find the project root by looking for go.mod dir, err := os.Getwd() if err != nil { t.Fatal(err) } for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir } parent := filepath.Dir(dir) if parent == dir { t.Fatal("could not find project root (go.mod)") } dir = parent } } // Verify the serial stays the same when the same time is used but content changes on a new day. func TestIntegrationSerialNewDay(t *testing.T) { dir := t.TempDir() configContent := `catalogs: cat1: zone: cat1.example.com. soa: mname: ns1.example.com. rname: hostmaster.example.com. ` writeTestFile(t, dir, "catz.yaml", configContent) cfg, err := loadConfig(filepath.Join(dir, "catz.yaml")) if err != nil { t.Fatal(err) } members := []ZoneEntry{ {Zone: "a.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 1}, } day1 := fixedTime(2026, 1, 15) _, err = processCatalog("cat1", cfg, members, dir, day1) if err != nil { t.Fatal(err) } content1 := readTestFile(t, filepath.Join(dir, "cat1.example.com.zone")) assertContains(t, content1, "2026011501") // Add a zone on day 2 members2 := []ZoneEntry{ {Zone: "a.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 1}, {Zone: "b.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 2}, } day2 := fixedTime(2026, 1, 16) changed, err := processCatalog("cat1", cfg, members2, dir, day2) if err != nil { t.Fatal(err) } if !changed { t.Error("expected changed with new zone") } content2 := readTestFile(t, filepath.Join(dir, "cat1.example.com.zone")) assertContains(t, content2, "2026011601") // Simulate not using the tool for a while, running on a much later date day3 := fixedTime(2026, 6, 15) members3 := []ZoneEntry{ {Zone: "a.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 1}, {Zone: "b.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 2}, {Zone: "c.example.com.", Catalogs: []string{"cat1"}, File: "test", Line: 3}, } changed, err = processCatalog("cat1", cfg, members3, dir, day3) if err != nil { t.Fatal(err) } if !changed { t.Error("expected changed with new zone") } content3 := readTestFile(t, filepath.Join(dir, "cat1.example.com.zone")) assertContains(t, content3, "2026061501") }