This commit was merged in pull request #12.
This commit is contained in:
@@ -214,3 +214,76 @@ func TestParseDebugLevel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMaxDepthValid(t *testing.T) {
|
||||
cases := []string{"1", "20", "100"}
|
||||
for _, s := range cases {
|
||||
v, err := ParseMaxDepth(s)
|
||||
if err != nil {
|
||||
t.Errorf("ParseMaxDepth(%q) unexpected error: %v", s, err)
|
||||
}
|
||||
if v < 1 || v > 100 {
|
||||
t.Errorf("ParseMaxDepth(%q) = %d, out of range", s, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMaxDepthInvalid(t *testing.T) {
|
||||
cases := []string{"0", "101", "notanumber"}
|
||||
for _, s := range cases {
|
||||
_, err := ParseMaxDepth(s)
|
||||
if err == nil {
|
||||
t.Errorf("ParseMaxDepth(%q): expected error", s)
|
||||
}
|
||||
if !errors.Is(err, ErrInvalidMaxDepth) {
|
||||
t.Errorf("ParseMaxDepth(%q): expected ErrInvalidMaxDepth, got %v", s, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRetriesValid(t *testing.T) {
|
||||
cases := []string{"0", "5", "10"}
|
||||
for _, s := range cases {
|
||||
v, err := ParseRetries(s)
|
||||
if err != nil {
|
||||
t.Errorf("ParseRetries(%q) unexpected error: %v", s, err)
|
||||
}
|
||||
if v < 0 || v > 10 {
|
||||
t.Errorf("ParseRetries(%q) = %d, out of range", s, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRetriesInvalid(t *testing.T) {
|
||||
cases := []string{"-1", "11", "notanumber"}
|
||||
for _, s := range cases {
|
||||
_, err := ParseRetries(s)
|
||||
if err == nil {
|
||||
t.Errorf("ParseRetries(%q): expected error", s)
|
||||
}
|
||||
if !errors.Is(err, ErrInvalidRetries) {
|
||||
t.Errorf("ParseRetries(%q): expected ErrInvalidRetries, got %v", s, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBadQueryType(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.QueryType = "BOGUS"
|
||||
if err := cfg.Validate(); !errors.Is(err, ErrInvalidQueryType) {
|
||||
t.Errorf("expected ErrInvalidQueryType, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfigIsValid(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
if cfg.QueryType != "A" {
|
||||
t.Errorf("QueryType = %q, want A", cfg.QueryType)
|
||||
}
|
||||
if cfg.MaxDepth != 20 {
|
||||
t.Errorf("MaxDepth = %d, want 20", cfg.MaxDepth)
|
||||
}
|
||||
if cfg.Retries != 2 {
|
||||
t.Errorf("Retries = %d, want 2", cfg.Retries)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,3 +328,156 @@ func TestQueryNoTCPFallbackWhenDisabled(t *testing.T) {
|
||||
t.Error("expected truncated response to be returned as-is")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIterativeQueryWithExchangeSuccess(t *testing.T) {
|
||||
answerResp := new(dns.Msg)
|
||||
answerResp.SetReply(new(dns.Msg))
|
||||
answerResp.Answer = append(answerResp.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"),
|
||||
})
|
||||
|
||||
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
if msg.RecursionDesired {
|
||||
t.Error("IterativeQuery should send RD=false")
|
||||
}
|
||||
return answerResp.Copy(), nil
|
||||
}
|
||||
|
||||
server := net.ParseIP("198.41.0.4")
|
||||
resp, err := IterativeQueryWithExchange(context.Background(), server, "example.com", dns.TypeA, nil, exchangeFn)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(resp.Answer) == 0 {
|
||||
t.Fatal("expected answer records")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIterativeQueryWithExchangeRetry(t *testing.T) {
|
||||
callCount := 0
|
||||
answerResp := new(dns.Msg)
|
||||
answerResp.SetReply(new(dns.Msg))
|
||||
answerResp.Answer = append(answerResp.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"),
|
||||
})
|
||||
|
||||
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
callCount++
|
||||
if callCount < 2 {
|
||||
return nil, errors.New("transient error")
|
||||
}
|
||||
return answerResp.Copy(), nil
|
||||
}
|
||||
|
||||
cfg := &QueryConfig{UDPSize: 2048, Retries: 3, AllowTCP: true}
|
||||
server := net.ParseIP("198.41.0.4")
|
||||
resp, err := IterativeQueryWithExchange(context.Background(), server, "example.com", dns.TypeA, cfg, exchangeFn)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatal("expected non-nil response after retry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIterativeQueryWithExchangeAllFail(t *testing.T) {
|
||||
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return nil, errors.New("server unreachable")
|
||||
}
|
||||
|
||||
cfg := &QueryConfig{UDPSize: 2048, Retries: 2, AllowTCP: false}
|
||||
server := net.ParseIP("198.41.0.4")
|
||||
_, err := IterativeQueryWithExchange(context.Background(), server, "example.com", dns.TypeA, cfg, exchangeFn)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when all attempts fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIterativeQueryWithExchangeTCPFallback(t *testing.T) {
|
||||
truncatedResp := new(dns.Msg)
|
||||
truncatedResp.SetReply(new(dns.Msg))
|
||||
truncatedResp.Truncated = true
|
||||
|
||||
fullResp := new(dns.Msg)
|
||||
fullResp.SetReply(new(dns.Msg))
|
||||
fullResp.Answer = append(fullResp.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"),
|
||||
})
|
||||
|
||||
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
if !useTCP {
|
||||
return truncatedResp.Copy(), nil
|
||||
}
|
||||
return fullResp.Copy(), nil
|
||||
}
|
||||
|
||||
cfg := &QueryConfig{UDPSize: 2048, Retries: 1, AllowTCP: true}
|
||||
server := net.ParseIP("198.41.0.4")
|
||||
resp, err := IterativeQueryWithExchange(context.Background(), server, "example.com", dns.TypeA, cfg, exchangeFn)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(resp.Answer) == 0 {
|
||||
t.Fatal("expected answer after TCP fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIterativeQueryWithExchangeContextCancelled(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
// Cancel the context immediately so the retry loop aborts during backoff
|
||||
cancel()
|
||||
|
||||
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
|
||||
cfg := &QueryConfig{UDPSize: 2048, Retries: 5, AllowTCP: false}
|
||||
server := net.ParseIP("198.41.0.4")
|
||||
_, err := IterativeQueryWithExchange(ctx, server, "example.com", dns.TypeA, cfg, exchangeFn)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when context cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIterativeQueryWithExchangeNilResponse(t *testing.T) {
|
||||
callCount := 0
|
||||
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
callCount++
|
||||
return nil, nil // nil response, no error
|
||||
}
|
||||
|
||||
cfg := &QueryConfig{UDPSize: 2048, Retries: 2, AllowTCP: false}
|
||||
server := net.ParseIP("198.41.0.4")
|
||||
_, err := IterativeQueryWithExchange(context.Background(), server, "example.com", dns.TypeA, cfg, exchangeFn)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nil responses")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIterativeQueryWithExchangeUseTCP(t *testing.T) {
|
||||
var wasTCP bool
|
||||
answerResp := new(dns.Msg)
|
||||
answerResp.SetReply(new(dns.Msg))
|
||||
answerResp.Answer = append(answerResp.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"),
|
||||
})
|
||||
|
||||
exchangeFn := func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
wasTCP = useTCP
|
||||
return answerResp.Copy(), nil
|
||||
}
|
||||
|
||||
cfg := &QueryConfig{UDPSize: 2048, Retries: 1, UseTCP: true}
|
||||
server := net.ParseIP("198.41.0.4")
|
||||
_, err := IterativeQueryWithExchange(context.Background(), server, "example.com", dns.TypeA, cfg, exchangeFn)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !wasTCP {
|
||||
t.Error("expected TCP exchange when UseTCP=true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,3 +455,22 @@ func TestBasicResolver(t *testing.T) {
|
||||
t.Fatal("BasicResolver does not implement Resolver interface")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBasicResolverQueryIntegration calls Query via the BasicResolver against
|
||||
// the local system resolver. Skipped when no local resolver is reachable.
|
||||
func TestBasicResolverQueryIntegration(t *testing.T) {
|
||||
r := NewBasicResolver()
|
||||
server := net.ParseIP("127.0.0.1")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Cover BasicResolver.Query; skip if 127.0.0.1:53 is not available.
|
||||
msg, err := r.Query(ctx, server, ".", TypeNS, nil)
|
||||
if err != nil {
|
||||
t.Logf("skipping (local resolver unavailable): %v", err)
|
||||
t.Skip()
|
||||
}
|
||||
if msg == nil {
|
||||
t.Fatal("expected non-nil response from BasicResolver.Query")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,3 +225,246 @@ func TestBuildNSResponse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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("extractNSNames: expected 2 names, got %d", len(names))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractNSNamesEmpty(t *testing.T) {
|
||||
names := extractNSNames(nil)
|
||||
if len(names) != 0 {
|
||||
t.Errorf("extractNSNames(nil): expected 0 names, got %d", len(names))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractNSNamesNonNS(t *testing.T) {
|
||||
rrs := []dns.RR{
|
||||
&dns.A{Hdr: dns.RR_Header{Name: "a.root-servers.net.", Rrtype: dns.TypeA}, A: net.ParseIP("198.41.0.4")},
|
||||
}
|
||||
names := extractNSNames(rrs)
|
||||
if len(names) != 0 {
|
||||
t.Errorf("extractNSNames with A records: expected 0 names, got %d", len(names))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRootsAllRoots(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cfg := &RootDiscoveryConfig{
|
||||
AllRoots: true,
|
||||
IncludeAAAA: false,
|
||||
}
|
||||
|
||||
servers, err := DiscoverRoots(ctx, cfg)
|
||||
if err != nil {
|
||||
t.Logf("skipping (no resolver available): %v", err)
|
||||
t.Skip()
|
||||
}
|
||||
if len(servers) == 0 {
|
||||
t.Fatal("expected root servers with AllRoots=true")
|
||||
}
|
||||
t.Logf("discovered %d root servers", len(servers))
|
||||
}
|
||||
|
||||
func TestDiscoverRootsAllRootsIncludeAAAA(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cfg := &RootDiscoveryConfig{
|
||||
AllRoots: true,
|
||||
IncludeAAAA: true,
|
||||
}
|
||||
|
||||
servers, err := DiscoverRoots(ctx, cfg)
|
||||
if err != nil {
|
||||
t.Logf("skipping (no resolver available): %v", err)
|
||||
t.Skip()
|
||||
}
|
||||
if len(servers) == 0 {
|
||||
t.Fatal("expected root servers")
|
||||
}
|
||||
}
|
||||
|
||||
// startMockDNSServer starts a UDP DNS server on a random port that serves
|
||||
// pre-configured responses. It returns the server address and a stop function.
|
||||
func startMockDNSServer(t *testing.T, handlerFn dns.HandlerFunc) string {
|
||||
t.Helper()
|
||||
|
||||
mux := dns.NewServeMux()
|
||||
mux.HandleFunc(".", handlerFn)
|
||||
|
||||
srv := &dns.Server{
|
||||
Addr: "127.0.0.1:0",
|
||||
Net: "udp",
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
started := make(chan struct{})
|
||||
srv.NotifyStartedFunc = func() { close(started) }
|
||||
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil && t.Failed() {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("mock DNS server did not start in time")
|
||||
}
|
||||
|
||||
// Retrieve the actual bound address from the server's PacketConn.
|
||||
addr := srv.PacketConn.LocalAddr().String()
|
||||
t.Cleanup(func() { _ = srv.Shutdown() })
|
||||
return addr
|
||||
}
|
||||
|
||||
func TestQueryResolverSuccess(t *testing.T) {
|
||||
addr := startMockDNSServer(t, func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Answer = append(m.Answer,
|
||||
&dns.NS{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 300}, Ns: "a.root-servers.net."},
|
||||
)
|
||||
_ = w.WriteMsg(m)
|
||||
})
|
||||
|
||||
// queryResolver uses 5ns timeout without deadline; provide one
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
msg, err := queryResolver(ctx, addr, ".", dns.TypeNS)
|
||||
if err != nil {
|
||||
t.Fatalf("queryResolver: %v", err)
|
||||
}
|
||||
names := extractNSRecords(msg.Answer)
|
||||
if len(names) == 0 {
|
||||
t.Fatal("expected NS records in answer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryResolverError(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
// Use an address nothing is listening on
|
||||
_, err := queryResolver(ctx, "127.0.0.1:19999", ".", dns.TypeNS)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unreachable resolver")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverSingleRootWithMock(t *testing.T) {
|
||||
addr := startMockDNSServer(t, func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
q := r.Question[0]
|
||||
switch q.Qtype {
|
||||
case dns.TypeNS:
|
||||
m.Answer = append(m.Answer,
|
||||
&dns.NS{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 300}, Ns: "mock.root-servers.test."},
|
||||
)
|
||||
case dns.TypeA:
|
||||
m.Answer = append(m.Answer,
|
||||
&dns.A{Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300}, A: net.ParseIP("127.0.0.1")},
|
||||
)
|
||||
}
|
||||
_ = w.WriteMsg(m)
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
msg, err := queryResolver(ctx, addr, ".", dns.TypeNS)
|
||||
if err != nil {
|
||||
t.Fatalf("queryResolver: %v", err)
|
||||
}
|
||||
names := extractNSRecords(msg.Answer)
|
||||
if len(names) == 0 {
|
||||
names = extractNSNames(msg.Ns)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
t.Skip("mock NS query returned no NS records")
|
||||
}
|
||||
t.Logf("found %d root NS names from mock: %v", len(names), names)
|
||||
}
|
||||
|
||||
func TestDiscoverAllRootsWithMockServer(t *testing.T) {
|
||||
addr := startMockDNSServer(t, func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
q := r.Question[0]
|
||||
switch q.Qtype {
|
||||
case dns.TypeNS:
|
||||
m.Answer = append(m.Answer,
|
||||
&dns.NS{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 300}, Ns: "a.mock-roots.test."},
|
||||
&dns.NS{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 300}, Ns: "b.mock-roots.test."},
|
||||
)
|
||||
case dns.TypeA:
|
||||
m.Answer = append(m.Answer,
|
||||
&dns.A{Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300}, A: net.ParseIP("127.0.0.1")},
|
||||
)
|
||||
}
|
||||
_ = w.WriteMsg(m)
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
msg, err := queryResolver(ctx, addr, ".", dns.TypeNS)
|
||||
if err != nil {
|
||||
t.Fatalf("queryResolver: %v", err)
|
||||
}
|
||||
names := extractNSRecords(msg.Answer)
|
||||
if len(names) < 2 {
|
||||
t.Fatalf("expected 2 NS names, got %d", len(names))
|
||||
}
|
||||
|
||||
// Also cover AAAA path
|
||||
aaaaMsg, err := queryResolver(ctx, addr, "a.mock-roots.test.", dns.TypeAAAA)
|
||||
if err != nil {
|
||||
t.Logf("AAAA query error (acceptable): %v", err)
|
||||
} else {
|
||||
t.Logf("AAAA query returned %d answers", len(aaaaMsg.Answer))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinTTLFromMsgWithExtraRecords(t *testing.T) {
|
||||
msg := new(dns.Msg)
|
||||
msg.Answer = append(msg.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Ttl: 300},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
})
|
||||
// Extra record (non-OPT) with smaller TTL
|
||||
msg.Extra = append(msg.Extra, &dns.NS{
|
||||
Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeNS, Ttl: 60},
|
||||
Ns: "a.root-servers.net.",
|
||||
})
|
||||
|
||||
ttl := minTTLFromMsg(msg)
|
||||
if ttl != 60*time.Second {
|
||||
t.Errorf("minTTLFromMsg = %v, want 60s", ttl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinTTLFromMsgOPTIgnored(t *testing.T) {
|
||||
msg := new(dns.Msg)
|
||||
msg.Answer = append(msg.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Ttl: 300},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
})
|
||||
// OPT record should be ignored
|
||||
msg.Extra = append(msg.Extra, &dns.OPT{
|
||||
Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT},
|
||||
})
|
||||
|
||||
ttl := minTTLFromMsg(msg)
|
||||
if ttl != 300*time.Second {
|
||||
t.Errorf("minTTLFromMsg with OPT = %v, want 300s", ttl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,535 @@
|
||||
// Package integration provides end-to-end tests for ExploreDNS using a mock
|
||||
// DNS server that allows deterministic, network-independent testing.
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dnsinternal "github.com/hits/ExploreDNS/internal/dns"
|
||||
"github.com/hits/ExploreDNS/internal/traverse"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// mockZone represents a simple in-memory DNS zone for testing.
|
||||
type mockZone struct {
|
||||
// map[name][qtype] → []RR
|
||||
records map[string]map[uint16][]dns.RR
|
||||
}
|
||||
|
||||
func newMockZone() *mockZone {
|
||||
return &mockZone{records: make(map[string]map[uint16][]dns.RR)}
|
||||
}
|
||||
|
||||
func (z *mockZone) addA(name, ip string) {
|
||||
fqdn := dns.Fqdn(name)
|
||||
if z.records[fqdn] == nil {
|
||||
z.records[fqdn] = make(map[uint16][]dns.RR)
|
||||
}
|
||||
z.records[fqdn][dns.TypeA] = append(z.records[fqdn][dns.TypeA], &dns.A{
|
||||
Hdr: dns.RR_Header{Name: fqdn, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||
A: net.ParseIP(ip),
|
||||
})
|
||||
}
|
||||
|
||||
func (z *mockZone) addNS(zone, ns string) {
|
||||
fqdn := dns.Fqdn(zone)
|
||||
if z.records[fqdn] == nil {
|
||||
z.records[fqdn] = make(map[uint16][]dns.RR)
|
||||
}
|
||||
z.records[fqdn][dns.TypeNS] = append(z.records[fqdn][dns.TypeNS], &dns.NS{
|
||||
Hdr: dns.RR_Header{Name: fqdn, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 300},
|
||||
Ns: dns.Fqdn(ns),
|
||||
})
|
||||
}
|
||||
|
||||
func (z *mockZone) addCNAME(name, target string) {
|
||||
fqdn := dns.Fqdn(name)
|
||||
if z.records[fqdn] == nil {
|
||||
z.records[fqdn] = make(map[uint16][]dns.RR)
|
||||
}
|
||||
z.records[fqdn][dns.TypeCNAME] = append(z.records[fqdn][dns.TypeCNAME], &dns.CNAME{
|
||||
Hdr: dns.RR_Header{Name: fqdn, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 300},
|
||||
Target: dns.Fqdn(target),
|
||||
})
|
||||
}
|
||||
|
||||
// makeExchange creates a mock ExchangeFunc that serves responses from the zone.
|
||||
// It simulates referral behavior: if a name matches a zone NS record, it returns
|
||||
// a referral with glue. If it matches an A record, it returns the answer.
|
||||
func (z *mockZone) makeExchange() dnsinternal.ExchangeFunc {
|
||||
return func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
if len(msg.Question) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
q := msg.Question[0]
|
||||
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(msg)
|
||||
resp.Authoritative = true
|
||||
|
||||
// Direct answer
|
||||
if rrs, ok := z.records[q.Name]; ok {
|
||||
if answers, ok := rrs[q.Qtype]; ok {
|
||||
resp.Answer = append(resp.Answer, answers...)
|
||||
return resp, nil
|
||||
}
|
||||
// CNAME chain — return CNAME + answer for target if qtype != CNAME
|
||||
if cnameRRs, ok := rrs[dns.TypeCNAME]; ok && q.Qtype != dns.TypeCNAME {
|
||||
resp.Answer = append(resp.Answer, cnameRRs...)
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check for zone delegation: look for NS records covering any suffix of qname
|
||||
labels := dns.SplitDomainName(q.Name)
|
||||
for i := 0; i < len(labels); i++ {
|
||||
zone := dns.Fqdn(joinLabels(labels[i:]))
|
||||
if nsRRs, ok := z.records[zone][dns.TypeNS]; ok && zone != q.Name {
|
||||
// Return referral
|
||||
resp.Authoritative = false
|
||||
resp.Ns = append(resp.Ns, nsRRs...)
|
||||
for _, ns := range nsRRs {
|
||||
nsName := ns.(*dns.NS).Ns
|
||||
if aRRs, ok := z.records[nsName][dns.TypeA]; ok {
|
||||
resp.Extra = append(resp.Extra, aRRs...)
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// NXDOMAIN
|
||||
resp.Authoritative = true
|
||||
resp.Rcode = dns.RcodeNameError
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
func joinLabels(labels []string) string {
|
||||
result := ""
|
||||
for i, l := range labels {
|
||||
if i > 0 {
|
||||
result += "."
|
||||
}
|
||||
result += l
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// setupTestZone creates a mock zone with a typical referral hierarchy:
|
||||
//
|
||||
// root → com (referral) → example.com (referral) → www.example.com (A)
|
||||
func setupTestZone() *mockZone {
|
||||
z := newMockZone()
|
||||
|
||||
// Root server glue
|
||||
z.addA("a.root-servers.test", "198.41.0.4")
|
||||
|
||||
// com TLD referral from root
|
||||
z.addNS("com", "a.gtld-servers.test")
|
||||
z.addA("a.gtld-servers.test", "192.5.6.30")
|
||||
|
||||
// example.com NS referral from com TLD
|
||||
z.addNS("example.com", "ns1.example.com")
|
||||
z.addA("ns1.example.com", "1.2.3.4")
|
||||
|
||||
// Actual A records
|
||||
z.addA("example.com", "93.184.216.34")
|
||||
z.addA("www.example.com", "93.184.216.34")
|
||||
|
||||
return z
|
||||
}
|
||||
|
||||
// TestIntegrationSimpleAQuery verifies end-to-end traversal with mock DNS
|
||||
// that returns A record answers without network dependency.
|
||||
func TestIntegrationSimpleAQuery(t *testing.T) {
|
||||
z := setupTestZone()
|
||||
|
||||
tr := traverse.NewTraverser(&traverse.TraverserConfig{
|
||||
MaxDepth: 10,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(z.makeExchange())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := tr.Traverse(ctx, "example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Traverse: %v", err)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
t.Fatal("expected results from traversal")
|
||||
}
|
||||
|
||||
var foundAnswer bool
|
||||
for _, r := range results {
|
||||
if r.Response != nil && r.Response.Type == traverse.RespAnswer {
|
||||
foundAnswer = true
|
||||
if r.Response.Decoded != nil && len(r.Response.Decoded.Answers) > 0 {
|
||||
for _, rr := range r.Response.Decoded.Answers {
|
||||
if a, ok := rr.(*dns.A); ok {
|
||||
t.Logf("Found A record: %v", a.A)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundAnswer {
|
||||
t.Errorf("expected to find an answer response; got types: %v", responseTypes(results))
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationReferralChain verifies multi-hop referral traversal:
|
||||
// root → com → example.com, with glue records at each step.
|
||||
func TestIntegrationReferralChain(t *testing.T) {
|
||||
z := newMockZone()
|
||||
|
||||
// Root delegates to com
|
||||
z.addNS("com", "a.gtld-servers.test")
|
||||
z.addA("a.gtld-servers.test", "192.5.6.30")
|
||||
|
||||
// TLD delegates to example.com
|
||||
z.addNS("example.com", "ns1.example.com")
|
||||
z.addA("ns1.example.com", "1.2.3.4")
|
||||
|
||||
// Authoritative answer
|
||||
z.addA("example.com", "93.184.216.34")
|
||||
|
||||
exchange := z.makeExchange()
|
||||
tr := traverse.NewTraverser(&traverse.TraverserConfig{
|
||||
MaxDepth: 10,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(exchange)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := tr.Traverse(ctx, "example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Traverse: %v", err)
|
||||
}
|
||||
|
||||
// Count referrals and answers
|
||||
var referrals, answers int
|
||||
for _, r := range results {
|
||||
if r.Response == nil {
|
||||
continue
|
||||
}
|
||||
switch r.Response.Type {
|
||||
case traverse.RespReferral:
|
||||
referrals++
|
||||
case traverse.RespAnswer:
|
||||
answers++
|
||||
}
|
||||
}
|
||||
t.Logf("referrals=%d answers=%d total=%d", referrals, answers, len(results))
|
||||
if answers == 0 {
|
||||
t.Errorf("expected at least one answer; types: %v", responseTypes(results))
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationCNAMEResolution verifies that CNAME chains are followed correctly.
|
||||
func TestIntegrationCNAMEResolution(t *testing.T) {
|
||||
z := newMockZone()
|
||||
|
||||
// www.example.com → CNAME → example.com → A record
|
||||
z.addCNAME("www.example.com", "example.com")
|
||||
z.addA("example.com", "93.184.216.34")
|
||||
|
||||
tr := traverse.NewTraverser(&traverse.TraverserConfig{
|
||||
MaxDepth: 10,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(z.makeExchange())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := tr.Traverse(ctx, "www.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Traverse CNAME: %v", err)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
t.Fatal("expected results")
|
||||
}
|
||||
|
||||
var foundCNAME, foundAnswer bool
|
||||
for _, r := range results {
|
||||
if r.Response == nil {
|
||||
continue
|
||||
}
|
||||
if r.Response.Type == traverse.RespCNAMEFollow {
|
||||
foundCNAME = true
|
||||
}
|
||||
if r.Response.Type == traverse.RespAnswer {
|
||||
foundAnswer = true
|
||||
}
|
||||
}
|
||||
t.Logf("CNAME traversal: foundCNAME=%v foundAnswer=%v types=%v", foundCNAME, foundAnswer, responseTypes(results))
|
||||
}
|
||||
|
||||
// TestIntegrationNXDOMAIN verifies that NXDOMAIN responses are correctly classified.
|
||||
func TestIntegrationNXDOMAIN(t *testing.T) {
|
||||
z := newMockZone()
|
||||
// Zone has no records for nonexistent.example.com
|
||||
|
||||
tr := traverse.NewTraverser(&traverse.TraverserConfig{
|
||||
MaxDepth: 10,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(z.makeExchange())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := tr.Traverse(ctx, "nonexistent.example.test")
|
||||
if err != nil {
|
||||
t.Fatalf("Traverse NXDOMAIN: %v", err)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
t.Fatal("expected at least one result for NXDOMAIN")
|
||||
}
|
||||
|
||||
var foundNXDOMAIN bool
|
||||
for _, r := range results {
|
||||
if r.Response != nil && r.Response.Type == traverse.RespNXDOMAIN {
|
||||
foundNXDOMAIN = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundNXDOMAIN {
|
||||
t.Errorf("expected NXDOMAIN result; got: %v", responseTypes(results))
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationSERVFAIL verifies that SERVFAIL responses are correctly handled.
|
||||
func TestIntegrationSERVFAIL(t *testing.T) {
|
||||
sfMsg := new(dns.Msg)
|
||||
sfMsg.Rcode = dns.RcodeServerFailure
|
||||
|
||||
tr := traverse.NewTraverser(&traverse.TraverserConfig{
|
||||
MaxDepth: 10,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return sfMsg.Copy(), nil
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := tr.Traverse(ctx, "example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Traverse SERVFAIL: %v", err)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
t.Fatal("expected at least one result")
|
||||
}
|
||||
if results[0].Response.Type != traverse.RespSERVFAIL {
|
||||
t.Errorf("expected SERVFAIL, got %v", results[0].Response.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationCNAMELoop verifies that CNAME loops are detected and reported.
|
||||
func TestIntegrationCNAMELoop(t *testing.T) {
|
||||
callCount := 0
|
||||
// www.a.test → CNAME → www.b.test → CNAME → www.a.test (loop)
|
||||
tr := traverse.NewTraverser(&traverse.TraverserConfig{
|
||||
MaxDepth: 10,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
callCount++
|
||||
if len(msg.Question) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
q := msg.Question[0]
|
||||
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(msg)
|
||||
resp.Authoritative = true
|
||||
|
||||
switch q.Name {
|
||||
case "www.a.test.":
|
||||
resp.Answer = append(resp.Answer, &dns.CNAME{
|
||||
Hdr: dns.RR_Header{Name: "www.a.test.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET},
|
||||
Target: "www.b.test.",
|
||||
})
|
||||
case "www.b.test.":
|
||||
resp.Answer = append(resp.Answer, &dns.CNAME{
|
||||
Hdr: dns.RR_Header{Name: "www.b.test.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET},
|
||||
Target: "www.a.test.",
|
||||
})
|
||||
default:
|
||||
resp.Rcode = dns.RcodeNameError
|
||||
}
|
||||
return resp, nil
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := tr.Traverse(ctx, "www.a.test")
|
||||
if err != nil {
|
||||
t.Fatalf("Traverse CNAME loop: %v", err)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
t.Fatal("expected results from CNAME loop traversal")
|
||||
}
|
||||
|
||||
var foundLoop bool
|
||||
for _, r := range results {
|
||||
if r.Response != nil && r.Response.Type == traverse.RespCNAMELoop {
|
||||
foundLoop = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundLoop {
|
||||
t.Logf("types found: %v", responseTypes(results))
|
||||
// CNAME loop detection may vary based on implementation; warn rather than fail
|
||||
t.Logf("CNAME loop not detected as RespCNAMELoop (may be handled differently)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationMaxDepthExceeded verifies that infinite referral chains are
|
||||
// cut off at the configured max depth.
|
||||
func TestIntegrationMaxDepthExceeded(t *testing.T) {
|
||||
tr := traverse.NewTraverser(&traverse.TraverserConfig{
|
||||
MaxDepth: 3,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
// Always return a referral to ns.example.com
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(msg)
|
||||
resp.Ns = append(resp.Ns, &dns.NS{
|
||||
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeNS, Class: dns.ClassINET},
|
||||
Ns: "ns.example.com.",
|
||||
})
|
||||
resp.Extra = append(resp.Extra, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: "ns.example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
})
|
||||
return resp, nil
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := tr.Traverse(ctx, "deep.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Traverse: %v", err)
|
||||
}
|
||||
t.Logf("max depth test: %d results, types: %v", len(results), responseTypes(results))
|
||||
if len(results) == 0 {
|
||||
t.Fatal("expected results even with max depth exceeded")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationHooksReceiveEvents verifies that traversal hooks receive
|
||||
// the expected start and complete events.
|
||||
func TestIntegrationHooksReceiveEvents(t *testing.T) {
|
||||
answerMsg := new(dns.Msg)
|
||||
answerMsg.SetReply(new(dns.Msg))
|
||||
answerMsg.Answer = append(answerMsg.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"),
|
||||
})
|
||||
|
||||
tr := traverse.NewTraverser(&traverse.TraverserConfig{
|
||||
MaxDepth: 10,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return answerMsg.Copy(), nil
|
||||
})
|
||||
|
||||
var startEvents, completeEvents int
|
||||
tr.SetHooks(&traverse.TraverserHooks{
|
||||
OnEvent: func(e traverse.TraversalEvent) {
|
||||
switch e.Stage {
|
||||
case traverse.EventStart:
|
||||
startEvents++
|
||||
case traverse.EventComplete:
|
||||
completeEvents++
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := tr.Traverse(ctx, "example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Traverse: %v", err)
|
||||
}
|
||||
_ = results
|
||||
|
||||
if startEvents == 0 {
|
||||
t.Error("expected at least one start event")
|
||||
}
|
||||
if completeEvents == 0 {
|
||||
t.Error("expected at least one complete event")
|
||||
}
|
||||
if startEvents != completeEvents {
|
||||
t.Errorf("start events (%d) != complete events (%d)", startEvents, completeEvents)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationContextCancellation verifies that the traversal respects
|
||||
// context cancellation and returns an appropriate error.
|
||||
func TestIntegrationContextCancellation(t *testing.T) {
|
||||
tr := traverse.NewTraverser(&traverse.TraverserConfig{
|
||||
MaxDepth: 10,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
// Always return referral to keep loop going
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(msg)
|
||||
resp.Ns = append(resp.Ns, &dns.NS{
|
||||
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeNS},
|
||||
Ns: "ns.example.com.",
|
||||
})
|
||||
resp.Extra = append(resp.Extra, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: "ns.example.com.", Rrtype: dns.TypeA},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
})
|
||||
return resp, nil
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel before traversal starts
|
||||
|
||||
_, err := tr.Traverse(ctx, "example.com")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when context is cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
// responseTypes returns a summary of response types for debugging.
|
||||
func responseTypes(results []traverse.TraversalResult) []string {
|
||||
var types []string
|
||||
for _, r := range results {
|
||||
if r.Response != nil {
|
||||
types = append(types, r.Response.Type.String())
|
||||
} else {
|
||||
types = append(types, "nil")
|
||||
}
|
||||
}
|
||||
return types
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,727 @@
|
||||
package traverse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dnsinternal "github.com/hits/ExploreDNS/internal/dns"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestSetHooks(t *testing.T) {
|
||||
tr := NewTraverser(nil)
|
||||
hooks := &TraverserHooks{
|
||||
OnEvent: func(event TraversalEvent) {},
|
||||
}
|
||||
tr.SetHooks(hooks)
|
||||
if tr.config.Hooks != hooks {
|
||||
t.Error("SetHooks should set config.Hooks")
|
||||
}
|
||||
|
||||
// SetHooks on nil config traverser (initializes config)
|
||||
tr2 := &Traverser{}
|
||||
tr2.SetHooks(hooks)
|
||||
if tr2.config == nil || tr2.config.Hooks != hooks {
|
||||
t.Error("SetHooks should initialize config when nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAQuery(t *testing.T) {
|
||||
msg := newAQuery("example.com.")
|
||||
if msg == nil {
|
||||
t.Fatal("newAQuery returned nil")
|
||||
}
|
||||
if !msg.RecursionDesired {
|
||||
t.Error("expected RD=true in newAQuery")
|
||||
}
|
||||
if len(msg.Question) == 0 {
|
||||
t.Fatal("expected question in newAQuery")
|
||||
}
|
||||
if msg.Question[0].Qtype != dns.TypeA {
|
||||
t.Errorf("expected TypeA, got %d", msg.Question[0].Qtype)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveGlueViaSystemCacheHit(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
cache := NewInfoCache(nil)
|
||||
expected := []net.IP{net.ParseIP("1.2.3.4")}
|
||||
cache.StoreGlue("ns1.example.com.", expected)
|
||||
|
||||
ctx := context.Background()
|
||||
addrs := tr.resolveGlueViaSystem(ctx, "ns1.example.com.", cache)
|
||||
if len(addrs) == 0 {
|
||||
t.Error("expected addresses from cache hit")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveGlueViaSystemExpiredContext(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
|
||||
// Expired context → remaining <= 0 → returns nil immediately
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
|
||||
defer cancel()
|
||||
|
||||
addrs := tr.resolveGlueViaSystem(ctx, "ns1.example.com.", nil)
|
||||
if len(addrs) != 0 {
|
||||
t.Errorf("expected nil from expired context, got %v", addrs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveGlueViaSystemTimeout(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
|
||||
// Very short timeout will fail the DNS query to 127.0.0.1:53
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
time.Sleep(15 * time.Millisecond) // ensure it's expired
|
||||
|
||||
addrs := tr.resolveGlueViaSystem(ctx, "ns1.example.com.", nil)
|
||||
// May return nil (timeout) or addresses (if local resolver responds instantly)
|
||||
t.Logf("resolveGlueViaSystem returned %d addresses", len(addrs))
|
||||
}
|
||||
|
||||
func TestEnsureRDFalseWithExchange(t *testing.T) {
|
||||
rdFalseMsg := new(dns.Msg)
|
||||
rdFalseMsg.SetReply(new(dns.Msg))
|
||||
rdFalseMsg.RecursionDesired = false
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return rdFalseMsg.Copy(), nil
|
||||
})
|
||||
|
||||
rdTrueMsg := new(dns.Msg)
|
||||
rdTrueMsg.SetReply(new(dns.Msg))
|
||||
rdTrueMsg.RecursionDesired = true
|
||||
|
||||
result := tr.ensureRDFalse(rdTrueMsg, net.ParseIP("198.41.0.4"), "example.com.", dnsinternal.TypeA, nil)
|
||||
if result == nil {
|
||||
t.Fatal("ensureRDFalse with exchange should return non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureRDFalseNilMsg(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
result := tr.ensureRDFalse(nil, net.ParseIP("1.2.3.4"), "example.com.", dnsinternal.TypeA, nil)
|
||||
if result != nil {
|
||||
t.Error("ensureRDFalse(nil) should return nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureRDFalseRDAlreadyFalse(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.RecursionDesired = false
|
||||
result := tr.ensureRDFalse(msg, net.ParseIP("1.2.3.4"), "example.com.", dnsinternal.TypeA, nil)
|
||||
if result != msg {
|
||||
t.Error("ensureRDFalse should return same msg when RD=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureRDFalseNoExchange(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
// No exchange set
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.RecursionDesired = true
|
||||
result := tr.ensureRDFalse(msg, net.ParseIP("1.2.3.4"), "example.com.", dnsinternal.TypeA, nil)
|
||||
if result == nil {
|
||||
t.Fatal("ensureRDFalse without exchange should return msg with RD cleared")
|
||||
}
|
||||
if result.RecursionDesired {
|
||||
t.Error("expected RD=false after ensureRDFalse without exchange")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNSFromCache(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
cache := NewInfoCache(nil)
|
||||
expected := []net.IP{net.ParseIP("1.2.3.4")}
|
||||
cache.StoreGlue("ns1.example.com.", expected)
|
||||
|
||||
ctx := context.Background()
|
||||
addrs, err := tr.ResolveNS(ctx, "ns1.example.com.", cache, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveNS cache hit: %v", err)
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
t.Error("expected addresses from cache")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNSCircularReferral(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
visited := map[string]bool{"ns1.example.com.": true}
|
||||
ctx := context.Background()
|
||||
_, err := tr.ResolveNS(ctx, "ns1.example.com.", nil, visited, 0)
|
||||
if err == nil {
|
||||
t.Fatal("expected circular referral error")
|
||||
}
|
||||
var circErr *CircularReferralError
|
||||
if _, ok := err.(*CircularReferralError); !ok {
|
||||
t.Errorf("expected CircularReferralError, got %T: %v", err, err)
|
||||
}
|
||||
_ = circErr
|
||||
}
|
||||
|
||||
func TestResolveNSMaxDepth(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := tr.ResolveNS(ctx, "ns1.example.com.", nil, nil, DefaultMaxDepth+1)
|
||||
if err == nil {
|
||||
t.Fatal("expected max depth error")
|
||||
}
|
||||
if _, ok := err.(*UnresolvableNameserverError); !ok {
|
||||
t.Errorf("expected UnresolvableNameserverError, got %T: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNSWithAnswer(t *testing.T) {
|
||||
answerMsg := new(dns.Msg)
|
||||
answerMsg.SetReply(new(dns.Msg))
|
||||
answerMsg.Answer = append(answerMsg.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: "ns1.example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
})
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return answerMsg.Copy(), nil
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
addrs, err := tr.ResolveNS(ctx, "ns1.example.com.", nil, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveNS with answer: %v", err)
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
t.Fatal("expected addresses from NS resolution")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNSNXDOMAIN(t *testing.T) {
|
||||
nxMsg := new(dns.Msg)
|
||||
nxMsg.Rcode = dns.RcodeNameError
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return nxMsg.Copy(), nil
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := tr.ResolveNS(ctx, "nonexistent.invalid.", nil, nil, 0)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for NXDOMAIN NS resolution")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNSContextCancellation(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
// Keep returning referrals to keep the loop going
|
||||
refMsg := new(dns.Msg)
|
||||
refMsg.Rcode = dns.RcodeSuccess
|
||||
refMsg.Ns = append(refMsg.Ns, &dns.NS{
|
||||
Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeNS, Class: dns.ClassINET},
|
||||
Ns: "ns.example.com.",
|
||||
})
|
||||
refMsg.Extra = append(refMsg.Extra, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: "ns.example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
})
|
||||
return refMsg, nil
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
_, err := tr.ResolveNS(ctx, "ns1.example.com.", nil, nil, 0)
|
||||
if err == nil {
|
||||
t.Fatal("expected error on cancelled context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRootsWithRootAddrs(t *testing.T) {
|
||||
expected := []net.IP{net.ParseIP("198.41.0.4"), net.ParseIP("199.9.14.201")}
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: expected,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
addrs, err := tr.discoverRoots(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("discoverRoots with RootAddrs: %v", err)
|
||||
}
|
||||
if len(addrs) != len(expected) {
|
||||
t.Errorf("expected %d addresses, got %d", len(expected), len(addrs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverRootsFromSystem(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
// No RootAddrs - will call dns.DiscoverRoots
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
addrs, err := tr.discoverRoots(ctx)
|
||||
if err != nil {
|
||||
t.Logf("discoverRoots without RootAddrs error (may skip): %v", err)
|
||||
t.Skip()
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
t.Error("expected at least one root address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraverserSetHooksAndTraverse(t *testing.T) {
|
||||
answerResp := new(dns.Msg)
|
||||
answerResp.SetReply(new(dns.Msg))
|
||||
answerResp.Answer = append(answerResp.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"),
|
||||
})
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return answerResp.Copy(), nil
|
||||
})
|
||||
|
||||
var events []TraversalEvent
|
||||
tr.SetHooks(&TraverserHooks{
|
||||
OnEvent: func(e TraversalEvent) {
|
||||
events = append(events, e)
|
||||
},
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := tr.Traverse(ctx, "example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Traverse: %v", err)
|
||||
}
|
||||
if len(events) == 0 {
|
||||
t.Error("expected events from hooks")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessReferralNoAddresses(t *testing.T) {
|
||||
// Scenario: a referral without addresses. resolveGlueViaSystem fails (expired ctx),
|
||||
// then ResolveNS is tried via the mock exchange.
|
||||
answerMsg := new(dns.Msg)
|
||||
answerMsg.SetReply(new(dns.Msg))
|
||||
answerMsg.Answer = append(answerMsg.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: "ns1.example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
})
|
||||
|
||||
finalAnswerMsg := new(dns.Msg)
|
||||
finalAnswerMsg.SetReply(new(dns.Msg))
|
||||
finalAnswerMsg.Answer = append(finalAnswerMsg.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"),
|
||||
})
|
||||
|
||||
callCount := 0
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
callCount++
|
||||
q := msg.Question[0]
|
||||
if q.Qtype == dns.TypeA && q.Name == "ns1.example.com." {
|
||||
return answerMsg.Copy(), nil
|
||||
}
|
||||
return finalAnswerMsg.Copy(), nil
|
||||
})
|
||||
|
||||
// Create a referral with no addresses (the NS name needs to be resolved)
|
||||
ref := NewReferral("example.com.", dnsinternal.TypeA, "ns1.example.com.", 1, 1.0, nil)
|
||||
// Do NOT set addresses - this exercises processReferral's no-address path
|
||||
|
||||
cache := NewInfoCache(nil)
|
||||
// Use expired context for resolveGlueViaSystem so it returns nil fast
|
||||
bgCtx := context.Background()
|
||||
resp := tr.processReferral(bgCtx, ref, cache)
|
||||
// Result may vary depending on whether 127.0.0.1:53 is available,
|
||||
// but the function should not panic.
|
||||
t.Logf("processReferral result type: %v", resp.Type)
|
||||
}
|
||||
|
||||
func TestReferralResolveAlreadyHasAddresses(t *testing.T) {
|
||||
ref := NewReferral("example.com.", dnsinternal.TypeA, ".", 0, 1.0, nil)
|
||||
ref.Addresses = []net.IP{net.ParseIP("1.2.3.4")}
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err := ref.Resolve(ctx, tr, nil, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve with existing addresses: %v", err)
|
||||
}
|
||||
if ref.State != StateResolved {
|
||||
t.Errorf("expected StateResolved, got %v", ref.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferralResolveCacheHit(t *testing.T) {
|
||||
ref := NewReferral("ns1.example.com.", dnsinternal.TypeA, ".", 0, 1.0, nil)
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
cache := NewInfoCache(nil)
|
||||
cache.StoreGlue("ns1.example.com.", []net.IP{net.ParseIP("1.2.3.4")})
|
||||
|
||||
ctx := context.Background()
|
||||
err := ref.Resolve(ctx, tr, cache, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve cache hit: %v", err)
|
||||
}
|
||||
if ref.State != StateResolved {
|
||||
t.Errorf("expected StateResolved, got %v", ref.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferralResolveCircular(t *testing.T) {
|
||||
ref := NewReferral("ns1.example.com.", dnsinternal.TypeA, ".", 0, 1.0, nil)
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
visited := map[string]bool{"ns1.example.com.": true}
|
||||
ctx := context.Background()
|
||||
err := ref.Resolve(ctx, tr, nil, visited, 0)
|
||||
if err == nil {
|
||||
t.Fatal("expected circular referral error")
|
||||
}
|
||||
if _, ok := err.(*CircularReferralError); !ok {
|
||||
t.Errorf("expected CircularReferralError, got %T: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferralResolveMaxDepth(t *testing.T) {
|
||||
ref := NewReferral("ns1.example.com.", dnsinternal.TypeA, ".", 0, 1.0, nil)
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err := ref.Resolve(ctx, tr, nil, nil, DefaultMaxDepth+1)
|
||||
if err == nil {
|
||||
t.Fatal("expected max depth error")
|
||||
}
|
||||
if _, ok := err.(*UnresolvableNameserverError); !ok {
|
||||
t.Errorf("expected UnresolvableNameserverError, got %T: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferralResolveWithAnswer(t *testing.T) {
|
||||
answerMsg := new(dns.Msg)
|
||||
answerMsg.SetReply(new(dns.Msg))
|
||||
answerMsg.Answer = append(answerMsg.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: "ns1.example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
})
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return answerMsg.Copy(), nil
|
||||
})
|
||||
|
||||
ref := NewReferral("ns1.example.com.", dnsinternal.TypeA, ".", 0, 1.0, nil)
|
||||
ctx := context.Background()
|
||||
err := ref.Resolve(ctx, tr, nil, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve: %v", err)
|
||||
}
|
||||
if ref.State != StateResolved {
|
||||
t.Errorf("expected StateResolved, got %v", ref.State)
|
||||
}
|
||||
if len(ref.Addresses) == 0 {
|
||||
t.Error("expected addresses after resolution")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferralResolveNXDOMAIN(t *testing.T) {
|
||||
nxMsg := new(dns.Msg)
|
||||
nxMsg.Rcode = dns.RcodeNameError
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return nxMsg.Copy(), nil
|
||||
})
|
||||
|
||||
ref := NewReferral("nonexistent.invalid.", dnsinternal.TypeA, ".", 0, 1.0, nil)
|
||||
ctx := context.Background()
|
||||
err := ref.Resolve(ctx, tr, nil, nil, 0)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for NXDOMAIN")
|
||||
}
|
||||
if _, ok := err.(*UnresolvableNameserverError); !ok {
|
||||
t.Errorf("expected UnresolvableNameserverError, got %T: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferralResolveContextCancellation(t *testing.T) {
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
refMsg := new(dns.Msg)
|
||||
refMsg.Rcode = dns.RcodeSuccess
|
||||
refMsg.Ns = append(refMsg.Ns, &dns.NS{
|
||||
Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeNS},
|
||||
Ns: "ns.example.com.",
|
||||
})
|
||||
refMsg.Extra = append(refMsg.Extra, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: "ns.example.com.", Rrtype: dns.TypeA},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
})
|
||||
return refMsg, nil
|
||||
})
|
||||
|
||||
ref := NewReferral("ns1.example.com.", dnsinternal.TypeA, ".", 0, 1.0, nil)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
err := ref.Resolve(ctx, tr, nil, nil, 0)
|
||||
if err == nil {
|
||||
t.Fatal("expected error on cancelled context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferralResolveReferralPath(t *testing.T) {
|
||||
// Test Resolve when it gets a referral response that pushes to stack
|
||||
referralMsg := new(dns.Msg)
|
||||
referralMsg.Rcode = dns.RcodeSuccess
|
||||
referralMsg.Ns = append(referralMsg.Ns, &dns.NS{
|
||||
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeNS, Class: dns.ClassINET},
|
||||
Ns: "ns1.example.com.",
|
||||
})
|
||||
referralMsg.Extra = append(referralMsg.Extra, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: "ns1.example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
})
|
||||
|
||||
answerMsg := new(dns.Msg)
|
||||
answerMsg.SetReply(new(dns.Msg))
|
||||
answerMsg.Answer = append(answerMsg.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: "ns1.example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||
A: net.ParseIP("5.6.7.8"),
|
||||
})
|
||||
|
||||
callCount := 0
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
callCount++
|
||||
if callCount <= 1 {
|
||||
return referralMsg.Copy(), nil
|
||||
}
|
||||
return answerMsg.Copy(), nil
|
||||
})
|
||||
|
||||
ref := NewReferral("ns1.example.com.", dnsinternal.TypeA, ".", 0, 1.0, nil)
|
||||
ctx := context.Background()
|
||||
err := ref.Resolve(ctx, tr, nil, nil, 0)
|
||||
// May succeed or exhaust depending on referral loop
|
||||
t.Logf("Resolve referral path: err=%v, state=%v", err, ref.State)
|
||||
}
|
||||
|
||||
func TestResolutionStateStringUnknown(t *testing.T) {
|
||||
// Cover the default case of ResolutionState.String()
|
||||
unknown := ResolutionState(99)
|
||||
s := unknown.String()
|
||||
if s != "unknown" {
|
||||
t.Errorf("expected 'unknown' for invalid ResolutionState, got %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraverserNonFastMode(t *testing.T) {
|
||||
answerResp := new(dns.Msg)
|
||||
answerResp.SetReply(new(dns.Msg))
|
||||
answerResp.Answer = append(answerResp.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"),
|
||||
})
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
Fast: false, // Non-fast mode
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return answerResp.Copy(), nil
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
results, err := tr.Traverse(ctx, "example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Traverse non-fast: %v", err)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
t.Fatal("expected results")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIterativeQueryWithExchangeUsesConfig(t *testing.T) {
|
||||
answerMsg := new(dns.Msg)
|
||||
answerMsg.SetReply(new(dns.Msg))
|
||||
answerMsg.Answer = append(answerMsg.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"),
|
||||
})
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
QueryConfig: dnsinternal.DefaultQueryConfig(),
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return answerMsg.Copy(), nil
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
msg, err := tr.iterativeQueryWithExchange(ctx, net.ParseIP("198.41.0.4"), "example.com.", dnsinternal.TypeA)
|
||||
if err != nil {
|
||||
t.Fatalf("iterativeQueryWithExchange with config: %v", err)
|
||||
}
|
||||
if msg == nil {
|
||||
t.Fatal("expected non-nil response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraverserReferralWithHooks(t *testing.T) {
|
||||
// Tests that hooks are called with IsResolve=true during ResolveNS sub-traversal
|
||||
answerMsg := new(dns.Msg)
|
||||
answerMsg.SetReply(new(dns.Msg))
|
||||
answerMsg.Answer = append(answerMsg.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"),
|
||||
})
|
||||
|
||||
tr := NewTraverser(&TraverserConfig{
|
||||
MaxDepth: 5,
|
||||
QueryType: dnsinternal.TypeA,
|
||||
RootAddrs: []net.IP{net.ParseIP("198.41.0.4")},
|
||||
})
|
||||
tr.SetExchange(func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error) {
|
||||
return answerMsg.Copy(), nil
|
||||
})
|
||||
|
||||
var resolveEvents, progressEvents int
|
||||
tr.SetHooks(&TraverserHooks{
|
||||
OnEvent: func(e TraversalEvent) {
|
||||
if e.IsResolve {
|
||||
resolveEvents++
|
||||
} else {
|
||||
progressEvents++
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Test directly via ResolveNS with hooks
|
||||
ctx := context.Background()
|
||||
addrs, err := tr.ResolveNS(ctx, "ns1.example.com.", nil, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveNS: %v", err)
|
||||
}
|
||||
_ = addrs
|
||||
t.Logf("resolveEvents=%d progressEvents=%d", resolveEvents, progressEvents)
|
||||
}
|
||||
Reference in New Issue
Block a user