CI / test (pull_request) Failing after 2m11s
Add comprehensive test coverage for internal packages: - internal/config: 66.2% → 98.5% - internal/dns: 67.8% → 84.3% - internal/output: 48.8% → 89.1% - internal/traverse: 56.3% → 86.9% Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai>
927 lines
26 KiB
Go
927 lines
26 KiB
Go
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 TestRRDataStringDefault(t *testing.T) {
|
|
soa := &miekgdns.SOA{
|
|
Hdr: miekgdns.RR_Header{Name: "example.com.", Rrtype: miekgdns.TypeSOA, Class: miekgdns.ClassINET, Ttl: 3600},
|
|
Ns: "ns1.example.com.",
|
|
Mbox: "admin.example.com.",
|
|
}
|
|
got := rrDataString(soa)
|
|
if got == "" {
|
|
t.Error("expected non-empty string for SOA default case")
|
|
}
|
|
}
|
|
|
|
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 TestContainsString(t *testing.T) {
|
|
items := []string{"a", "b", "c"}
|
|
if !containsString(items, "a") {
|
|
t.Error("containsString should find 'a'")
|
|
}
|
|
if !containsString(items, "c") {
|
|
t.Error("containsString should find 'c'")
|
|
}
|
|
if containsString(items, "d") {
|
|
t.Error("containsString should not find 'd'")
|
|
}
|
|
if containsString(nil, "a") {
|
|
t.Error("containsString on nil should return false")
|
|
}
|
|
}
|
|
|
|
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 TestCollectUniqueServerIPs(t *testing.T) {
|
|
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
|
results := []traverse.TraversalResult{
|
|
{Referral: ref, Response: &traverse.Response{Server: net.ParseIP("1.2.3.4"), Referral: ref, Type: traverse.RespAnswer}},
|
|
{Referral: ref, Response: &traverse.Response{Server: net.ParseIP("1.2.3.4"), Referral: ref, Type: traverse.RespAnswer}}, // duplicate
|
|
{Referral: ref, Response: &traverse.Response{Server: net.ParseIP("5.6.7.8"), Referral: ref, Type: traverse.RespAnswer}},
|
|
{Referral: ref, Response: nil},
|
|
}
|
|
ips := collectUniqueServerIPs(results)
|
|
if len(ips) != 2 {
|
|
t.Errorf("expected 2 unique IPs, got %d", len(ips))
|
|
}
|
|
}
|
|
|
|
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 TestNewFormatterNilWriter(t *testing.T) {
|
|
f := NewFormatter(DefaultConfig(), nil)
|
|
if f == nil {
|
|
t.Fatal("NewFormatter with nil writer 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 }
|