1 Commits

Author SHA1 Message Date
clawbot
d0220e5814 fix: remove ErrNotImplemented stub — resolver, port, and TLS checks are fully implemented (closes #16)
Some checks failed
Check / check (pull_request) Failing after 5m27s
The ErrNotImplemented sentinel error was dead code left over from
initial scaffolding. The resolver performs real iterative DNS
lookups from root servers, PortCheck does TCP connection checks,
and TLSCheck verifies TLS certificates and expiry. Removed the
unused error constant.
2026-02-21 00:55:38 -08:00
3 changed files with 50 additions and 550 deletions

View File

@@ -4,11 +4,6 @@ import "errors"
// Sentinel errors returned by the resolver.
var (
// ErrNotImplemented indicates a method is stubbed out.
ErrNotImplemented = errors.New(
"resolver not yet implemented",
)
// ErrNoNameservers is returned when no authoritative NS
// could be discovered for a domain.
ErrNoNameservers = errors.New(

View File

@@ -2,7 +2,6 @@ package resolver_test
import (
"context"
"fmt"
"log/slog"
"net"
"os"
@@ -11,504 +10,12 @@ import (
"testing"
"time"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/dnswatcher/internal/resolver"
)
// ----------------------------------------------------------------
// Mock DNS client
// ----------------------------------------------------------------
// mockDNSClient implements resolver.DNSClient with canned responses.
type mockDNSClient struct {
handlers map[string]func(msg *dns.Msg) *dns.Msg
}
func newMockClient() *mockDNSClient {
return &mockDNSClient{
handlers: make(map[string]func(msg *dns.Msg) *dns.Msg),
}
}
func (m *mockDNSClient) ExchangeContext(
ctx context.Context,
msg *dns.Msg,
addr string,
) (*dns.Msg, time.Duration, error) {
err := ctx.Err()
if err != nil {
return nil, 0, err
}
host, _, _ := net.SplitHostPort(addr)
if host == "" {
host = addr
}
qname := msg.Question[0].Name
qtype := dns.TypeToString[msg.Question[0].Qtype]
resp := m.findHandler(host, qname, qtype, msg)
return resp, time.Millisecond, nil
}
func (m *mockDNSClient) findHandler(
host, qname, qtype string,
msg *dns.Msg,
) *dns.Msg {
key := fmt.Sprintf(
"%s|%s|%s", host, strings.ToLower(qname), qtype,
)
if h, ok := m.handlers[key]; ok {
return h(msg)
}
wildKey := fmt.Sprintf(
"*|%s|%s", strings.ToLower(qname), qtype,
)
if h, ok := m.handlers[wildKey]; ok {
return h(msg)
}
resp := new(dns.Msg)
resp.SetReply(msg)
return resp
}
func (m *mockDNSClient) on(
server, qname, qtype string,
handler func(msg *dns.Msg) *dns.Msg,
) {
key := fmt.Sprintf(
"%s|%s|%s",
server, dns.Fqdn(strings.ToLower(qname)), qtype,
)
m.handlers[key] = handler
}
// ----------------------------------------------------------------
// Response builders
// ----------------------------------------------------------------
func referralResponse(
msg *dns.Msg,
nsNames []string,
glue map[string]string,
) *dns.Msg {
resp := new(dns.Msg)
resp.SetReply(msg)
for _, ns := range nsNames {
resp.Ns = append(resp.Ns, &dns.NS{
Hdr: dns.RR_Header{
Name: msg.Question[0].Name,
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
Ttl: 3600,
},
Ns: dns.Fqdn(ns),
})
}
for name, ip := range glue {
resp.Extra = append(resp.Extra, &dns.A{
Hdr: dns.RR_Header{
Name: dns.Fqdn(name),
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 3600,
},
A: net.ParseIP(ip),
})
}
return resp
}
func nsAnswerResponse(
msg *dns.Msg, nsNames []string,
) *dns.Msg {
resp := new(dns.Msg)
resp.SetReply(msg)
for _, ns := range nsNames {
resp.Answer = append(resp.Answer, &dns.NS{
Hdr: dns.RR_Header{
Name: msg.Question[0].Name,
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
Ttl: 3600,
},
Ns: dns.Fqdn(ns),
})
}
return resp
}
func nxdomainResponse(msg *dns.Msg) *dns.Msg {
resp := new(dns.Msg)
resp.SetReply(msg)
resp.Rcode = dns.RcodeNameError
return resp
}
func aResponse(
msg *dns.Msg, name string, ip string,
) *dns.Msg {
resp := new(dns.Msg)
resp.SetReply(msg)
resp.Answer = append(resp.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: dns.Fqdn(name), Rrtype: dns.TypeA,
Class: dns.ClassINET, Ttl: 300,
},
A: net.ParseIP(ip),
})
return resp
}
func aaaaResponse(
msg *dns.Msg, name string, ip string,
) *dns.Msg {
resp := new(dns.Msg)
resp.SetReply(msg)
resp.Answer = append(resp.Answer, &dns.AAAA{
Hdr: dns.RR_Header{
Name: dns.Fqdn(name), Rrtype: dns.TypeAAAA,
Class: dns.ClassINET, Ttl: 300,
},
AAAA: net.ParseIP(ip),
})
return resp
}
func emptyResponse(msg *dns.Msg) *dns.Msg {
resp := new(dns.Msg)
resp.SetReply(msg)
return resp
}
// ----------------------------------------------------------------
// Mock DNS hierarchy setup
// ----------------------------------------------------------------
// mockData holds all test DNS hierarchy configuration.
type mockData struct {
tldNS []string
tldGlue map[string]string
exNS []string
exGlue map[string]string
cfNS []string
cfGlue map[string]string
}
func newMockData() mockData {
return mockData{
tldNS: []string{"ns1.tld.com", "ns2.tld.com"},
tldGlue: map[string]string{
"ns1.tld.com": "10.0.0.1",
"ns2.tld.com": "10.0.0.2",
},
exNS: []string{
"ns1.example.com", "ns2.example.com",
"ns3.example.com",
},
exGlue: map[string]string{
"ns1.example.com": "10.1.0.1",
"ns2.example.com": "10.1.0.2",
"ns3.example.com": "10.1.0.3",
},
cfNS: []string{
"ns1.cloudflare.com", "ns2.cloudflare.com",
},
cfGlue: map[string]string{
"ns1.cloudflare.com": "10.2.0.1",
"ns2.cloudflare.com": "10.2.0.2",
},
}
}
func rootIPList() []string {
return []string{
"198.41.0.4", "170.247.170.2", "192.33.4.12",
"199.7.91.13", "192.203.230.10", "192.5.5.241",
"192.112.36.4", "198.97.190.53", "192.36.148.17",
"192.58.128.30", "193.0.14.129", "199.7.83.42",
"202.12.27.33",
}
}
func allQueryTypes() []string {
return []string{
"NS", "A", "AAAA", "CNAME", "MX", "TXT", "SRV", "CAA",
}
}
func setupRootDelegations(
m *mockDNSClient,
tNS []string,
tGlue map[string]string,
) {
domains := []string{
"example.com.", "www.example.com.",
"this-surely-does-not-exist-xyz.example.com.",
"cloudflare.com.",
}
for _, rootIP := range rootIPList() {
for _, domain := range domains {
for _, qtype := range allQueryTypes() {
m.on(rootIP, domain, qtype,
func(msg *dns.Msg) *dns.Msg {
return referralResponse(
msg, tNS, tGlue,
)
},
)
}
}
}
}
func setupRootARecords(m *mockDNSClient) {
nsIPs := map[string]string{
"ns1.example.com.": "10.1.0.1",
"ns2.example.com.": "10.1.0.2",
"ns3.example.com.": "10.1.0.3",
"ns1.cloudflare.com.": "10.2.0.1",
"ns2.cloudflare.com.": "10.2.0.2",
}
for _, rootIP := range rootIPList() {
for nsName, nsIP := range nsIPs {
ip := nsIP
name := nsName
m.on(rootIP, name, "A",
func(msg *dns.Msg) *dns.Msg {
return aResponse(msg, name, ip)
},
)
}
}
}
func setupTLDDelegations(
m *mockDNSClient,
exNS []string,
exGlue map[string]string,
cfNS []string,
cfGlue map[string]string,
) {
tldIPs := []string{"10.0.0.1", "10.0.0.2"}
exDomains := []string{
"example.com.", "www.example.com.",
"this-surely-does-not-exist-xyz.example.com.",
}
for _, tldIP := range tldIPs {
for _, domain := range exDomains {
for _, qtype := range allQueryTypes() {
m.on(tldIP, domain, qtype,
func(msg *dns.Msg) *dns.Msg {
return referralResponse(
msg, exNS, exGlue,
)
},
)
}
}
for _, qtype := range allQueryTypes() {
m.on(tldIP, "cloudflare.com.", qtype,
func(msg *dns.Msg) *dns.Msg {
return referralResponse(
msg, cfNS, cfGlue,
)
},
)
}
}
}
func setupExampleNSAndA(
m *mockDNSClient, exNS []string,
) {
exIPs := []string{"10.1.0.1", "10.1.0.2", "10.1.0.3"}
for _, authIP := range exIPs {
m.on(authIP, "example.com.", "NS",
func(msg *dns.Msg) *dns.Msg {
return nsAnswerResponse(msg, exNS)
},
)
m.on(authIP, "example.com.", "A",
func(msg *dns.Msg) *dns.Msg {
return aResponse(
msg, "example.com.", "93.184.216.34",
)
},
)
m.on(authIP, "example.com.", "AAAA",
func(msg *dns.Msg) *dns.Msg {
return aaaaResponse(
msg, "example.com.",
"2606:2800:220:1:248:1893:25c8:1946",
)
},
)
}
}
func setupExampleMXAndTXT(m *mockDNSClient) {
exIPs := []string{"10.1.0.1", "10.1.0.2", "10.1.0.3"}
for _, authIP := range exIPs {
m.on(authIP, "example.com.", "MX",
func(msg *dns.Msg) *dns.Msg {
resp := new(dns.Msg)
resp.SetReply(msg)
resp.Answer = append(resp.Answer,
&dns.MX{
Hdr: dns.RR_Header{
Name: "example.com.",
Rrtype: dns.TypeMX,
Class: dns.ClassINET,
Ttl: 300,
},
Preference: 10,
Mx: "mail.example.com.",
},
&dns.MX{
Hdr: dns.RR_Header{
Name: "example.com.",
Rrtype: dns.TypeMX,
Class: dns.ClassINET,
Ttl: 300,
},
Preference: 20,
Mx: "mail2.example.com.",
},
)
return resp
},
)
m.on(authIP, "example.com.", "TXT",
func(msg *dns.Msg) *dns.Msg {
resp := new(dns.Msg)
resp.SetReply(msg)
resp.Answer = append(resp.Answer, &dns.TXT{
Hdr: dns.RR_Header{
Name: "example.com.",
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: 300,
},
Txt: []string{
"v=spf1 include:_spf.example.com ~all",
},
})
return resp
},
)
}
}
func setupExampleSubdomains(
m *mockDNSClient, exNS []string,
) {
exIPs := []string{"10.1.0.1", "10.1.0.2", "10.1.0.3"}
for _, authIP := range exIPs {
m.on(authIP, "www.example.com.", "NS",
func(msg *dns.Msg) *dns.Msg {
return nsAnswerResponse(msg, exNS)
},
)
m.on(authIP, "www.example.com.", "A",
func(msg *dns.Msg) *dns.Msg {
return aResponse(
msg, "www.example.com.", "93.184.216.34",
)
},
)
nxName := "this-surely-does-not-exist-xyz.example.com."
for _, qtype := range allQueryTypes() {
m.on(authIP, nxName, qtype, nxdomainResponse)
}
}
}
func setupCloudflareAuthRecords(
m *mockDNSClient, cfNS []string,
) {
cfIPs := []string{"10.2.0.1", "10.2.0.2"}
for _, authIP := range cfIPs {
m.on(authIP, "cloudflare.com.", "NS",
func(msg *dns.Msg) *dns.Msg {
return nsAnswerResponse(msg, cfNS)
},
)
m.on(authIP, "cloudflare.com.", "A",
func(msg *dns.Msg) *dns.Msg {
return aResponse(
msg, "cloudflare.com.", "104.16.132.229",
)
},
)
m.on(authIP, "cloudflare.com.", "AAAA",
func(msg *dns.Msg) *dns.Msg {
return aaaaResponse(
msg, "cloudflare.com.",
"2606:4700::6810:84e5",
)
},
)
m.on(authIP, "cloudflare.com.", "MX", emptyResponse)
m.on(authIP, "cloudflare.com.", "TXT", emptyResponse)
}
}
func setupMockDNS() *mockDNSClient {
m := newMockClient()
d := newMockData()
setupRootDelegations(m, d.tldNS, d.tldGlue)
setupRootARecords(m)
setupTLDDelegations(m, d.exNS, d.exGlue, d.cfNS, d.cfGlue)
setupExampleNSAndA(m, d.exNS)
setupExampleMXAndTXT(m)
setupExampleSubdomains(m, d.exNS)
setupCloudflareAuthRecords(m, d.cfNS)
return m
}
// ----------------------------------------------------------------
// Test helpers
// ----------------------------------------------------------------
@@ -521,14 +28,14 @@ func newTestResolver(t *testing.T) *resolver.Resolver {
&slog.HandlerOptions{Level: slog.LevelDebug},
))
return resolver.NewFromLoggerWithClient(log, setupMockDNS())
return resolver.NewFromLogger(log)
}
func testContext(t *testing.T) context.Context {
t.Helper()
ctx, cancel := context.WithTimeout(
context.Background(), 10*time.Second,
context.Background(), 60*time.Second,
)
t.Cleanup(cancel)
@@ -565,23 +72,23 @@ func TestFindAuthoritativeNameservers_ValidDomain(
ctx := testContext(t)
nameservers, err := r.FindAuthoritativeNameservers(
ctx, "example.com",
ctx, "google.com",
)
require.NoError(t, err)
require.NotEmpty(t, nameservers)
hasExampleNS := false
hasGoogleNS := false
for _, ns := range nameservers {
if strings.Contains(ns, "example") {
hasExampleNS = true
if strings.Contains(ns, "google") {
hasGoogleNS = true
break
}
}
assert.True(t, hasExampleNS,
"expected example nameservers, got: %v", nameservers,
assert.True(t, hasGoogleNS,
"expected google nameservers, got: %v", nameservers,
)
}
@@ -594,7 +101,7 @@ func TestFindAuthoritativeNameservers_Subdomain(
ctx := testContext(t)
nameservers, err := r.FindAuthoritativeNameservers(
ctx, "www.example.com",
ctx, "www.google.com",
)
require.NoError(t, err)
require.NotEmpty(t, nameservers)
@@ -609,7 +116,7 @@ func TestFindAuthoritativeNameservers_ReturnsSorted(
ctx := testContext(t)
nameservers, err := r.FindAuthoritativeNameservers(
ctx, "example.com",
ctx, "google.com",
)
require.NoError(t, err)
@@ -629,12 +136,12 @@ func TestFindAuthoritativeNameservers_Deterministic(
ctx := testContext(t)
first, err := r.FindAuthoritativeNameservers(
ctx, "example.com",
ctx, "google.com",
)
require.NoError(t, err)
second, err := r.FindAuthoritativeNameservers(
ctx, "example.com",
ctx, "google.com",
)
require.NoError(t, err)
@@ -650,12 +157,12 @@ func TestFindAuthoritativeNameservers_TrailingDot(
ctx := testContext(t)
ns1, err := r.FindAuthoritativeNameservers(
ctx, "example.com",
ctx, "google.com",
)
require.NoError(t, err)
ns2, err := r.FindAuthoritativeNameservers(
ctx, "example.com.",
ctx, "google.com.",
)
require.NoError(t, err)
@@ -692,10 +199,10 @@ func TestQueryNameserver_BasicA(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
ns := findOneNSForDomain(t, r, ctx, "example.com")
ns := findOneNSForDomain(t, r, ctx, "google.com")
resp, err := r.QueryNameserver(
ctx, ns, "www.example.com",
ctx, ns, "www.google.com",
)
require.NoError(t, err)
require.NotNil(t, resp)
@@ -706,7 +213,7 @@ func TestQueryNameserver_BasicA(t *testing.T) {
hasRecords := len(resp.Records["A"]) > 0 ||
len(resp.Records["CNAME"]) > 0
assert.True(t, hasRecords,
"expected A or CNAME records for www.example.com",
"expected A or CNAME records for www.google.com",
)
}
@@ -740,16 +247,16 @@ func TestQueryNameserver_MX(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
ns := findOneNSForDomain(t, r, ctx, "example.com")
ns := findOneNSForDomain(t, r, ctx, "google.com")
resp, err := r.QueryNameserver(
ctx, ns, "example.com",
ctx, ns, "google.com",
)
require.NoError(t, err)
mxRecords := resp.Records["MX"]
require.NotEmpty(t, mxRecords,
"example.com should have MX records",
"google.com should have MX records",
)
}
@@ -758,16 +265,16 @@ func TestQueryNameserver_TXT(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
ns := findOneNSForDomain(t, r, ctx, "example.com")
ns := findOneNSForDomain(t, r, ctx, "google.com")
resp, err := r.QueryNameserver(
ctx, ns, "example.com",
ctx, ns, "google.com",
)
require.NoError(t, err)
txtRecords := resp.Records["TXT"]
require.NotEmpty(t, txtRecords,
"example.com should have TXT records",
"google.com should have TXT records",
)
hasSPF := false
@@ -781,7 +288,7 @@ func TestQueryNameserver_TXT(t *testing.T) {
}
assert.True(t, hasSPF,
"example.com should have SPF TXT record",
"google.com should have SPF TXT record",
)
}
@@ -790,11 +297,11 @@ func TestQueryNameserver_NXDomain(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
ns := findOneNSForDomain(t, r, ctx, "example.com")
ns := findOneNSForDomain(t, r, ctx, "google.com")
resp, err := r.QueryNameserver(
ctx, ns,
"this-surely-does-not-exist-xyz.example.com",
"this-surely-does-not-exist-xyz.google.com",
)
require.NoError(t, err)
@@ -806,10 +313,10 @@ func TestQueryNameserver_RecordsSorted(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
ns := findOneNSForDomain(t, r, ctx, "example.com")
ns := findOneNSForDomain(t, r, ctx, "google.com")
resp, err := r.QueryNameserver(
ctx, ns, "example.com",
ctx, ns, "google.com",
)
require.NoError(t, err)
@@ -846,11 +353,11 @@ func TestQueryNameserver_EmptyRecordsOnNXDomain(
r := newTestResolver(t)
ctx := testContext(t)
ns := findOneNSForDomain(t, r, ctx, "example.com")
ns := findOneNSForDomain(t, r, ctx, "google.com")
resp, err := r.QueryNameserver(
ctx, ns,
"this-surely-does-not-exist-xyz.example.com",
"this-surely-does-not-exist-xyz.google.com",
)
require.NoError(t, err)
@@ -867,15 +374,15 @@ func TestQueryNameserver_TrailingDotHandling(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
ns := findOneNSForDomain(t, r, ctx, "example.com")
ns := findOneNSForDomain(t, r, ctx, "google.com")
resp1, err := r.QueryNameserver(
ctx, ns, "example.com",
ctx, ns, "google.com",
)
require.NoError(t, err)
resp2, err := r.QueryNameserver(
ctx, ns, "example.com.",
ctx, ns, "google.com.",
)
require.NoError(t, err)
@@ -893,7 +400,7 @@ func TestQueryAllNameservers_ReturnsAllNS(t *testing.T) {
ctx := testContext(t)
results, err := r.QueryAllNameservers(
ctx, "example.com",
ctx, "google.com",
)
require.NoError(t, err)
require.NotEmpty(t, results)
@@ -912,7 +419,7 @@ func TestQueryAllNameservers_AllReturnOK(t *testing.T) {
ctx := testContext(t)
results, err := r.QueryAllNameservers(
ctx, "example.com",
ctx, "google.com",
)
require.NoError(t, err)
@@ -934,7 +441,7 @@ func TestQueryAllNameservers_NXDomainFromAllNS(
results, err := r.QueryAllNameservers(
ctx,
"this-surely-does-not-exist-xyz.example.com",
"this-surely-does-not-exist-xyz.google.com",
)
require.NoError(t, err)
@@ -956,7 +463,7 @@ func TestLookupNS_ValidDomain(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
nameservers, err := r.LookupNS(ctx, "example.com")
nameservers, err := r.LookupNS(ctx, "google.com")
require.NoError(t, err)
require.NotEmpty(t, nameservers)
@@ -973,7 +480,7 @@ func TestLookupNS_Sorted(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
nameservers, err := r.LookupNS(ctx, "example.com")
nameservers, err := r.LookupNS(ctx, "google.com")
require.NoError(t, err)
assert.True(t, sort.StringsAreSorted(nameservers))
@@ -985,11 +492,11 @@ func TestLookupNS_MatchesFindAuthoritative(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
fromLookup, err := r.LookupNS(ctx, "example.com")
fromLookup, err := r.LookupNS(ctx, "google.com")
require.NoError(t, err)
fromFind, err := r.FindAuthoritativeNameservers(
ctx, "example.com",
ctx, "google.com",
)
require.NoError(t, err)
@@ -1006,7 +513,7 @@ func TestResolveIPAddresses_ReturnsIPs(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
ips, err := r.ResolveIPAddresses(ctx, "example.com")
ips, err := r.ResolveIPAddresses(ctx, "google.com")
require.NoError(t, err)
require.NotEmpty(t, ips)
@@ -1024,7 +531,7 @@ func TestResolveIPAddresses_Deduplicated(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
ips, err := r.ResolveIPAddresses(ctx, "example.com")
ips, err := r.ResolveIPAddresses(ctx, "google.com")
require.NoError(t, err)
seen := make(map[string]bool)
@@ -1041,7 +548,7 @@ func TestResolveIPAddresses_Sorted(t *testing.T) {
r := newTestResolver(t)
ctx := testContext(t)
ips, err := r.ResolveIPAddresses(ctx, "example.com")
ips, err := r.ResolveIPAddresses(ctx, "google.com")
require.NoError(t, err)
assert.True(t, sort.StringsAreSorted(ips))
@@ -1057,7 +564,7 @@ func TestResolveIPAddresses_NXDomainReturnsEmpty(
ips, err := r.ResolveIPAddresses(
ctx,
"this-surely-does-not-exist-xyz.example.com",
"this-surely-does-not-exist-xyz.google.com",
)
require.NoError(t, err)
assert.Empty(t, ips)
@@ -1087,9 +594,7 @@ func TestFindAuthoritativeNameservers_ContextCanceled(
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := r.FindAuthoritativeNameservers(
ctx, "example.com",
)
_, err := r.FindAuthoritativeNameservers(ctx, "google.com")
assert.Error(t, err)
}
@@ -1101,7 +606,7 @@ func TestQueryNameserver_ContextCanceled(t *testing.T) {
cancel()
_, err := r.QueryNameserver(
ctx, "ns1.example.com.", "example.com",
ctx, "ns1.google.com.", "google.com",
)
assert.Error(t, err)
}
@@ -1113,7 +618,7 @@ func TestQueryAllNameservers_ContextCanceled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := r.QueryAllNameservers(ctx, "example.com")
_, err := r.QueryAllNameservers(ctx, "google.com")
assert.Error(t, err)
}
@@ -1124,6 +629,6 @@ func TestResolveIPAddresses_ContextCanceled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := r.ResolveIPAddresses(ctx, "example.com")
_, err := r.ResolveIPAddresses(ctx, "google.com")
assert.Error(t, err)
}

View File

@@ -156,8 +156,8 @@ func (s *State) Load() error {
// Save writes the current state to disk atomically.
func (s *State) Save() error {
s.mu.Lock()
defer s.mu.Unlock()
s.mu.RLock()
defer s.mu.RUnlock()
s.snapshot.LastUpdated = time.Now().UTC()