test: merge PR #13 test coverage improvements into main
Resolves merge conflicts between Phase 4.2 comprehensive test suite and the test coverage improvement branch: - config_test.go: take PR's better table-driven tests + keep main's extra tests - coverage_test.go: keep main's Phase 4.2 comprehensive tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -216,24 +216,32 @@ func TestParseDebugLevel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseMaxDepthValid(t *testing.T) {
|
func TestParseMaxDepthValid(t *testing.T) {
|
||||||
cases := []string{"1", "20", "100"}
|
cases := []struct {
|
||||||
for _, s := range cases {
|
input string
|
||||||
v, err := ParseMaxDepth(s)
|
want int
|
||||||
if err != nil {
|
}{
|
||||||
t.Errorf("ParseMaxDepth(%q) unexpected error: %v", s, err)
|
{"1", 1},
|
||||||
|
{"20", 20},
|
||||||
|
{"100", 100},
|
||||||
}
|
}
|
||||||
if v < 1 || v > 100 {
|
for _, tc := range cases {
|
||||||
t.Errorf("ParseMaxDepth(%q) = %d, out of range", s, v)
|
got, err := ParseMaxDepth(tc.input)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ParseMaxDepth(%q) unexpected error: %v", tc.input, err)
|
||||||
|
}
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("ParseMaxDepth(%q) = %d, want %d", tc.input, got, tc.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseMaxDepthInvalid(t *testing.T) {
|
func TestParseMaxDepthInvalid(t *testing.T) {
|
||||||
cases := []string{"0", "101", "notanumber"}
|
cases := []string{"0", "101", "notanumber", "-1"}
|
||||||
for _, s := range cases {
|
for _, s := range cases {
|
||||||
_, err := ParseMaxDepth(s)
|
_, err := ParseMaxDepth(s)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("ParseMaxDepth(%q): expected error", s)
|
t.Errorf("ParseMaxDepth(%q): expected error", s)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if !errors.Is(err, ErrInvalidMaxDepth) {
|
if !errors.Is(err, ErrInvalidMaxDepth) {
|
||||||
t.Errorf("ParseMaxDepth(%q): expected ErrInvalidMaxDepth, got %v", s, err)
|
t.Errorf("ParseMaxDepth(%q): expected ErrInvalidMaxDepth, got %v", s, err)
|
||||||
@@ -242,14 +250,21 @@ t.Errorf("ParseMaxDepth(%q): expected ErrInvalidMaxDepth, got %v", s, err)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseRetriesValid(t *testing.T) {
|
func TestParseRetriesValid(t *testing.T) {
|
||||||
cases := []string{"0", "5", "10"}
|
cases := []struct {
|
||||||
for _, s := range cases {
|
input string
|
||||||
v, err := ParseRetries(s)
|
want int
|
||||||
if err != nil {
|
}{
|
||||||
t.Errorf("ParseRetries(%q) unexpected error: %v", s, err)
|
{"0", 0},
|
||||||
|
{"2", 2},
|
||||||
|
{"10", 10},
|
||||||
}
|
}
|
||||||
if v < 0 || v > 10 {
|
for _, tc := range cases {
|
||||||
t.Errorf("ParseRetries(%q) = %d, out of range", s, v)
|
got, err := ParseRetries(tc.input)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ParseRetries(%q) unexpected error: %v", tc.input, err)
|
||||||
|
}
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("ParseRetries(%q) = %d, want %d", tc.input, got, tc.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,6 +275,7 @@ for _, s := range cases {
|
|||||||
_, err := ParseRetries(s)
|
_, err := ParseRetries(s)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("ParseRetries(%q): expected error", s)
|
t.Errorf("ParseRetries(%q): expected error", s)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if !errors.Is(err, ErrInvalidRetries) {
|
if !errors.Is(err, ErrInvalidRetries) {
|
||||||
t.Errorf("ParseRetries(%q): expected ErrInvalidRetries, got %v", s, err)
|
t.Errorf("ParseRetries(%q): expected ErrInvalidRetries, got %v", s, err)
|
||||||
@@ -267,6 +283,11 @@ t.Errorf("ParseRetries(%q): expected ErrInvalidRetries, got %v", s, err)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrintUsage(t *testing.T) {
|
||||||
|
// PrintUsage writes to stderr; just ensure it doesn't panic.
|
||||||
|
PrintUsage()
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateBadQueryType(t *testing.T) {
|
func TestValidateBadQueryType(t *testing.T) {
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
cfg.QueryType = "BOGUS"
|
cfg.QueryType = "BOGUS"
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIterativeQueryWithExchangeSuccess(t *testing.T) {
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetReply(new(dns.Msg))
|
||||||
|
resp.RecursionDesired = false
|
||||||
|
resp.Answer = append(resp.Answer, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||||
|
A: net.ParseIP("93.184.216.34"),
|
||||||
|
})
|
||||||
|
|
||||||
|
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||||
|
if msg.RecursionDesired {
|
||||||
|
t.Error("iterative query should have RecursionDesired=false")
|
||||||
|
}
|
||||||
|
return resp.Copy(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &QueryConfig{
|
||||||
|
UDPSize: 2048,
|
||||||
|
Retries: 1,
|
||||||
|
UseTCP: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
server := net.ParseIP("8.8.8.8")
|
||||||
|
result, err := IterativeQueryWithExchange(context.Background(), server, "example.com", TypeA, cfg, exchangeFn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(result.Answer) != 1 {
|
||||||
|
t.Fatalf("expected 1 answer, got %d", len(result.Answer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterativeQueryWithExchangeNilConfig(t *testing.T) {
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetReply(new(dns.Msg))
|
||||||
|
|
||||||
|
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||||
|
return resp.Copy(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
server := net.ParseIP("8.8.8.8")
|
||||||
|
_, err := IterativeQueryWithExchange(context.Background(), server, "example.com", TypeA, nil, exchangeFn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error with nil config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterativeQueryWithExchangeNilResponse(t *testing.T) {
|
||||||
|
var mu sync.Mutex
|
||||||
|
callCount := 0
|
||||||
|
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||||
|
mu.Lock()
|
||||||
|
callCount++
|
||||||
|
mu.Unlock()
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &QueryConfig{
|
||||||
|
UDPSize: 2048,
|
||||||
|
Retries: 2,
|
||||||
|
UseTCP: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
server := net.ParseIP("8.8.8.8")
|
||||||
|
_, err := IterativeQueryWithExchange(context.Background(), server, "example.com", TypeA, cfg, exchangeFn)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when response is nil")
|
||||||
|
}
|
||||||
|
if callCount != 2 {
|
||||||
|
t.Errorf("expected 2 attempts, got %d", callCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterativeQueryWithExchangeTCPFallback(t *testing.T) {
|
||||||
|
truncated := new(dns.Msg)
|
||||||
|
truncated.Truncated = true
|
||||||
|
truncated.SetReply(new(dns.Msg))
|
||||||
|
|
||||||
|
full := new(dns.Msg)
|
||||||
|
full.SetReply(new(dns.Msg))
|
||||||
|
full.Answer = append(full.Answer, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA},
|
||||||
|
A: net.ParseIP("1.2.3.4"),
|
||||||
|
})
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
calls := []bool{}
|
||||||
|
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||||
|
mu.Lock()
|
||||||
|
calls = append(calls, useTCP)
|
||||||
|
mu.Unlock()
|
||||||
|
if !useTCP {
|
||||||
|
return truncated.Copy(), nil
|
||||||
|
}
|
||||||
|
return full.Copy(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &QueryConfig{
|
||||||
|
UDPSize: 2048,
|
||||||
|
Retries: 1,
|
||||||
|
UseTCP: false,
|
||||||
|
AllowTCP: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
server := net.ParseIP("8.8.8.8")
|
||||||
|
result, err := IterativeQueryWithExchange(context.Background(), server, "example.com", TypeA, cfg, exchangeFn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(calls) != 2 {
|
||||||
|
t.Errorf("expected 2 calls (UDP + TCP), got %d", len(calls))
|
||||||
|
}
|
||||||
|
if len(result.Answer) != 1 {
|
||||||
|
t.Fatalf("expected 1 answer, got %d", len(result.Answer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterativeQueryWithExchangeAlwaysTCP(t *testing.T) {
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetReply(new(dns.Msg))
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
calls := []bool{}
|
||||||
|
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||||
|
mu.Lock()
|
||||||
|
calls = append(calls, useTCP)
|
||||||
|
mu.Unlock()
|
||||||
|
return resp.Copy(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &QueryConfig{
|
||||||
|
UDPSize: 2048,
|
||||||
|
Retries: 1,
|
||||||
|
UseTCP: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
server := net.ParseIP("8.8.8.8")
|
||||||
|
_, err := IterativeQueryWithExchange(context.Background(), server, "example.com", TypeA, cfg, exchangeFn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(calls) != 1 || !calls[0] {
|
||||||
|
t.Errorf("expected single TCP call, got %v", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterativeQueryWithExchangeRetriesOnFailure(t *testing.T) {
|
||||||
|
var mu sync.Mutex
|
||||||
|
callCount := 0
|
||||||
|
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||||
|
mu.Lock()
|
||||||
|
callCount++
|
||||||
|
mu.Unlock()
|
||||||
|
return nil, errors.New("connection refused")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &QueryConfig{
|
||||||
|
UDPSize: 2048,
|
||||||
|
Retries: 3,
|
||||||
|
UseTCP: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
server := net.ParseIP("8.8.8.8")
|
||||||
|
_, err := IterativeQueryWithExchange(context.Background(), server, "example.com", TypeA, cfg, exchangeFn)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
if callCount != 3 {
|
||||||
|
t.Errorf("expected 3 calls, got %d", callCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterativeQueryWithExchangeContextCancel(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &QueryConfig{
|
||||||
|
UDPSize: 2048,
|
||||||
|
Retries: 1,
|
||||||
|
UseTCP: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
server := net.ParseIP("8.8.8.8")
|
||||||
|
_, err := IterativeQueryWithExchange(ctx, server, "example.com", TypeA, cfg, exchangeFn)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error on cancelled context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractNSNames(t *testing.T) {
|
||||||
|
rrs := []dns.RR{
|
||||||
|
&dns.NS{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeNS}, Ns: "a.root-servers.net."},
|
||||||
|
&dns.NS{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeNS}, Ns: "b.root-servers.net."},
|
||||||
|
}
|
||||||
|
names := extractNSNames(rrs)
|
||||||
|
if len(names) != 2 {
|
||||||
|
t.Fatalf("expected 2 names, got %d", len(names))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractNSNamesEmpty(t *testing.T) {
|
||||||
|
names := extractNSNames(nil)
|
||||||
|
if len(names) != 0 {
|
||||||
|
t.Errorf("expected 0 names, got %d", len(names))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterativeQueryWithExchangeZeroUDPSize(t *testing.T) {
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetReply(new(dns.Msg))
|
||||||
|
|
||||||
|
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||||
|
return resp.Copy(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &QueryConfig{
|
||||||
|
UDPSize: 0, // should use default
|
||||||
|
Retries: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
server := net.ParseIP("8.8.8.8")
|
||||||
|
_, err := IterativeQueryWithExchange(context.Background(), server, "example.com", TypeA, cfg, exchangeFn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// startTestDNSServer starts a local DNS server on a random port and returns the address and a stop function.
|
||||||
|
func startTestDNSServer(t *testing.T, handler dns.HandlerFunc) (string, func()) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("cannot start test DNS server: %v", err)
|
||||||
|
}
|
||||||
|
addr := pc.LocalAddr().String()
|
||||||
|
|
||||||
|
mux := dns.NewServeMux()
|
||||||
|
mux.HandleFunc(".", handler)
|
||||||
|
|
||||||
|
srv := &dns.Server{
|
||||||
|
PacketConn: pc,
|
||||||
|
Net: "udp",
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
started := make(chan struct{})
|
||||||
|
srv.NotifyStartedFunc = func() { close(started) }
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = srv.ActivateAndServe()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-started:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Skip("test DNS server did not start in time")
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr, func() { _ = srv.Shutdown() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryUsesRealExchange(t *testing.T) {
|
||||||
|
addr, stop := startTestDNSServer(t, func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetReply(r)
|
||||||
|
resp.Answer = append(resp.Answer, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||||
|
A: net.ParseIP("1.2.3.4"),
|
||||||
|
})
|
||||||
|
_ = w.WriteMsg(resp)
|
||||||
|
})
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
host, portStr, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse addr: %v", err)
|
||||||
|
}
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(portStr, "%d", &port)
|
||||||
|
|
||||||
|
// Patch the realExchange to use the test server by using QueryWithExchange with a custom exchangeFn.
|
||||||
|
// Since we can't inject into Query directly, use realExchangeWithPort for test.
|
||||||
|
serverIP := net.ParseIP(host)
|
||||||
|
cfg := DefaultQueryConfig()
|
||||||
|
cfg.Retries = 1
|
||||||
|
|
||||||
|
// Test QueryWithExchange (already covered), but now test Query+realExchange flow via
|
||||||
|
// a patched exchange that routes to our test server port.
|
||||||
|
patchedExchange := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||||
|
c := &dns.Client{Net: "udp", ReadTimeout: 3 * time.Second, WriteTimeout: 3 * time.Second}
|
||||||
|
r, _, err := c.ExchangeContext(ctx, msg, fmt.Sprintf("%s:%d", host, port))
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := QueryWithExchange(context.Background(), serverIP, "example.com", TypeA, cfg, patchedExchange)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("QueryWithExchange: %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.Answer) == 0 {
|
||||||
|
t.Fatal("expected at least 1 answer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRealExchangeViaDirect(t *testing.T) {
|
||||||
|
// Test realExchange directly via the exported Query function
|
||||||
|
// by using a server that will respond or fail quickly.
|
||||||
|
// We use a loopback address with a timeout to exercise code paths.
|
||||||
|
addr, stop := startTestDNSServer(t, func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetReply(r)
|
||||||
|
resp.Answer = append(resp.Answer, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||||
|
A: net.ParseIP("5.6.7.8"),
|
||||||
|
})
|
||||||
|
_ = w.WriteMsg(resp)
|
||||||
|
})
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
host, portStr, _ := net.SplitHostPort(addr)
|
||||||
|
serverIP := net.ParseIP(host)
|
||||||
|
|
||||||
|
// Exercise realExchange via Query — we need a way to target the test port.
|
||||||
|
// Use a custom exchange that calls through realExchange-like logic.
|
||||||
|
cfg := DefaultQueryConfig()
|
||||||
|
cfg.Retries = 1
|
||||||
|
|
||||||
|
resp, err := QueryWithExchange(context.Background(), serverIP, "example.com", TypeA, cfg,
|
||||||
|
func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||||
|
targetAddr := fmt.Sprintf("%s:%s", host, portStr)
|
||||||
|
c := &dns.Client{Net: "udp", ReadTimeout: 3 * time.Second, WriteTimeout: 3 * time.Second}
|
||||||
|
r, _, e := c.ExchangeContext(ctx, msg, targetAddr)
|
||||||
|
return r, e
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.Answer) == 0 {
|
||||||
|
t.Fatal("expected answers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryFunctionDirectly(t *testing.T) {
|
||||||
|
// Exercise Query() itself (which calls realExchange) by using 127.0.0.1:53.
|
||||||
|
// The test skips if no local DNS is available.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
server := net.ParseIP("127.0.0.1")
|
||||||
|
cfg := DefaultQueryConfig()
|
||||||
|
cfg.Retries = 1
|
||||||
|
cfg.Timeout = 2 * time.Second
|
||||||
|
|
||||||
|
_, err := Query(ctx, server, ".", TypeNS, cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("skipping (no local DNS at 127.0.0.1:53): %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterativeQueryDirectly(t *testing.T) {
|
||||||
|
// Exercise IterativeQuery() itself (which calls realExchange) by using 127.0.0.1:53.
|
||||||
|
// The test skips if no local DNS is available.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
server := net.ParseIP("127.0.0.1")
|
||||||
|
cfg := DefaultQueryConfig()
|
||||||
|
cfg.Retries = 1
|
||||||
|
cfg.Timeout = 2 * time.Second
|
||||||
|
|
||||||
|
_, err := IterativeQuery(ctx, server, ".", TypeNS, cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("skipping (no local DNS at 127.0.0.1:53): %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicResolverQuery(t *testing.T) {
|
||||||
|
// Exercise BasicResolver.Query() which calls Query() → realExchange.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
br := NewBasicResolver()
|
||||||
|
server := net.ParseIP("127.0.0.1")
|
||||||
|
cfg := DefaultQueryConfig()
|
||||||
|
cfg.Retries = 1
|
||||||
|
cfg.Timeout = 2 * time.Second
|
||||||
|
|
||||||
|
_, err := br.Query(ctx, server, ".", TypeNS, cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("skipping (no local DNS at 127.0.0.1:53): %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverAllRoots(t *testing.T) {
|
||||||
|
// discoverAllRoots calls queryResolver(ctx, "127.0.0.1:53", ...)
|
||||||
|
// Skip if local DNS is not available.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cfg := &RootDiscoveryConfig{
|
||||||
|
AllRoots: true,
|
||||||
|
IncludeAAAA: false,
|
||||||
|
}
|
||||||
|
servers, err := DiscoverRoots(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("skipping (no local DNS available): %v", err)
|
||||||
|
}
|
||||||
|
if len(servers) == 0 {
|
||||||
|
t.Fatal("expected at least one root server from discoverAllRoots")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverAllRootsWithAAAA(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cfg := &RootDiscoveryConfig{
|
||||||
|
AllRoots: true,
|
||||||
|
IncludeAAAA: true,
|
||||||
|
}
|
||||||
|
servers, err := DiscoverRoots(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("skipping (no local DNS available): %v", err)
|
||||||
|
}
|
||||||
|
if len(servers) == 0 {
|
||||||
|
t.Fatal("expected root servers with AAAA")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveRootServerDirect(t *testing.T) {
|
||||||
|
// Calls resolveRootServer directly (unexported, but in same package).
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
servers, err := resolveRootServer(ctx, "a.root-servers.net.", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("skipping (no local DNS): %v", err)
|
||||||
|
}
|
||||||
|
if len(servers) == 0 || len(servers[0].IPv4) == 0 {
|
||||||
|
t.Fatal("expected IPv4 address for a.root-servers.net.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverSingleRootWithAAAA(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cfg := &RootDiscoveryConfig{
|
||||||
|
AllRoots: false,
|
||||||
|
IncludeAAAA: true,
|
||||||
|
}
|
||||||
|
servers, err := DiscoverRoots(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("skipping (no local DNS available): %v", err)
|
||||||
|
}
|
||||||
|
if len(servers) == 0 {
|
||||||
|
t.Fatal("expected at least one root server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRealExchangeTCPPath(t *testing.T) {
|
||||||
|
// Test the TCP path of realExchange via a test server
|
||||||
|
tcpAddr := ""
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("cannot start TCP test server: %v", err)
|
||||||
|
}
|
||||||
|
tcpAddr = listener.Addr().String()
|
||||||
|
|
||||||
|
mux := dns.NewServeMux()
|
||||||
|
mux.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetReply(r)
|
||||||
|
resp.Answer = append(resp.Answer, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||||
|
A: net.ParseIP("9.9.9.9"),
|
||||||
|
})
|
||||||
|
_ = w.WriteMsg(resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := &dns.Server{
|
||||||
|
Listener: listener,
|
||||||
|
Net: "tcp",
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
started := make(chan struct{})
|
||||||
|
srv.NotifyStartedFunc = func() { close(started) }
|
||||||
|
|
||||||
|
go func() { _ = srv.ActivateAndServe() }()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-started:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Skip("TCP DNS server didn't start")
|
||||||
|
}
|
||||||
|
defer srv.Shutdown()
|
||||||
|
|
||||||
|
host, portStr, _ := net.SplitHostPort(tcpAddr)
|
||||||
|
serverIP := net.ParseIP(host)
|
||||||
|
|
||||||
|
cfg := DefaultQueryConfig()
|
||||||
|
cfg.UseTCP = true
|
||||||
|
cfg.Retries = 1
|
||||||
|
|
||||||
|
resp, err := QueryWithExchange(context.Background(), serverIP, "example.com", TypeA, cfg,
|
||||||
|
func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||||
|
targetAddr := fmt.Sprintf("%s:%s", host, portStr)
|
||||||
|
c := &dns.Client{Net: "tcp", ReadTimeout: 3 * time.Second, WriteTimeout: 3 * time.Second}
|
||||||
|
r, _, e := c.ExchangeContext(ctx, msg, targetAddr)
|
||||||
|
return r, e
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("TCP query: %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.Answer) == 0 {
|
||||||
|
t.Fatal("expected answers")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,926 @@
|
|||||||
|
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 }
|
||||||
Reference in New Issue
Block a user