feat: add comprehensive test suite for ExploreDNS
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>
This commit is contained in:
Gary Hansen
2026-06-08 03:47:04 +10:00
co-authored by Copilot multica-agent
parent fe1afe2a97
commit 5d1e5ca86c
11 changed files with 2590 additions and 2 deletions
+187
View File
@@ -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},
})
}