CI / test (pull_request) Failing after 2m36s
- 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>
379 lines
11 KiB
Go
379 lines
11 KiB
Go
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) {
|
|
root := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
|
child := traverse.NewReferral("example.com.", dns.TypeA, "com.", 1, 0.5, root)
|
|
|
|
var buf bytes.Buffer
|
|
cfg := DefaultConfig()
|
|
cfg.Color = false
|
|
formatter := newTextFormatter(cfg, &buf)
|
|
|
|
if err := formatter.WriteProgress(traverse.TraversalEvent{
|
|
Stage: traverse.EventStart,
|
|
Result: traverse.TraversalResult{Referral: child},
|
|
}); err != nil {
|
|
t.Fatalf("WriteProgress: %v", err)
|
|
}
|
|
|
|
out := buf.String()
|
|
if !strings.HasPrefix(out, " 2 ") {
|
|
t.Fatalf("expected depth-based indentation, got %q", out)
|
|
}
|
|
}
|
|
|
|
func TestAttachHooksRespectsShowFlags(t *testing.T) {
|
|
ref := traverse.NewReferral("example.com.", dns.TypeA, ".", 0, 1.0, nil)
|
|
var progressCount int
|
|
|
|
cfg := DefaultConfig()
|
|
cfg.ShowProgress = false
|
|
cfg.ShowResolves = false
|
|
cfg.ShowAllStats = false
|
|
|
|
var buf bytes.Buffer
|
|
formatter := NewFormatter(cfg, &buf)
|
|
hooks := AttachHooks(cfg, formatter)
|
|
hooks.OnEvent(traverse.TraversalEvent{
|
|
Stage: traverse.EventStart,
|
|
Result: traverse.TraversalResult{Referral: ref},
|
|
})
|
|
|
|
if buf.Len() != 0 {
|
|
t.Fatalf("expected no output when ShowProgress is false")
|
|
}
|
|
|
|
cfg.ShowProgress = true
|
|
hooks = AttachHooks(cfg, formatter)
|
|
hooks.OnEvent(traverse.TraversalEvent{
|
|
Stage: traverse.EventStart,
|
|
Result: traverse.TraversalResult{Referral: ref},
|
|
})
|
|
progressCount = strings.Count(buf.String(), "\n")
|
|
if progressCount == 0 {
|
|
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)
|
|
}
|
|
}
|