package output import ( "bytes" "context" "net" "strings" "testing" "github.com/hits/ExploreDNS/internal/dns" "github.com/hits/ExploreDNS/internal/traverse" miekgdns "github.com/miekg/dns" ) // ---- stats.go coverage ---- func TestRRDataString(t *testing.T) { cases := []struct { rr miekgdns.RR want string }{ { &miekgdns.A{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeA}, A: net.ParseIP("1.2.3.4")}, "1.2.3.4", }, { &miekgdns.AAAA{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeAAAA}, AAAA: net.ParseIP("::1")}, "::1", }, { &miekgdns.CNAME{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeCNAME}, Target: "example.com."}, "example.com.", }, { &miekgdns.NS{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeNS}, Ns: "ns1.example.com."}, "ns1.example.com.", }, { &miekgdns.MX{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeMX}, Preference: 10, Mx: "mail.example.com."}, "10 mail.example.com.", }, { &miekgdns.TXT{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeTXT}, Txt: []string{"v=spf1", "include:example.com"}}, "v=spf1 include:example.com", }, } for _, tc := range cases { got := rrDataString(tc.rr) if got != tc.want { t.Errorf("rrDataString(%T) = %q, want %q", tc.rr, got, tc.want) } } } func TestSummaryTypeLabel(t *testing.T) { cases := []struct { input string want string }{ {"nodata", "found no such record"}, {"nxdomain", "name does not exist"}, {"servfail", "resulted in SERVFAIL"}, {"refused", "query refused by server"}, {"notimp", "query type not implemented by server"}, {"cname_loop", "resulted in a CNAME loop"}, {"error", "resulted in an error"}, {"referral", "resulted in a referral"}, {"unknown_type", "unknown_type"}, } for _, tc := range cases { got := summaryTypeLabel(tc.input) if got != tc.want { t.Errorf("summaryTypeLabel(%q) = %q, want %q", tc.input, got, tc.want) } } } func TestCollectServers(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 0.5, nil) resp := &traverse.Response{ Referral: ref, Server: net.ParseIP("1.2.3.4"), Type: traverse.RespAnswer, } results := []traverse.TraversalResult{ {Referral: ref, Response: resp}, } servers := collectServers(results) if len(servers) == 0 { t.Fatal("expected at least one server") } } func TestCollectServersNilServer(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) resp := &traverse.Response{ Referral: ref, Server: nil, Type: traverse.RespAnswer, } results := []traverse.TraversalResult{{Referral: ref, Response: resp}} servers := collectServers(results) if len(servers) != 0 { t.Errorf("expected 0 servers with nil server, got %d", len(servers)) } } func TestCollectServersDedup(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 0.5, nil) resp := &traverse.Response{ Referral: ref, Server: net.ParseIP("1.2.3.4"), Type: traverse.RespAnswer, } results := []traverse.TraversalResult{ {Referral: ref, Response: resp}, {Referral: ref, Response: resp}, } servers := collectServers(results) for _, ips := range servers { for _, ip := range ips { count := 0 for _, i := range ips { if i == ip { count++ } } if count > 1 { t.Errorf("duplicate IP %s in server list", ip) } } } } func TestServerName(t *testing.T) { t.Run("uses bailiwick", func(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 1.0, nil) result := traverse.TraversalResult{Referral: ref, Response: nil} name := serverName(result) if name != "com" { t.Errorf("serverName = %q, want 'com'", name) } }) t.Run("uses NSName when bailiwick is root", func(t *testing.T) { ref := traverse.NewReferral("ns1.example.com.", dns.TypeA, ".", 0, 1.0, nil) ref.NSName = "ns1.example.com." result := traverse.TraversalResult{Referral: ref, Response: nil} name := serverName(result) if name != "ns1.example.com." { t.Errorf("serverName = %q, want 'ns1.example.com.'", name) } }) t.Run("uses server IP from response", func(t *testing.T) { ref := traverse.NewReferral("ns1.example.com.", dns.TypeA, ".", 0, 1.0, nil) resp := &traverse.Response{ Referral: ref, Server: net.ParseIP("1.2.3.4"), } result := traverse.TraversalResult{Referral: ref, Response: resp} name := serverName(result) if name != "1.2.3.4" { t.Errorf("serverName = %q, want '1.2.3.4'", name) } }) t.Run("unknown fallback", func(t *testing.T) { result := traverse.TraversalResult{Referral: nil, Response: nil} name := serverName(result) if name != "unknown" { t.Errorf("serverName = %q, want 'unknown'", name) } }) } func TestComputeSummaryNonAnswerTypes(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) for _, respType := range []traverse.ResponseType{ traverse.RespNXDOMAIN, traverse.RespSERVFAIL, traverse.RespNODATA, } { resp := &traverse.Response{Referral: ref, Type: respType} stats := ComputeSummary([]traverse.TraversalResult{{Referral: ref, Response: resp}}) if stats == nil { t.Errorf("ComputeSummary returned nil for %v", respType) continue } if len(stats.ByType) == 0 { t.Errorf("expected ByType entry for %v", respType) } } } func TestComputeSummaryNilResponse(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) stats := ComputeSummary([]traverse.TraversalResult{{Referral: ref, Response: nil}}) if stats != nil { t.Error("expected nil stats for nil response") } } func TestComputeSummaryNilReferral(t *testing.T) { resp := &traverse.Response{Type: traverse.RespAnswer} stats := ComputeSummary([]traverse.TraversalResult{{Referral: nil, Response: resp}}) if stats != nil { t.Error("expected nil stats for nil referral") } } func TestComputeSummaryAnswerKeyEmpty(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) // Answer with only CNAME (no data key) should go into ByType resp := &traverse.Response{ Referral: ref, Type: traverse.RespAnswer, Decoded: &dns.DecodedResponse{ Answers: []miekgdns.RR{ &miekgdns.CNAME{ Hdr: miekgdns.RR_Header{Name: "www.example.com.", Rrtype: miekgdns.TypeCNAME}, Target: "example.com.", }, }, }, } stats := ComputeSummary([]traverse.TraversalResult{{Referral: ref, Response: resp}}) if stats == nil { t.Fatal("expected non-nil stats") } } // ---- text.go coverage ---- func TestTextFormatterWriteResolve(t *testing.T) { ref := traverse.NewReferral("ns1.example.com.", dns.TypeA, "example.com.", 1, 0.5, nil) var buf bytes.Buffer cfg := DefaultConfig() cfg.Color = false f := newTextFormatter(cfg, &buf) err := f.WriteResolve(traverse.TraversalEvent{ Stage: traverse.EventStart, Result: traverse.TraversalResult{Referral: ref}, IsResolve: true, }) if err != nil { t.Fatalf("WriteResolve: %v", err) } if buf.Len() == 0 { t.Error("expected output from WriteResolve") } } func TestTextFormatterWriteResolveNonStart(t *testing.T) { ref := traverse.NewReferral("ns1.example.com.", dns.TypeA, "example.com.", 1, 0.5, nil) var buf bytes.Buffer cfg := DefaultConfig() f := newTextFormatter(cfg, &buf) err := f.WriteResolve(traverse.TraversalEvent{ Stage: traverse.EventComplete, Result: traverse.TraversalResult{Referral: ref}, IsResolve: true, }) if err != nil { t.Fatalf("WriteResolve: %v", err) } if buf.Len() != 0 { t.Error("expected no output for non-start resolve event") } } func TestTextFormatterWriteServers(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 1.0, nil) resp := &traverse.Response{ Referral: ref, Server: net.ParseIP("1.2.3.4"), Type: traverse.RespAnswer, Decoded: &dns.DecodedResponse{ Answers: []miekgdns.RR{ &miekgdns.A{ Hdr: miekgdns.RR_Header{Name: "example.com.", Rrtype: miekgdns.TypeA}, A: net.ParseIP("1.2.3.4"), }, }, }, } var buf bytes.Buffer cfg := DefaultConfig() cfg.Color = false cfg.ShowServers = true cfg.ShowResults = false cfg.ShowSummaryResults = false f := newTextFormatter(cfg, &buf) err := f.WriteSummary([]traverse.TraversalResult{{Referral: ref, Response: resp}}) if err != nil { t.Fatalf("WriteSummary: %v", err) } if !strings.Contains(buf.String(), "The following servers were encountered:") { t.Errorf("expected server list header, got %q", buf.String()) } } func TestTextFormatterWriteResults(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) resp := &traverse.Response{ Referral: ref, Server: net.ParseIP("1.2.3.4"), Type: traverse.RespAnswer, Decoded: &dns.DecodedResponse{ Answers: []miekgdns.RR{ &miekgdns.A{ Hdr: miekgdns.RR_Header{Name: "example.com.", Rrtype: miekgdns.TypeA}, A: net.ParseIP("93.184.216.34"), }, }, }, } var buf bytes.Buffer cfg := DefaultConfig() cfg.Color = false cfg.ShowServers = false cfg.ShowResults = true cfg.ShowSummaryResults = false f := newTextFormatter(cfg, &buf) err := f.WriteSummary([]traverse.TraversalResult{{Referral: ref, Response: resp}}) if err != nil { t.Fatalf("WriteSummary: %v", err) } if !strings.Contains(buf.String(), "Results:") { t.Errorf("expected 'Results:' header, got %q", buf.String()) } } func TestTextFormatterFormatResultLineAllTypes(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) cases := []struct { respType traverse.ResponseType contains string }{ {traverse.RespNODATA, "no such record"}, {traverse.RespNXDOMAIN, "does not exist"}, {traverse.RespSERVFAIL, "SERVFAIL"}, {traverse.RespREFUSED, "refused"}, {traverse.RespNOTIMPL, "not implemented"}, {traverse.RespCNAMELoop, "CNAME loop"}, {traverse.RespError, "error"}, } cfg := DefaultConfig() cfg.Color = false f := newTextFormatter(cfg, &bytes.Buffer{}) for _, tc := range cases { resp := &traverse.Response{ Referral: ref, Type: tc.respType, } result := traverse.TraversalResult{Referral: ref, Response: resp} line := f.formatResultLine(result) if !strings.Contains(strings.ToLower(line), strings.ToLower(tc.contains)) { t.Errorf("formatResultLine(%v) = %q, want substring %q", tc.respType, line, tc.contains) } } } func TestTextFormatterFormatResultLineErrorWithMessage(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) resp := &traverse.Response{ Referral: ref, Type: traverse.RespError, ErrorMessage: "custom error message", } cfg := DefaultConfig() cfg.Color = false f := newTextFormatter(cfg, &bytes.Buffer{}) line := f.formatResultLine(traverse.TraversalResult{Referral: ref, Response: resp}) if !strings.Contains(line, "custom error message") { t.Errorf("expected custom error message, got %q", line) } } func TestTextFormatterFormatResultLineCNAMELoopWithMessage(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) resp := &traverse.Response{ Referral: ref, Type: traverse.RespCNAMELoop, ErrorMessage: "CNAME loop detected: example.com", } cfg := DefaultConfig() cfg.Color = false f := newTextFormatter(cfg, &bytes.Buffer{}) line := f.formatResultLine(traverse.TraversalResult{Referral: ref, Response: resp}) if !strings.Contains(line, "CNAME loop detected") { t.Errorf("expected CNAME loop message, got %q", line) } } func TestTextFormatterFormatResultLineAnswerMultiple(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) resp := &traverse.Response{ Referral: ref, Type: traverse.RespAnswer, Decoded: &dns.DecodedResponse{ Answers: []miekgdns.RR{ &miekgdns.A{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeA}, A: net.ParseIP("1.1.1.1")}, &miekgdns.A{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeA}, A: net.ParseIP("2.2.2.2")}, }, }, } cfg := DefaultConfig() cfg.Color = false f := newTextFormatter(cfg, &bytes.Buffer{}) line := f.formatResultLine(traverse.TraversalResult{Referral: ref, Response: resp}) if !strings.Contains(line, "/") { t.Errorf("expected '/' separator for multiple answers, got %q", line) } } func TestTextFormatterColorize(t *testing.T) { cfg := DefaultConfig() cfg.Color = true f := newTextFormatter(cfg, &bytes.Buffer{}) colored := f.colorize("hello", colorGreen) if !strings.Contains(colored, "\033[") { t.Error("expected ANSI color code in colored output") } cfg.Color = false f2 := newTextFormatter(cfg, &bytes.Buffer{}) plain := f2.colorize("hello", colorGreen) if plain != "hello" { t.Errorf("expected plain text without color, got %q", plain) } } func TestTextFormatterColorizeEmpty(t *testing.T) { cfg := DefaultConfig() cfg.Color = true f := newTextFormatter(cfg, &bytes.Buffer{}) out := f.colorize("hello", "") if out != "hello" { t.Errorf("empty color should return plain text, got %q", out) } } func TestTextFormatterVerboseProgress(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 0.5, nil) var buf bytes.Buffer cfg := DefaultConfig() cfg.Color = false cfg.Verbose = true f := newTextFormatter(cfg, &buf) err := f.WriteProgress(traverse.TraversalEvent{ Stage: traverse.EventStart, Result: traverse.TraversalResult{Referral: ref}, }) if err != nil { t.Fatalf("WriteProgress: %v", err) } if buf.Len() == 0 { t.Error("expected output with verbose mode") } out := buf.String() if !strings.Contains(out, "com") { t.Errorf("expected bailiwick in verbose output, got %q", out) } } func TestTextFormatterProgressResolving(t *testing.T) { ref := traverse.NewReferral("ns1.example.com.", dns.TypeA, "example.com.", 1, 0.5, nil) // no addresses = resolving var buf bytes.Buffer cfg := DefaultConfig() cfg.Color = false f := newTextFormatter(cfg, &buf) err := f.WriteProgress(traverse.TraversalEvent{ Stage: traverse.EventStart, Result: traverse.TraversalResult{Referral: ref}, }) if err != nil { t.Fatalf("WriteProgress: %v", err) } if !strings.Contains(buf.String(), "resolving") { t.Errorf("expected 'resolving' in output, got %q", buf.String()) } } func TestTextFormatterWriteServersWithVersions(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 1.0, nil) resp := &traverse.Response{ Referral: ref, Server: net.ParseIP("1.2.3.4"), Type: traverse.RespAnswer, Decoded: &dns.DecodedResponse{ Answers: []miekgdns.RR{ &miekgdns.A{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeA}, A: net.ParseIP("1.2.3.4")}, }, }, } var buf bytes.Buffer cfg := DefaultConfig() cfg.Color = false cfg.ShowServers = true cfg.ShowResults = false cfg.ShowSummaryResults = false cfg.ShowVersions = true cfg.Fingerprints = map[string]string{"1.2.3.4": "BIND 9.16"} f := newTextFormatter(cfg, &buf) err := f.WriteSummary([]traverse.TraversalResult{{Referral: ref, Response: resp}}) if err != nil { t.Fatalf("WriteSummary: %v", err) } if !strings.Contains(buf.String(), "BIND 9.16") { t.Errorf("expected version string in server output, got %q", buf.String()) } } func TestTextFormatterWriteResult(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) resp := &traverse.Response{ Referral: ref, Type: traverse.RespAnswer, Decoded: &dns.DecodedResponse{ Answers: []miekgdns.RR{ &miekgdns.A{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeA}, A: net.ParseIP("1.2.3.4")}, }, }, } var buf bytes.Buffer cfg := DefaultConfig() cfg.Color = false f := newTextFormatter(cfg, &buf) err := f.WriteResult(traverse.TraversalResult{Referral: ref, Response: resp}) if err != nil { t.Fatalf("WriteResult: %v", err) } if buf.Len() == 0 { t.Error("expected output from WriteResult") } } func TestTextFormatterWriteResultNilRefs(t *testing.T) { var buf bytes.Buffer cfg := DefaultConfig() f := newTextFormatter(cfg, &buf) err := f.WriteResult(traverse.TraversalResult{Referral: nil, Response: nil}) if err != nil { t.Fatalf("WriteResult: %v", err) } if buf.Len() != 0 { t.Error("expected no output for nil referral/response") } } func TestReferralServerLabelVariants(t *testing.T) { t.Run("with addresses", func(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) ref.Addresses = []net.IP{net.ParseIP("1.2.3.4")} label := referralServerLabel(ref, nil) if !strings.Contains(label, "1.2.3.4") { t.Errorf("expected IP in label, got %q", label) } }) t.Run("with NSName", func(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) ref.NSName = "ns1.example.com." label := referralServerLabel(ref, nil) if label != "ns1.example.com." { t.Errorf("expected NSName, got %q", label) } }) t.Run("with bailiwick", func(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 1.0, nil) label := referralServerLabel(ref, nil) if label != "com" { t.Errorf("expected trimmed bailiwick, got %q", label) } }) t.Run("unknown", func(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) label := referralServerLabel(ref, nil) if label != "unknown" { t.Errorf("expected 'unknown', got %q", label) } }) t.Run("with response server", func(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) resp := &traverse.Response{Server: net.ParseIP("5.6.7.8")} label := referralServerLabel(ref, resp) if label != "5.6.7.8" { t.Errorf("expected server IP, got %q", label) } }) } // ---- json.go coverage ---- func TestJSONFormatterWriteResolve(t *testing.T) { ref := traverse.NewReferral("ns1.example.com.", dns.TypeA, "example.com.", 1, 0.5, nil) var buf bytes.Buffer cfg := DefaultConfig() cfg.Format = FormatJSON cfg.ShowResolves = true f := newJSONFormatter(cfg, &buf) err := f.WriteResolve(traverse.TraversalEvent{ Stage: traverse.EventStart, Result: traverse.TraversalResult{Referral: ref}, IsResolve: true, }) if err != nil { t.Fatalf("WriteResolve: %v", err) } if len(f.payload.Resolves) != 1 { t.Errorf("expected 1 resolve entry, got %d", len(f.payload.Resolves)) } } func TestJSONFormatterWriteResolveShowResolvesFalse(t *testing.T) { ref := traverse.NewReferral("ns1.example.com.", dns.TypeA, "example.com.", 1, 0.5, nil) var buf bytes.Buffer cfg := DefaultConfig() cfg.Format = FormatJSON cfg.ShowResolves = false f := newJSONFormatter(cfg, &buf) err := f.WriteResolve(traverse.TraversalEvent{ Stage: traverse.EventStart, Result: traverse.TraversalResult{Referral: ref}, IsResolve: true, }) if err != nil { t.Fatalf("WriteResolve: %v", err) } if len(f.payload.Resolves) != 0 { t.Errorf("expected 0 resolve entries when ShowResolves=false, got %d", len(f.payload.Resolves)) } } func TestJSONFormatterWriteResult(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) resp := &traverse.Response{ Referral: ref, Server: net.ParseIP("1.2.3.4"), Type: traverse.RespAnswer, Decoded: &dns.DecodedResponse{ Answers: []miekgdns.RR{ &miekgdns.A{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeA}, A: net.ParseIP("1.2.3.4")}, }, }, } var buf bytes.Buffer cfg := DefaultConfig() cfg.Format = FormatJSON cfg.ShowAllStats = true f := newJSONFormatter(cfg, &buf) err := f.WriteResult(traverse.TraversalResult{Referral: ref, Response: resp}) if err != nil { t.Fatalf("WriteResult: %v", err) } if len(f.payload.Results) != 1 { t.Errorf("expected 1 result entry, got %d", len(f.payload.Results)) } } func TestJSONFormatterWriteResultShowAllStatsFalse(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) resp := &traverse.Response{Referral: ref, Type: traverse.RespAnswer} var buf bytes.Buffer cfg := DefaultConfig() cfg.Format = FormatJSON cfg.ShowAllStats = false f := newJSONFormatter(cfg, &buf) err := f.WriteResult(traverse.TraversalResult{Referral: ref, Response: resp}) if err != nil { t.Fatalf("WriteResult: %v", err) } if len(f.payload.Results) != 0 { t.Errorf("expected 0 result entries when ShowAllStats=false, got %d", len(f.payload.Results)) } } func TestJSONFormatterWriteSummaryWithServersAndVersions(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 1.0, nil) resp := &traverse.Response{ Referral: ref, Server: net.ParseIP("1.2.3.4"), Type: traverse.RespAnswer, Decoded: &dns.DecodedResponse{ Answers: []miekgdns.RR{ &miekgdns.A{Hdr: miekgdns.RR_Header{Rrtype: miekgdns.TypeA}, A: net.ParseIP("1.2.3.4")}, }, }, } var buf bytes.Buffer cfg := DefaultConfig() cfg.Format = FormatJSON cfg.ShowServers = true cfg.ShowVersions = true cfg.Fingerprints = map[string]string{"1.2.3.4": "BIND 9.16"} f := newJSONFormatter(cfg, &buf) err := f.WriteSummary([]traverse.TraversalResult{{Referral: ref, Response: resp}}) if err != nil { t.Fatalf("WriteSummary: %v", err) } found := false for _, srv := range f.payload.Servers { if srv.Version == "BIND 9.16" { found = true } } if !found { t.Error("expected version in server list") } } func TestJSONFormatterStageName(t *testing.T) { if stageName(traverse.EventStart) != "start" { t.Errorf("expected 'start', got %q", stageName(traverse.EventStart)) } if stageName(traverse.EventComplete) != "complete" { t.Errorf("expected 'complete', got %q", stageName(traverse.EventComplete)) } if stageName(traverse.EventStage(99)) != "unknown" { t.Errorf("expected 'unknown' for unknown stage") } } func TestJSONFormatterEventToJSONNilReferral(t *testing.T) { var buf bytes.Buffer cfg := DefaultConfig() cfg.Format = FormatJSON f := newJSONFormatter(cfg, &buf) item := f.eventToJSON(traverse.TraversalEvent{ Stage: traverse.EventStart, Result: traverse.TraversalResult{Referral: nil}, }) if item.Name != "" { t.Errorf("expected empty name for nil referral, got %q", item.Name) } } func TestJSONFormatterEventToJSONWithResponse(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) resp := &traverse.Response{ Referral: ref, Server: net.ParseIP("1.2.3.4"), } var buf bytes.Buffer cfg := DefaultConfig() cfg.Format = FormatJSON f := newJSONFormatter(cfg, &buf) item := f.eventToJSON(traverse.TraversalEvent{ Stage: traverse.EventStart, Result: traverse.TraversalResult{Referral: ref, Response: resp}, }) if item.Server != "1.2.3.4" { t.Errorf("expected server IP, got %q", item.Server) } } func TestJSONFormatterWriteProgressShowProgressFalse(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) var buf bytes.Buffer cfg := DefaultConfig() cfg.Format = FormatJSON cfg.ShowProgress = false f := newJSONFormatter(cfg, &buf) err := f.WriteProgress(traverse.TraversalEvent{ Stage: traverse.EventStart, Result: traverse.TraversalResult{Referral: ref}, }) if err != nil { t.Fatalf("WriteProgress: %v", err) } if len(f.payload.Progress) != 0 { t.Errorf("expected 0 progress entries when ShowProgress=false, got %d", len(f.payload.Progress)) } } // ---- runner.go coverage ---- func TestRunTraversalNilTraverser(t *testing.T) { _, err := RunTraversal(context.Background(), nil, nil, nil, "example.com") if err == nil { t.Fatal("expected error for nil traverser") } } func TestNewFormatterNilConfig(t *testing.T) { f := NewFormatter(nil, &bytes.Buffer{}) if f == nil { t.Fatal("NewFormatter(nil) should not return nil") } } func TestAttachHooksNilCfg(t *testing.T) { h := AttachHooks(nil, nil) if h != nil { t.Fatal("AttachHooks(nil, nil) should return nil") } } func TestAttachHooksDebugMode(t *testing.T) { ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil) var buf bytes.Buffer cfg := DefaultConfig() cfg.Debug = 1 cfg.ShowResolves = true // formatter that returns an error on WriteResolve formatter := &errorFormatter{} hooks := AttachHooks(cfg, formatter) if hooks == nil { t.Fatal("expected non-nil hooks") } // Call OnEvent with IsResolve=true - should call WriteResolve and log error to stderr (debug>0) hooks.OnEvent(traverse.TraversalEvent{ Stage: traverse.EventStart, Result: traverse.TraversalResult{Referral: ref}, IsResolve: true, }) _ = buf.String() // no assertion - just ensure it doesn't panic } // errorFormatter is a mock formatter for testing error paths. type errorFormatter struct{} func (f *errorFormatter) WriteProgress(_ traverse.TraversalEvent) error { return nil } func (f *errorFormatter) WriteResolve(_ traverse.TraversalEvent) error { return nil } func (f *errorFormatter) WriteResult(_ traverse.TraversalResult) error { return nil } func (f *errorFormatter) WriteSummary(_ []traverse.TraversalResult) error { return nil } func (f *errorFormatter) Flush() error { return nil }