CI / test (pull_request) Failing after 2m40s
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>
204 lines
4.9 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|