// Package resolver_test verifies the iterative resolver against // live DNS. Per TESTING.md, all resolver tests use real queries // against real nameservers — no mocking of the DNS client layer. // Tests target long-lived public infrastructure (root servers, // gTLD servers, IANA, Google, Cloudflare, GitHub) to minimize // fixture churn, plus dedicated fixture records in the // sneak.cloud zone. package resolver_test import ( "context" "log/slog" "net" "os" "sort" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/dnswatcher/internal/resolver" ) // ---------------------------------------------------------------- // Test fixtures // ---------------------------------------------------------------- // Fixture hostnames in the sneak.cloud zone (hosted on // Cloudflare). These records must exist in the sneak.cloud zone: // // basic.dns.sneak.cloud A 192.0.2.1 // multi.dns.sneak.cloud A 192.0.2.1 // multi.dns.sneak.cloud A 192.0.2.2 // ipv6.dns.sneak.cloud AAAA 2001:db8::1 // dual.dns.sneak.cloud A 192.0.2.1 // dual.dns.sneak.cloud AAAA 2001:db8::1 // cname-target.dns.sneak.cloud A 198.51.100.1 // cname.dns.sneak.cloud CNAME cname-target.dns.sneak.cloud const ( testDomain = "sneak.cloud" testHostBasic = "basic.dns.sneak.cloud" testHostMultiA = "multi.dns.sneak.cloud" testHostIPv6 = "ipv6.dns.sneak.cloud" testHostDualStack = "dual.dns.sneak.cloud" testHostCNAME = "cname.dns.sneak.cloud" ) // ---------------------------------------------------------------- // Test helpers // ---------------------------------------------------------------- func newTestResolver(t *testing.T) *resolver.Resolver { t.Helper() log := slog.New(slog.NewTextHandler( os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}, )) return resolver.NewFromLogger(log) } func testContext(t *testing.T) context.Context { t.Helper() ctx, cancel := context.WithTimeout( context.Background(), 60*time.Second, ) t.Cleanup(cancel) return ctx } func findOneNSForDomain( t *testing.T, r *resolver.Resolver, ctx context.Context, //nolint:revive // test helper domain string, ) string { t.Helper() nameservers, err := r.FindAuthoritativeNameservers( ctx, domain, ) require.NoError(t, err) require.NotEmpty(t, nameservers) return nameservers[0] } // ---------------------------------------------------------------- // FindAuthoritativeNameservers tests // ---------------------------------------------------------------- func TestFindAuthoritativeNameservers_ValidDomain( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) nameservers, err := r.FindAuthoritativeNameservers( ctx, "google.com", ) require.NoError(t, err) require.NotEmpty(t, nameservers) hasGoogleNS := false for _, ns := range nameservers { if strings.Contains(ns, "google") { hasGoogleNS = true break } } assert.True(t, hasGoogleNS, "expected google nameservers, got: %v", nameservers, ) } func TestFindAuthoritativeNameservers_Subdomain( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) nameservers, err := r.FindAuthoritativeNameservers( ctx, "www.google.com", ) require.NoError(t, err) require.NotEmpty(t, nameservers) } func TestFindAuthoritativeNameservers_TLD(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) nameservers, err := r.FindAuthoritativeNameservers( ctx, "com", ) require.NoError(t, err) require.NotEmpty(t, nameservers, "should find TLD nameservers", ) hasGTLD := false for _, ns := range nameservers { if strings.Contains(ns, "gtld-servers.net") { hasGTLD = true break } } assert.True(t, hasGTLD, "expected gtld-servers for com, got: %v", nameservers, ) } func TestFindAuthoritativeNameservers_ReturnsSorted( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) nameservers, err := r.FindAuthoritativeNameservers( ctx, "google.com", ) require.NoError(t, err) assert.True( t, sort.StringsAreSorted(nameservers), "nameservers should be sorted, got: %v", nameservers, ) } func TestFindAuthoritativeNameservers_Deterministic( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) first, err := r.FindAuthoritativeNameservers( ctx, "google.com", ) require.NoError(t, err) second, err := r.FindAuthoritativeNameservers( ctx, "google.com", ) require.NoError(t, err) assert.Equal(t, first, second) } func TestFindAuthoritativeNameservers_TrailingDot( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns1, err := r.FindAuthoritativeNameservers( ctx, "google.com", ) require.NoError(t, err) ns2, err := r.FindAuthoritativeNameservers( ctx, "google.com.", ) require.NoError(t, err) assert.Equal(t, ns1, ns2) } func TestFindAuthoritativeNameservers_CloudflareDomain( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) nameservers, err := r.FindAuthoritativeNameservers( ctx, "cloudflare.com", ) require.NoError(t, err) require.NotEmpty(t, nameservers) for _, ns := range nameservers { assert.True(t, strings.HasSuffix(ns, "."), "NS should be FQDN with trailing dot: %s", ns, ) } } func TestFindAuthoritativeNameservers_AllNSRespondAuthoritatively( t *testing.T, ) { // Every NS returned by FindAuthoritativeNameservers must // actually serve the zone. A lame NS would return REFUSED, // SERVFAIL, or a non-authoritative answer. t.Parallel() r := newTestResolver(t) ctx := testContext(t) nameservers, err := r.FindAuthoritativeNameservers( ctx, "google.com", ) require.NoError(t, err) require.NotEmpty(t, nameservers) for _, ns := range nameservers { resp, queryErr := r.QueryNameserver( ctx, ns, "www.google.com", ) require.NoError(t, queryErr, "NS %s should be queryable", ns, ) assert.Equal( t, resolver.StatusOK, resp.Status, "NS %s should respond authoritatively, "+ "status=%s error=%s", ns, resp.Status, resp.Error, ) hasRecords := len(resp.Records["A"]) > 0 || len(resp.Records["CNAME"]) > 0 assert.True(t, hasRecords, "NS %s should return records for www.google.com", ns, ) } } func TestFindAuthoritativeNameservers_IsIterative( t *testing.T, ) { // Verify that resolution works for well-known domains, // proving we trace from root rather than relying on a // system stub resolver that might not be configured. t.Parallel() r := newTestResolver(t) ctx := testContext(t) // Resolve a well-known domain to prove root->TLD->domain // tracing works. nameservers, err := r.FindAuthoritativeNameservers( ctx, "example.com", ) require.NoError(t, err) require.NotEmpty(t, nameservers) t.Logf("example.com NS: %v", nameservers) } // ---------------------------------------------------------------- // QueryNameserver tests // ---------------------------------------------------------------- func TestQueryNameserver_BasicA(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, "google.com") resp, err := r.QueryNameserver( ctx, ns, "www.google.com", ) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, resolver.StatusOK, resp.Status) assert.Equal(t, ns, resp.Nameserver) hasRecords := len(resp.Records["A"]) > 0 || len(resp.Records["CNAME"]) > 0 assert.True(t, hasRecords, "expected A or CNAME records for www.google.com", ) } func TestQueryNameserver_MultipleA(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, testDomain) resp, err := r.QueryNameserver(ctx, ns, testHostMultiA) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, resolver.StatusOK, resp.Status) aRecords := resp.Records["A"] require.Len( t, aRecords, 2, "multi.dns should have exactly 2 A records", ) sort.Strings(aRecords) assert.Equal(t, []string{"192.0.2.1", "192.0.2.2"}, aRecords) } func TestQueryNameserver_AAAA(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, "cloudflare.com") resp, err := r.QueryNameserver( ctx, ns, "cloudflare.com", ) require.NoError(t, err) aaaaRecords := resp.Records["AAAA"] require.NotEmpty(t, aaaaRecords, "cloudflare.com should have AAAA records", ) for _, ip := range aaaaRecords { parsed := net.ParseIP(ip) require.NotNil(t, parsed, "should be valid IP: %s", ip, ) } } func TestQueryNameserver_DualStack(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, testDomain) resp, err := r.QueryNameserver(ctx, ns, testHostDualStack) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, resolver.StatusOK, resp.Status) assert.Contains(t, resp.Records["A"], "192.0.2.1") assert.Contains(t, resp.Records["AAAA"], "2001:db8::1") } func TestQueryNameserver_CNAME(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, testDomain) resp, err := r.QueryNameserver(ctx, ns, testHostCNAME) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, resolver.StatusOK, resp.Status) cnameRecords := resp.Records["CNAME"] require.NotEmpty( t, cnameRecords, "cname.dns should have CNAME records", ) assert.Contains( t, cnameRecords, "cname-target.dns.sneak.cloud.", ) } func TestQueryNameserver_MX(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, "google.com") resp, err := r.QueryNameserver( ctx, ns, "google.com", ) require.NoError(t, err) mxRecords := resp.Records["MX"] require.NotEmpty(t, mxRecords, "google.com should have MX records", ) } func TestQueryNameserver_TXT(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, "google.com") resp, err := r.QueryNameserver( ctx, ns, "google.com", ) require.NoError(t, err) txtRecords := resp.Records["TXT"] require.NotEmpty(t, txtRecords, "google.com should have TXT records", ) hasSPF := false for _, txt := range txtRecords { if strings.Contains(txt, "v=spf1") { hasSPF = true break } } assert.True(t, hasSPF, "google.com should have SPF TXT record", ) } func TestQueryNameserver_NXDomain(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, "google.com") resp, err := r.QueryNameserver( ctx, ns, "this-surely-does-not-exist-xyz.google.com", ) require.NoError(t, err) assert.Equal(t, resolver.StatusNXDomain, resp.Status) } func TestQueryNameserver_RecordsSorted(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, "google.com") resp, err := r.QueryNameserver( ctx, ns, "google.com", ) require.NoError(t, err) for recordType, values := range resp.Records { assert.True( t, sort.StringsAreSorted(values), "%s records should be sorted", recordType, ) } } func TestQueryNameserver_ResponseIncludesNameserver( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, "cloudflare.com") resp, err := r.QueryNameserver( ctx, ns, "cloudflare.com", ) require.NoError(t, err) assert.Equal(t, ns, resp.Nameserver) } func TestQueryNameserver_EmptyRecordsOnNXDomain( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, "google.com") resp, err := r.QueryNameserver( ctx, ns, "this-surely-does-not-exist-xyz.google.com", ) require.NoError(t, err) totalRecords := 0 for _, values := range resp.Records { totalRecords += len(values) } assert.Zero(t, totalRecords) } func TestQueryNameserver_TrailingDotHandling(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, "google.com") resp1, err := r.QueryNameserver( ctx, ns, "google.com", ) require.NoError(t, err) resp2, err := r.QueryNameserver( ctx, ns, "google.com.", ) require.NoError(t, err) assert.Equal(t, resp1.Status, resp2.Status) } func TestQueryNameserver_NSRecordsForZoneApex( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, "cloudflare.com") // Querying a zone apex should return NS records. resp, err := r.QueryNameserver( ctx, ns, "cloudflare.com", ) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, resolver.StatusOK, resp.Status) nsRecords := resp.Records["NS"] require.NotEmpty(t, nsRecords, "zone apex should have NS records", ) hasCloudflare := false for _, nsRecord := range nsRecords { if strings.Contains(nsRecord, "cloudflare") { hasCloudflare = true break } } assert.True(t, hasCloudflare, "NS records should include cloudflare, got: %v", nsRecords, ) } func TestQueryNameserver_NoNSRecordsForLeafHostname( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ns := findOneNSForDomain(t, r, ctx, "google.com") // A leaf hostname (not a zone cut) has no NS records. resp, err := r.QueryNameserver( ctx, ns, "www.google.com", ) require.NoError(t, err) require.NotNil(t, resp) assert.Empty(t, resp.Records["NS"], "leaf hostname should not have NS records, got: %v", resp.Records["NS"], ) } func TestQueryNameserver_LameNameserver(t *testing.T) { // Querying a nameserver that does not serve a zone should // return a non-ok status, not silently return empty results // with status "ok". a.iana-servers.net serves example.com, // not google.com. t.Parallel() r := newTestResolver(t) ctx := testContext(t) resp, err := r.QueryNameserver( ctx, "a.iana-servers.net.", "www.google.com", ) require.NoError(t, err, "should not return Go-level error") require.NotNil(t, resp) assert.NotEqual( t, resolver.StatusOK, resp.Status, "lame NS should not return status ok; got status=%s "+ "records=%v", resp.Status, resp.Records, ) } // ---------------------------------------------------------------- // QueryNameserverIP tests // ---------------------------------------------------------------- func TestQueryNameserverIP_BasicA(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) // a.iana-servers.net (199.43.135.53) is authoritative for // example.com; query it directly by IP. resp, err := r.QueryNameserverIP( ctx, "a.iana-servers.net.", "199.43.135.53", "example.com", ) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, resolver.StatusOK, resp.Status) assert.Equal(t, "a.iana-servers.net.", resp.Nameserver) assert.NotEmpty(t, resp.Records["A"], "example.com should have A records", ) } // ---------------------------------------------------------------- // QueryAllNameservers tests // ---------------------------------------------------------------- func TestQueryAllNameservers_ReturnsAllNS(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) results, err := r.QueryAllNameservers( ctx, "google.com", ) require.NoError(t, err) require.NotEmpty(t, results) assert.GreaterOrEqual(t, len(results), 2) for ns, resp := range results { assert.Equal(t, ns, resp.Nameserver) } } func TestQueryAllNameservers_AllReturnOK(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) results, err := r.QueryAllNameservers( ctx, "google.com", ) require.NoError(t, err) for ns, resp := range results { assert.Equal( t, resolver.StatusOK, resp.Status, "NS %s should return OK", ns, ) } } func TestQueryAllNameservers_Consistent(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) results, err := r.QueryAllNameservers(ctx, testHostBasic) require.NoError(t, err) require.NotEmpty(t, results) // All NSes should return the same A records for a // well-configured hostname. var referenceRecords map[string][]string for ns, resp := range results { require.Equal( t, resolver.StatusOK, resp.Status, "NS %s should return OK status", ns, ) if referenceRecords == nil { referenceRecords = resp.Records continue } assert.Equal( t, referenceRecords["A"], resp.Records["A"], "NS %s A records should match", ns, ) } } func TestQueryAllNameservers_NXDomainFromAllNS( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) results, err := r.QueryAllNameservers( ctx, "this-surely-does-not-exist-xyz.google.com", ) require.NoError(t, err) for ns, resp := range results { assert.Equal( t, resolver.StatusNXDomain, resp.Status, "NS %s should return nxdomain", ns, ) } } // ---------------------------------------------------------------- // LookupNS tests // ---------------------------------------------------------------- func TestLookupNS_ValidDomain(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) nameservers, err := r.LookupNS(ctx, "google.com") require.NoError(t, err) require.NotEmpty(t, nameservers) for _, ns := range nameservers { assert.True(t, strings.HasSuffix(ns, "."), "NS should have trailing dot: %s", ns, ) } } func TestLookupNS_Sorted(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) nameservers, err := r.LookupNS(ctx, "google.com") require.NoError(t, err) assert.True(t, sort.StringsAreSorted(nameservers)) } func TestLookupNS_MatchesFindAuthoritative(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) fromLookup, err := r.LookupNS(ctx, "google.com") require.NoError(t, err) fromFind, err := r.FindAuthoritativeNameservers( ctx, "google.com", ) require.NoError(t, err) assert.Equal(t, fromFind, fromLookup) } // ---------------------------------------------------------------- // ResolveIPAddresses tests // ---------------------------------------------------------------- func TestResolveIPAddresses_ReturnsIPs(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ips, err := r.ResolveIPAddresses(ctx, "google.com") require.NoError(t, err) require.NotEmpty(t, ips) for _, ip := range ips { parsed := net.ParseIP(ip) assert.NotNil(t, parsed, "should be valid IP: %s", ip, ) } } func TestResolveIPAddresses_BasicA(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ips, err := r.ResolveIPAddresses(ctx, testHostBasic) require.NoError(t, err) require.NotEmpty(t, ips) assert.Contains(t, ips, "192.0.2.1") } func TestResolveIPAddresses_MultipleA(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ips, err := r.ResolveIPAddresses(ctx, testHostMultiA) require.NoError(t, err) sort.Strings(ips) assert.Contains(t, ips, "192.0.2.1") assert.Contains(t, ips, "192.0.2.2") } func TestResolveIPAddresses_IPv6Only(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ips, err := r.ResolveIPAddresses(ctx, testHostIPv6) require.NoError(t, err) require.NotEmpty(t, ips) assert.Contains(t, ips, "2001:db8::1") // Should not contain any IPv4 for _, ip := range ips { parsed := net.ParseIP(ip) require.NotNil(t, parsed, "should be valid IP: %s", ip) assert.Nil( t, parsed.To4(), "ipv6-only host should not return IPv4: %s", ip, ) } } func TestResolveIPAddresses_DualStack(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ips, err := r.ResolveIPAddresses(ctx, testHostDualStack) require.NoError(t, err) assert.Contains(t, ips, "192.0.2.1") assert.Contains(t, ips, "2001:db8::1") } func TestResolveIPAddresses_FollowsCNAME(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) // www.github.com is a long-standing CNAME to github.com; // resolution must follow the chain to A records. ips, err := r.ResolveIPAddresses(ctx, "www.github.com") require.NoError(t, err) require.NotEmpty(t, ips) } func TestResolveIPAddresses_Deduplicated(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ips, err := r.ResolveIPAddresses(ctx, "google.com") require.NoError(t, err) seen := make(map[string]bool) for _, ip := range ips { assert.False(t, seen[ip], "duplicate IP: %s", ip) seen[ip] = true } } func TestResolveIPAddresses_Sorted(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ips, err := r.ResolveIPAddresses(ctx, "google.com") require.NoError(t, err) assert.True(t, sort.StringsAreSorted(ips)) } func TestResolveIPAddresses_NXDomainReturnsEmpty( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ips, err := r.ResolveIPAddresses( ctx, "this-surely-does-not-exist-xyz.google.com", ) require.NoError(t, err) assert.Empty(t, ips) } func TestResolveIPAddresses_CloudflareDomain(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx := testContext(t) ips, err := r.ResolveIPAddresses(ctx, "cloudflare.com") require.NoError(t, err) require.NotEmpty(t, ips) } // ---------------------------------------------------------------- // Context cancellation tests // ---------------------------------------------------------------- func TestFindAuthoritativeNameservers_ContextCanceled( t *testing.T, ) { t.Parallel() r := newTestResolver(t) ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := r.FindAuthoritativeNameservers(ctx, "google.com") assert.Error(t, err) } func TestQueryNameserver_ContextCanceled(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := r.QueryNameserver( ctx, "ns1.google.com.", "google.com", ) assert.Error(t, err) } func TestQueryAllNameservers_ContextCanceled(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := r.QueryAllNameservers(ctx, "google.com") assert.Error(t, err) } func TestResolveIPAddresses_ContextCanceled(t *testing.T) { t.Parallel() r := newTestResolver(t) ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := r.ResolveIPAddresses(ctx, "google.com") assert.Error(t, err) }