Files
ExploreDNS/internal/traverse/cache_test.go
T
Garyandmultica-agent 2835ee86cf
CI / test (pull_request) Failing after 2m40s
feat: implement Phase 2.3 DNS traversal engine
Add core traversal engine with iterative resolution from root servers:
- referral.go: Referral struct with name, type, bailiwick, addresses, state
- cache.go: Chained InfoCache with parent inheritance for NS and glue records
- stack.go: LIFO stack with configurable max depth enforcement
- response.go: Response classifier (referral, answer, CNAME follow, NODATA,
  NXDOMAIN, SERVFAIL) with child referral generation and probability splitting
- traverser.go: Traverser orchestrates full traversal from root to leaf,
  following all referral branches, handling CNAME chains, and respecting
  max depth limits

Add IterativeQuery/IterativeQueryWithExchange to dns package (RD=false queries).

57 new tests covering all acceptance criteria: traversal from root to leaf,
comprehensive branch following, max depth enforcement, probability distribution,
chained cache behavior, CNAME following, NXDOMAIN/SERVFAIL handling, context
cancellation, and concurrent access.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-06 13:23:01 +10:00

204 lines
4.9 KiB
Go

package traverse
import (
"fmt"
"net"
"strings"
"sync"
"testing"
"github.com/miekg/dns"
)
func TestNewInfoCache(t *testing.T) {
c := NewInfoCache(nil)
if c.parent != nil {
t.Error("root cache should have nil parent")
}
if c.NSCount() != 0 {
t.Errorf("NSCount = %d, want 0", c.NSCount())
}
if c.GlueCount() != 0 {
t.Errorf("GlueCount = %d, want 0", c.GlueCount())
}
}
func TestInfoCacheStoreAndLookupNS(t *testing.T) {
c := NewInfoCache(nil)
c.StoreNS("com.", []string{"a.gtld-servers.net.", "b.gtld-servers.net."})
if c.NSCount() != 1 {
t.Errorf("NSCount = %d, want 1", c.NSCount())
}
names := c.LookupNS("com.")
if len(names) != 2 {
t.Fatalf("expected 2 nameservers, got %d", len(names))
}
if names[0] != "a.gtld-servers.net." {
t.Errorf("nameserver[0] = %q, want %q", names[0], "a.gtld-servers.net.")
}
}
func TestInfoCacheNSDedup(t *testing.T) {
c := NewInfoCache(nil)
c.StoreNS("com.", []string{"a.gtld-servers.net.", "a.gtld-servers.net."})
names := c.LookupNS("com.")
if len(names) != 1 {
t.Errorf("expected 1 deduped NS, got %d", len(names))
}
}
func TestInfoCacheNSCaseInsensitive(t *testing.T) {
c := NewInfoCache(nil)
c.StoreNS("COM.", []string{"A.GTLD-SERVERS.NET."})
names := c.LookupNS("com.")
if len(names) != 1 {
t.Fatalf("expected 1 NS, got %d", len(names))
}
if names[0] != "a.gtld-servers.net." {
t.Errorf("NS = %q, want %q", names[0], "a.gtld-servers.net.")
}
}
func TestInfoCacheNSLookupMiss(t *testing.T) {
c := NewInfoCache(nil)
names := c.LookupNS("org.")
if names != nil {
t.Errorf("expected nil for miss, got %v", names)
}
}
func TestInfoCacheNSStoreEmpty(t *testing.T) {
c := NewInfoCache(nil)
c.StoreNS("com.", nil)
if c.NSCount() != 0 {
t.Errorf("expected 0 after empty store, got %d", c.NSCount())
}
}
func TestInfoCacheChainedNS(t *testing.T) {
parent := NewInfoCache(nil)
parent.StoreNS("com.", []string{"a.gtld-servers.net."})
child := parent.Child()
if child.parent != parent {
t.Error("child parent should be the parent cache")
}
names := child.LookupNS("com.")
if len(names) != 1 {
t.Fatalf("expected 1 NS from parent, got %d", len(names))
}
if child.NSCount() != 0 {
t.Errorf("child NSCount = %d, want 0", child.NSCount())
}
}
func TestInfoCacheChildOverridesParent(t *testing.T) {
parent := NewInfoCache(nil)
parent.StoreNS("com.", []string{"a.gtld-servers.net."})
child := parent.Child()
child.StoreNS("com.", []string{"b.gtld-servers.net."})
names := child.LookupNS("com.")
if len(names) != 1 {
t.Fatalf("expected 1 NS, got %d", len(names))
}
if names[0] != "b.gtld-servers.net." {
t.Errorf("expected child's NS to override, got %q", names[0])
}
}
func TestInfoCacheStoreAndLookupGlue(t *testing.T) {
c := NewInfoCache(nil)
addrs := []net.IP{net.ParseIP("1.2.3.4"), net.ParseIP("5.6.7.8")}
c.StoreGlue("ns1.example.com.", addrs)
result := c.LookupGlue("ns1.example.com.")
if len(result) != 2 {
t.Fatalf("expected 2 glue addresses, got %d", len(result))
}
}
func TestInfoCacheGlueDedup(t *testing.T) {
c := NewInfoCache(nil)
ip := net.ParseIP("1.2.3.4")
c.StoreGlue("ns1.example.com.", []net.IP{ip, ip})
result := c.LookupGlue("ns1.example.com.")
if len(result) != 1 {
t.Errorf("expected 1 deduped glue, got %d", len(result))
}
}
func TestInfoCacheChainedGlue(t *testing.T) {
parent := NewInfoCache(nil)
parent.StoreGlue("ns1.example.com.", []net.IP{net.ParseIP("1.2.3.4")})
child := parent.Child()
result := child.LookupGlue("ns1.example.com.")
if len(result) != 1 {
t.Fatalf("expected 1 glue from parent, got %d", len(result))
}
if child.GlueCount() != 0 {
t.Errorf("child GlueCount = %d, want 0", child.GlueCount())
}
}
func TestInfoCacheGlueLookupMiss(t *testing.T) {
c := NewInfoCache(nil)
result := c.LookupGlue("nonexistent.example.com.")
if result != nil {
t.Errorf("expected nil for miss, got %v", result)
}
}
func TestInfoCacheConcurrentAccess(t *testing.T) {
c := NewInfoCache(nil)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
name := strings.ToLower(dns.Fqdn(fmt.Sprintf("ns%d.example.com.", i)))
c.StoreNS("example.com.", []string{name})
c.StoreGlue(name, []net.IP{net.ParseIP(fmt.Sprintf("1.2.3.%d", i%256))})
_ = c.LookupNS("example.com.")
_ = c.LookupGlue(name)
}(i)
}
wg.Wait()
}
func TestInfoCacheNilParent(t *testing.T) {
c := NewInfoCache(nil)
if c.LookupNS("com.") != nil {
t.Error("root cache should return nil for miss")
}
if c.LookupGlue("ns.example.com.") != nil {
t.Error("root cache should return nil for glue miss")
}
}
func TestNormalize(t *testing.T) {
tests := []struct {
input string
want string
}{
{"example.com", "example.com."},
{"Example.COM.", "example.com."},
{"EXAMPLE.COM", "example.com."},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := normalize(tt.input)
if got != tt.want {
t.Errorf("normalize(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}