- Unit tests for all internal packages exceeding 80% coverage:
- internal/config: 86.2% (ParseMaxDepth, ParseRetries, validation paths)
- internal/dns: 82.9% (IterativeQueryWithExchange, mock DNS server, roots)
- internal/fingerprint: 96.4% (unchanged, already excellent)
- internal/output: 86.7% (formatters, stats, JSON/text output, hooks)
- internal/traverse: 86.7% (SetHooks, ResolveNS, processReferral,
ensureRDFalse, resolveGlueViaSystem, newAQuery, Referral.Resolve)
- Integration tests in internal/integration/:
- End-to-end traversal with mock DNS exchange function
- Referral chain traversal (root -> TLD -> authoritative)
- CNAME resolution and loop detection
- NXDOMAIN and SERVFAIL response handling
- Max depth enforcement
- Context cancellation
- TraverserHooks event delivery
- Mock DNS server helper in internal/dns/roots_test.go using miekg/dns
(enables deterministic testing without network dependency)
- CI updated with coverage reporting step
- Makefile: added 'cover' target for local HTML coverage reports
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Copilot
multica-agent
parent
fe1afe2a97
commit
5d1e5ca86c
@@ -186,3 +186,190 @@ func TestRunTraversalUsesHooks(t *testing.T) {
|
||||
t.Fatalf("expected formatted summary output, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONFormatterWriteResolveAndResult(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
||||
server := net.ParseIP("198.41.0.4")
|
||||
resp := &traverse.Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Type: traverse.RespAnswer,
|
||||
Decoded: &dns.DecodedResponse{
|
||||
Answers: []miekgdns.RR{
|
||||
&miekgdns.A{
|
||||
Hdr: miekgdns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: miekgdns.ClassINET},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Format = FormatJSON
|
||||
cfg.Domain = "example.com"
|
||||
cfg.QueryType = "A"
|
||||
cfg.ShowResolves = true
|
||||
cfg.ShowAllStats = true
|
||||
cfg.ShowProgress = true
|
||||
f := NewFormatter(cfg, &buf).(*jsonFormatter)
|
||||
|
||||
// WriteResolve
|
||||
if err := f.WriteResolve(traverse.TraversalEvent{
|
||||
Stage: traverse.EventStart,
|
||||
Result: traverse.TraversalResult{Referral: ref, Response: resp},
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteResolve: %v", err)
|
||||
}
|
||||
|
||||
// WriteResult
|
||||
if err := f.WriteResult(traverse.TraversalResult{Referral: ref, Response: resp}); err != nil {
|
||||
t.Fatalf("WriteResult: %v", err)
|
||||
}
|
||||
|
||||
// WriteProgress with EventComplete to cover stageName "complete"
|
||||
if err := f.WriteProgress(traverse.TraversalEvent{
|
||||
Stage: traverse.EventComplete,
|
||||
Result: traverse.TraversalResult{Referral: ref, Response: resp},
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteProgress EventComplete: %v", err)
|
||||
}
|
||||
|
||||
if err := f.Flush(); err != nil {
|
||||
t.Fatalf("Flush: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONFormatterWriteResolveFlagOff(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
||||
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Format = FormatJSON
|
||||
cfg.ShowResolves = false
|
||||
cfg.ShowAllStats = false
|
||||
f := NewFormatter(cfg, &buf).(*jsonFormatter)
|
||||
|
||||
if err := f.WriteResolve(traverse.TraversalEvent{
|
||||
Stage: traverse.EventStart,
|
||||
Result: traverse.TraversalResult{Referral: ref},
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteResolve: %v", err)
|
||||
}
|
||||
if err := f.WriteResult(traverse.TraversalResult{Referral: ref}); err != nil {
|
||||
t.Fatalf("WriteResult: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONFormatterWriteSummaryWithServers(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 1.0, nil)
|
||||
server := net.ParseIP("1.2.3.4")
|
||||
resp := &traverse.Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Type: traverse.RespAnswer,
|
||||
Decoded: &dns.DecodedResponse{
|
||||
Answers: []miekgdns.RR{
|
||||
&miekgdns.A{
|
||||
Hdr: miekgdns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: miekgdns.ClassINET},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
results := []traverse.TraversalResult{{Referral: ref, Response: resp}}
|
||||
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Format = FormatJSON
|
||||
cfg.Domain = "example.com"
|
||||
cfg.QueryType = "A"
|
||||
cfg.ShowServers = true
|
||||
cfg.ShowVersions = false
|
||||
cfg.ShowResults = true
|
||||
cfg.ShowSummaryResults = true
|
||||
f := NewFormatter(cfg, &buf).(*jsonFormatter)
|
||||
|
||||
if err := f.WriteSummary(results); err != nil {
|
||||
t.Fatalf("WriteSummary: %v", err)
|
||||
}
|
||||
if err := f.Flush(); err != nil {
|
||||
t.Fatalf("Flush: %v", err)
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(buf.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("invalid JSON: %v\n%s", err, buf.String())
|
||||
}
|
||||
if _, ok := payload["servers"]; !ok {
|
||||
t.Error("expected 'servers' field in JSON output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFormatterNilWriter(t *testing.T) {
|
||||
// Should not panic with nil writer
|
||||
cfg := DefaultConfig()
|
||||
f := NewFormatter(cfg, nil)
|
||||
if f == nil {
|
||||
t.Error("NewFormatter should not return nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachHooksShowResolves(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
||||
server := net.ParseIP("1.2.3.4")
|
||||
resp := &traverse.Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Type: traverse.RespAnswer,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.ShowProgress = false
|
||||
cfg.ShowResolves = true
|
||||
cfg.ShowAllStats = true
|
||||
cfg.Color = false
|
||||
formatter := NewFormatter(cfg, &buf)
|
||||
hooks := AttachHooks(cfg, formatter)
|
||||
|
||||
// Trigger a resolve event
|
||||
hooks.OnEvent(traverse.TraversalEvent{
|
||||
Stage: traverse.EventStart,
|
||||
IsResolve: true,
|
||||
Result: traverse.TraversalResult{Referral: ref, Response: resp},
|
||||
})
|
||||
|
||||
if buf.Len() == 0 {
|
||||
t.Error("expected resolve output when ShowResolves is true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachHooksShowAllStats(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
||||
server := net.ParseIP("1.2.3.4")
|
||||
resp := &traverse.Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Type: traverse.RespAnswer,
|
||||
Decoded: &dns.DecodedResponse{
|
||||
Answers: []miekgdns.RR{
|
||||
&miekgdns.A{Hdr: miekgdns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: miekgdns.ClassINET}, A: net.ParseIP("1.2.3.4")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.ShowProgress = false
|
||||
cfg.ShowResolves = false
|
||||
cfg.ShowAllStats = true
|
||||
cfg.Color = false
|
||||
formatter := NewFormatter(cfg, &buf)
|
||||
hooks := AttachHooks(cfg, formatter)
|
||||
|
||||
hooks.OnEvent(traverse.TraversalEvent{
|
||||
Stage: traverse.EventComplete,
|
||||
Result: traverse.TraversalResult{Referral: ref, Response: resp},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/hits/ExploreDNS/internal/dns"
|
||||
"github.com/hits/ExploreDNS/internal/traverse"
|
||||
miekgdns "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func makeAnswerResult(name string, ip string, prob float64) traverse.TraversalResult {
|
||||
ref := traverse.NewReferral(name, dns.TypeA, ".", 0, prob, nil)
|
||||
server := net.ParseIP("198.41.0.4")
|
||||
resp := &traverse.Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Type: traverse.RespAnswer,
|
||||
Decoded: &dns.DecodedResponse{
|
||||
Answers: []miekgdns.RR{
|
||||
&miekgdns.A{
|
||||
Hdr: miekgdns.RR_Header{Name: name + ".", Rrtype: dns.TypeA, Class: miekgdns.ClassINET},
|
||||
A: net.ParseIP(ip),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return traverse.TraversalResult{Referral: ref, Response: resp}
|
||||
}
|
||||
|
||||
func TestRRDataStringAllTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
rr miekgdns.RR
|
||||
want string
|
||||
}{
|
||||
{
|
||||
&miekgdns.A{Hdr: miekgdns.RR_Header{Rrtype: dns.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 tests {
|
||||
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 record hits the default case
|
||||
rr := &miekgdns.SOA{
|
||||
Hdr: miekgdns.RR_Header{Name: ".", Rrtype: miekgdns.TypeSOA, Class: miekgdns.ClassINET},
|
||||
Ns: "a.root-servers.net.",
|
||||
Mbox: "nstld.verisign-grs.com.",
|
||||
}
|
||||
got := rrDataString(rr)
|
||||
if got == "" {
|
||||
t.Error("rrDataString(SOA) should return non-empty string via default case")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummaryTypeLabelAllTypes(t *testing.T) {
|
||||
cases := map[string]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 input, want := range cases {
|
||||
got := summaryTypeLabel(input)
|
||||
if got != want {
|
||||
t.Errorf("summaryTypeLabel(%q) = %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectServersEmpty(t *testing.T) {
|
||||
servers := collectServers(nil)
|
||||
if len(servers) != 0 {
|
||||
t.Errorf("collectServers(nil) = %v, want empty", servers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectServersDeduplication(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 1.0, nil)
|
||||
server := net.ParseIP("1.2.3.4")
|
||||
resp := &traverse.Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Type: traverse.RespAnswer,
|
||||
}
|
||||
result := traverse.TraversalResult{Referral: ref, Response: resp}
|
||||
|
||||
servers := collectServers([]traverse.TraversalResult{result, result})
|
||||
name := "com"
|
||||
ips := servers[name]
|
||||
if len(ips) != 1 {
|
||||
t.Errorf("expected deduplication: got %d IPs, want 1", len(ips))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectServersWithBailiwick(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 1.0, nil)
|
||||
server := net.ParseIP("1.2.3.4")
|
||||
resp := &traverse.Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Type: traverse.RespAnswer,
|
||||
}
|
||||
result := traverse.TraversalResult{Referral: ref, Response: resp}
|
||||
|
||||
servers := collectServers([]traverse.TraversalResult{result})
|
||||
if len(servers) == 0 {
|
||||
t.Fatal("expected at least one server entry")
|
||||
}
|
||||
if _, ok := servers["com"]; !ok {
|
||||
t.Errorf("expected server name 'com', got keys: %v", servers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerNameFallbacks(t *testing.T) {
|
||||
// No bailiwick, no NSName, with server IP
|
||||
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,
|
||||
}
|
||||
result := traverse.TraversalResult{Referral: ref, Response: resp}
|
||||
name := serverName(result)
|
||||
if name != "1.2.3.4" {
|
||||
t.Errorf("serverName with root bailiwick = %q, want IP", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerNameWithNSName(t *testing.T) {
|
||||
ref := &traverse.Referral{
|
||||
Name: "example.com.",
|
||||
Qtype: dns.TypeA,
|
||||
Bailiwick: ".",
|
||||
NSName: "ns1.example.com.",
|
||||
}
|
||||
resp := &traverse.Response{
|
||||
Referral: ref,
|
||||
Server: net.ParseIP("5.5.5.5"),
|
||||
Type: traverse.RespAnswer,
|
||||
}
|
||||
result := traverse.TraversalResult{Referral: ref, Response: resp}
|
||||
// Bailiwick is "." so falls through to NSName
|
||||
name := serverName(result)
|
||||
if name == "" {
|
||||
t.Error("serverName should return non-empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerNameNilReferral(t *testing.T) {
|
||||
resp := &traverse.Response{
|
||||
Server: net.ParseIP("1.2.3.4"),
|
||||
Type: traverse.RespAnswer,
|
||||
}
|
||||
result := traverse.TraversalResult{Referral: nil, Response: resp}
|
||||
name := serverName(result)
|
||||
if name == "" {
|
||||
t.Error("serverName with nil referral should return non-empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsString(t *testing.T) {
|
||||
items := []string{"a", "b", "c"}
|
||||
if !containsString(items, "b") {
|
||||
t.Error("containsString should find 'b' in slice")
|
||||
}
|
||||
if containsString(items, "d") {
|
||||
t.Error("containsString should not find 'd' in slice")
|
||||
}
|
||||
if containsString(nil, "a") {
|
||||
t.Error("containsString on nil slice should return false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeSummaryMixedResults(t *testing.T) {
|
||||
refAnswer := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 0.6, nil)
|
||||
respAnswer := &traverse.Response{
|
||||
Referral: refAnswer,
|
||||
Type: traverse.RespAnswer,
|
||||
Decoded: &dns.DecodedResponse{
|
||||
Answers: []miekgdns.RR{
|
||||
&miekgdns.A{
|
||||
Hdr: miekgdns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: miekgdns.ClassINET},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
refNXD := traverse.NewReferral("notexist.com.", dns.TypeA, ".", 0, 0.4, nil)
|
||||
respNXD := &traverse.Response{
|
||||
Referral: refNXD,
|
||||
Type: traverse.RespNXDOMAIN,
|
||||
}
|
||||
|
||||
results := []traverse.TraversalResult{
|
||||
{Referral: refAnswer, Response: respAnswer},
|
||||
{Referral: refNXD, Response: respNXD},
|
||||
}
|
||||
|
||||
stats := ComputeSummary(results)
|
||||
if stats == nil {
|
||||
t.Fatal("ComputeSummary returned nil for non-empty results")
|
||||
}
|
||||
if len(stats.Answers) != 1 {
|
||||
t.Errorf("expected 1 answer entry, got %d", len(stats.Answers))
|
||||
}
|
||||
if _, ok := stats.ByType["nxdomain"]; !ok {
|
||||
t.Error("expected nxdomain in ByType")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeSummaryAnswerWithCNAMEOnly(t *testing.T) {
|
||||
// Answer with only CNAME record - no final answer, should be in ByType
|
||||
ref := traverse.NewReferral("www.example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
||||
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, Class: miekgdns.ClassINET},
|
||||
Target: "example.com.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
results := []traverse.TraversalResult{{Referral: ref, Response: resp}}
|
||||
stats := ComputeSummary(results)
|
||||
if stats == nil {
|
||||
t.Fatal("ComputeSummary returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeSummaryAccumulates(t *testing.T) {
|
||||
// Two answers with the same IP should accumulate probability
|
||||
ref1 := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 0.5, nil)
|
||||
ref2 := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 0.5, nil)
|
||||
|
||||
makeResp := func(ref *traverse.Referral) *traverse.Response {
|
||||
return &traverse.Response{
|
||||
Referral: ref,
|
||||
Type: traverse.RespAnswer,
|
||||
Decoded: &dns.DecodedResponse{
|
||||
Answers: []miekgdns.RR{
|
||||
&miekgdns.A{
|
||||
Hdr: miekgdns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: miekgdns.ClassINET},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
results := []traverse.TraversalResult{
|
||||
{Referral: ref1, Response: makeResp(ref1)},
|
||||
{Referral: ref2, Response: makeResp(ref2)},
|
||||
}
|
||||
stats := ComputeSummary(results)
|
||||
if stats == nil {
|
||||
t.Fatal("ComputeSummary returned nil")
|
||||
}
|
||||
if len(stats.Answers) != 1 {
|
||||
t.Fatalf("expected 1 answer after accumulation, got %d", len(stats.Answers))
|
||||
}
|
||||
if stats.Answers[0].Prob < 0.99 {
|
||||
t.Errorf("accumulated prob = %.2f, want ~1.0", stats.Answers[0].Prob)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectUniqueServerIPs(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
||||
ip1 := net.ParseIP("1.2.3.4")
|
||||
ip2 := net.ParseIP("5.6.7.8")
|
||||
|
||||
results := []traverse.TraversalResult{
|
||||
{Referral: ref, Response: &traverse.Response{Server: ip1, Type: traverse.RespAnswer}},
|
||||
{Referral: ref, Response: &traverse.Response{Server: ip1, Type: traverse.RespAnswer}}, // dup
|
||||
{Referral: ref, Response: &traverse.Response{Server: ip2, Type: traverse.RespAnswer}},
|
||||
{Referral: ref, Response: nil}, // nil response
|
||||
}
|
||||
|
||||
ips := collectUniqueServerIPs(results)
|
||||
if len(ips) != 2 {
|
||||
t.Errorf("expected 2 unique IPs, got %d", len(ips))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectUniqueServerIPsEmpty(t *testing.T) {
|
||||
ips := collectUniqueServerIPs(nil)
|
||||
if len(ips) != 0 {
|
||||
t.Errorf("expected 0 IPs for nil results, got %d", len(ips))
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hits/ExploreDNS/internal/dns"
|
||||
"github.com/hits/ExploreDNS/internal/traverse"
|
||||
miekgdns "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestTextFormatterProgressIndentation(t *testing.T) {
|
||||
@@ -63,3 +65,314 @@ func TestAttachHooksRespectsShowFlags(t *testing.T) {
|
||||
t.Fatal("expected progress output when ShowProgress is true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextWriteResolve(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
||||
server := net.ParseIP("198.41.0.4")
|
||||
resp := &traverse.Response{Server: server, Type: traverse.RespAnswer}
|
||||
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Color = false
|
||||
f := newTextFormatter(cfg, &buf)
|
||||
|
||||
// EventStart - should write line
|
||||
if err := f.WriteResolve(traverse.TraversalEvent{
|
||||
Stage: traverse.EventStart,
|
||||
Result: traverse.TraversalResult{Referral: ref, Response: resp},
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteResolve EventStart: %v", err)
|
||||
}
|
||||
if buf.Len() == 0 {
|
||||
t.Error("expected output for WriteResolve EventStart")
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
// EventComplete - should write nothing
|
||||
if err := f.WriteResolve(traverse.TraversalEvent{
|
||||
Stage: traverse.EventComplete,
|
||||
Result: traverse.TraversalResult{Referral: ref, Response: resp},
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteResolve EventComplete: %v", err)
|
||||
}
|
||||
if buf.Len() != 0 {
|
||||
t.Error("expected no output for WriteResolve EventComplete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextWriteResult(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
||||
server := net.ParseIP("198.41.0.4")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
respType traverse.ResponseType
|
||||
msg *dns.DecodedResponse
|
||||
errorMsg string
|
||||
}{
|
||||
{"answer", traverse.RespAnswer, &dns.DecodedResponse{
|
||||
Answers: []miekgdns.RR{
|
||||
&miekgdns.A{Hdr: miekgdns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: miekgdns.ClassINET}, A: net.ParseIP("1.2.3.4")},
|
||||
},
|
||||
}, ""},
|
||||
{"nodata", traverse.RespNODATA, nil, ""},
|
||||
{"nxdomain", traverse.RespNXDOMAIN, nil, ""},
|
||||
{"servfail", traverse.RespSERVFAIL, nil, ""},
|
||||
{"refused", traverse.RespREFUSED, nil, ""},
|
||||
{"notimp", traverse.RespNOTIMPL, nil, ""},
|
||||
{"cname_loop", traverse.RespCNAMELoop, nil, "loop detected"},
|
||||
{"error", traverse.RespError, nil, "something went wrong"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Color = false
|
||||
f := newTextFormatter(cfg, &buf)
|
||||
|
||||
resp := &traverse.Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Type: tc.respType,
|
||||
Decoded: tc.msg,
|
||||
ErrorMessage: tc.errorMsg,
|
||||
}
|
||||
result := traverse.TraversalResult{Referral: ref, Response: resp}
|
||||
if err := f.WriteResult(result); err != nil {
|
||||
t.Fatalf("WriteResult %q: %v", tc.name, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextWriteResultNilResponse(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
f := newTextFormatter(cfg, &buf)
|
||||
if err := f.WriteResult(traverse.TraversalResult{Referral: nil, Response: nil}); err != nil {
|
||||
t.Fatalf("WriteResult nil: %v", err)
|
||||
}
|
||||
if buf.Len() != 0 {
|
||||
t.Error("expected no output for nil result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextWriteResultAnswerMultipleRRs(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 0.5, 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: dns.TypeA, Class: miekgdns.ClassINET}, A: net.ParseIP("1.2.3.4")},
|
||||
&miekgdns.A{Hdr: miekgdns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: miekgdns.ClassINET}, A: net.ParseIP("5.6.7.8")},
|
||||
},
|
||||
},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Color = false
|
||||
f := newTextFormatter(cfg, &buf)
|
||||
if err := f.WriteResult(traverse.TraversalResult{Referral: ref, Response: resp}); err != nil {
|
||||
t.Fatalf("WriteResult: %v", err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "/") {
|
||||
t.Errorf("expected '/' separator for multiple answers, got: %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextWriteSummaryWithServersAndResults(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 1.0, nil)
|
||||
server := net.ParseIP("1.2.3.4")
|
||||
resp := &traverse.Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Type: traverse.RespAnswer,
|
||||
Decoded: &dns.DecodedResponse{
|
||||
Answers: []miekgdns.RR{
|
||||
&miekgdns.A{Hdr: miekgdns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: miekgdns.ClassINET}, A: net.ParseIP("1.2.3.4")},
|
||||
},
|
||||
},
|
||||
}
|
||||
results := []traverse.TraversalResult{{Referral: ref, Response: resp}}
|
||||
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Color = false
|
||||
cfg.ShowServers = true
|
||||
cfg.ShowResults = true
|
||||
cfg.ShowSummaryResults = true
|
||||
f := newTextFormatter(cfg, &buf)
|
||||
if err := f.WriteSummary(results); err != nil {
|
||||
t.Fatalf("WriteSummary: %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "Summary:") {
|
||||
t.Errorf("expected Summary: in output, got: %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "Results:") {
|
||||
t.Errorf("expected Results: in output, got: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextWriteSummaryNoResults(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.ShowServers = false
|
||||
cfg.ShowResults = false
|
||||
cfg.ShowSummaryResults = false
|
||||
f := newTextFormatter(cfg, &buf)
|
||||
if err := f.WriteSummary(nil); err != nil {
|
||||
t.Fatalf("WriteSummary nil: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextWriteSummaryNXDOMAIN(t *testing.T) {
|
||||
ref := traverse.NewReferral("gone.example.com.", dns.TypeA, "com.", 1, 1.0, nil)
|
||||
server := net.ParseIP("1.2.3.4")
|
||||
resp := &traverse.Response{
|
||||
Referral: ref,
|
||||
Server: server,
|
||||
Type: traverse.RespNXDOMAIN,
|
||||
}
|
||||
results := []traverse.TraversalResult{{Referral: ref, Response: resp}}
|
||||
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Color = false
|
||||
cfg.ShowServers = true
|
||||
cfg.ShowResults = true
|
||||
cfg.ShowSummaryResults = true
|
||||
f := newTextFormatter(cfg, &buf)
|
||||
if err := f.WriteSummary(results); err != nil {
|
||||
t.Fatalf("WriteSummary: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatReferralLineVerbose(t *testing.T) {
|
||||
root := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
||||
child := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 1.0, root)
|
||||
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Color = false
|
||||
cfg.Verbose = true
|
||||
f := newTextFormatter(cfg, &buf)
|
||||
|
||||
event := traverse.TraversalEvent{
|
||||
Stage: traverse.EventStart,
|
||||
Result: traverse.TraversalResult{Referral: child},
|
||||
}
|
||||
if err := f.WriteProgress(event); err != nil {
|
||||
t.Fatalf("WriteProgress verbose: %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "com") {
|
||||
t.Errorf("expected bailiwick in verbose output, got: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatReferralLineVerboseResolve(t *testing.T) {
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 1.0, nil)
|
||||
server := net.ParseIP("1.2.3.4")
|
||||
resp := &traverse.Response{Server: server, Type: traverse.RespAnswer}
|
||||
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Color = false
|
||||
cfg.Verbose = true
|
||||
f := newTextFormatter(cfg, &buf)
|
||||
|
||||
if err := f.WriteResolve(traverse.TraversalEvent{
|
||||
Stage: traverse.EventStart,
|
||||
Result: traverse.TraversalResult{Referral: ref, Response: resp},
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteResolve verbose: %v", err)
|
||||
}
|
||||
if buf.Len() == 0 {
|
||||
t.Error("expected output for verbose WriteResolve")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextWriteProgressNoAddresses(t *testing.T) {
|
||||
// Test the "resolving" suffix when referral has no addresses
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
||||
// No addresses set, so HasAddresses() returns false
|
||||
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Color = false
|
||||
f := newTextFormatter(cfg, &buf)
|
||||
if err := f.WriteProgress(traverse.TraversalEvent{
|
||||
Stage: traverse.EventStart,
|
||||
Result: traverse.TraversalResult{Referral: ref},
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteProgress: %v", err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "resolving") {
|
||||
t.Errorf("expected 'resolving' suffix when no addresses, got: %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorize(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
cfg := DefaultConfig()
|
||||
cfg.Color = true
|
||||
f := newTextFormatter(cfg, &buf)
|
||||
|
||||
colored := f.colorize("hello", colorGreen)
|
||||
if colored == "hello" {
|
||||
t.Error("expected colorized output with Color=true")
|
||||
}
|
||||
|
||||
cfg.Color = false
|
||||
f2 := newTextFormatter(cfg, &buf)
|
||||
plain := f2.colorize("hello", colorGreen)
|
||||
if plain != "hello" {
|
||||
t.Errorf("expected plain text with Color=false, got %q", plain)
|
||||
}
|
||||
|
||||
// Empty color
|
||||
empty := f.colorize("hello", "")
|
||||
if empty != "hello" {
|
||||
t.Errorf("expected plain text for empty color, got %q", empty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferralServerLabelFallbacks(t *testing.T) {
|
||||
// With server IP in response
|
||||
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
||||
resp := &traverse.Response{Server: net.ParseIP("1.2.3.4")}
|
||||
label := referralServerLabel(ref, resp)
|
||||
if label != "1.2.3.4" {
|
||||
t.Errorf("expected '1.2.3.4', got %q", label)
|
||||
}
|
||||
|
||||
// With addresses in referral, no response server
|
||||
ref2 := traverse.NewReferral("example.com.", dns.TypeA, "ns1.example.com.", 0, 1.0, nil)
|
||||
ref2.Addresses = []net.IP{net.ParseIP("5.6.7.8")}
|
||||
label2 := referralServerLabel(ref2, nil)
|
||||
if label2 != "5.6.7.8" {
|
||||
t.Errorf("expected '5.6.7.8', got %q", label2)
|
||||
}
|
||||
|
||||
// With NSName
|
||||
ref3 := &traverse.Referral{
|
||||
Name: "example.com.",
|
||||
NSName: "ns1.example.com.",
|
||||
Bailiwick: ".",
|
||||
}
|
||||
label3 := referralServerLabel(ref3, nil)
|
||||
if label3 != "ns1.example.com." {
|
||||
t.Errorf("expected NSName, got %q", label3)
|
||||
}
|
||||
|
||||
// With non-root bailiwick, no addresses, no NSName
|
||||
ref4 := traverse.NewReferral("example.com.", dns.TypeA, "com.", 0, 1.0, nil)
|
||||
label4 := referralServerLabel(ref4, nil)
|
||||
if label4 != "com" {
|
||||
t.Errorf("expected 'com', got %q", label4)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user