Files
dnswatcher/internal/resolver/iterative_internal_test.go
T
sneak 5f20fa31c8
check / check (push) Canceled after 0s
resolver: a query cut short by the caller's context records no reason (closes #298)
When the caller's context ends, the next query returns
ErrContextCanceled before anything is sent. querySingleType recorded
that as a network error, which outranks a timeout, so a nameserver that
had only timed out could be reported as "network error: context
canceled". It now records no reason for it.

Model: opus-5-5
2026-10-15 16:38:24 +00:00

167 lines
5.8 KiB
Go

package resolver
import (
"context"
"log/slog"
"strconv"
"syscall"
"testing"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestClassifyResponse sets a nameserver's status from the results of
// its queries and the record types whose query failed, built here. One
// that answered some record types, even with no records, has not failed
// when its query for another type got no usable reply, whatever the
// reason, and is ok, not nodata: that type may have records. One whose
// every query got none has failed, and its error says all queries timed
// out only when every one did: a reply such as a refusal, or a network
// error, at one of its addresses is given instead. Only one whose every
// type answered with no records is nodata.
func TestClassifyResponse(t *testing.T) {
t.Parallel()
tests := []struct {
name string
results queryState
failedTypes []string
wantStatus string
wantError string
}{
{"every type answered with no records",
queryState{answered: true},
nil, StatusNoData, ""},
{"some types answered with no records, another timed out",
queryState{answered: true, gotTimeout: true},
[]string{"A"}, StatusOK, ""},
{"some types answered with no records, another got SERVFAIL",
queryState{answered: true, gotErrorReply: true, errorReply: "SERVFAIL"},
[]string{"A"}, StatusOK, ""},
{"some types answered with no records, another was refused",
queryState{answered: true, gotRefused: true},
[]string{"A"}, StatusOK, ""},
{"some types answered with no records, another got a network error",
queryState{answered: true, netErr: syscall.ECONNREFUSED},
[]string{"A"}, StatusOK, ""},
{"some types answered with no records, another's reply was " +
"truncated and its retry over TCP failed",
queryState{answered: true, netErr: ErrTruncated},
[]string{"TXT"}, StatusOK, ""},
{"some types answered with no records, another got a referral",
queryState{answered: true, gotReferral: true},
[]string{"A"}, StatusOK, ""},
{"every query timed out",
queryState{gotTimeout: true},
[]string{"A", "AAAA", "CNAME"}, StatusTimeout, "all queries timed out"},
{"every query got NOTIMP",
queryState{gotErrorReply: true, errorReply: "NOTIMP"},
[]string{"A", "AAAA", "CNAME"}, StatusError, "server returned NOTIMP"},
{"every query got SERVFAIL at one address, timed out at another",
queryState{gotErrorReply: true, errorReply: "SERVFAIL", gotTimeout: true},
[]string{"A"}, StatusError, "server returned SERVFAIL"},
{"every query was refused at one address, timed out at another",
queryState{gotRefused: true, gotTimeout: true},
[]string{"A"}, StatusError, "server returned REFUSED"},
{"every query got a network error at one address, timed out at another",
queryState{netErr: syscall.ECONNREFUSED, gotTimeout: true},
[]string{"A"}, StatusError, "network error: connection refused"},
{"every query got a referral at one address, timed out at another",
queryState{gotReferral: true, gotTimeout: true},
[]string{"A"}, StatusError, "server returned a referral"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
resp := &NameserverResponse{Status: StatusOK, FailedTypes: tt.failedTypes}
classifyResponse(resp, tt.results)
assert.Equal(t, tt.wantStatus, resp.Status)
assert.Equal(t, tt.wantError, resp.Error)
assert.Equal(t, tt.failedTypes, resp.FailedTypes)
})
}
}
// TestReadReply checks which replies to a query about one record type,
// built here, are an answer: one with the code NOERROR or NXDOMAIN. A
// reply with any other code is not, and the type's query has failed; a
// nameserver whose only reply it is has failed, and Error gives the
// code, or its number when the code has no name.
func TestReadReply(t *testing.T) {
t.Parallel()
tests := []struct {
rcode int
wantStatus string
wantError string
}{
{dns.RcodeSuccess, StatusNoData, ""},
{dns.RcodeNameError, StatusNXDomain, ""},
{dns.RcodeServerFailure, StatusError, "server returned SERVFAIL"},
{dns.RcodeNotImplemented, StatusError, "server returned NOTIMP"},
{dns.RcodeFormatError, StatusError, "server returned FORMERR"},
{12, StatusError, "server returned 12"}, // unassigned, no name
}
for _, tt := range tests {
t.Run(strconv.Itoa(tt.rcode), func(t *testing.T) {
t.Parallel()
msg := new(dns.Msg)
msg.Authoritative = true
msg.Rcode = tt.rcode
resp := &NameserverResponse{Records: map[string][]string{}}
var state queryState
err := readReply(msg, resp, &state)
classifyResponse(resp, state)
if tt.wantStatus == StatusError {
require.ErrorIs(t, err, ErrUnusableReply)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.wantStatus, resp.Status)
assert.Equal(t, tt.wantError, resp.Error)
})
}
}
// TestQuerySingleType_ContextEnded asks a nameserver about one record
// type with a context that has already ended, so no query is sent. A
// query the context cut short says nothing about the nameserver and
// records no reason: a nameserver whose earlier query timed out is
// still reported as timed out, not as a network error.
func TestQuerySingleType_ContextEnded(t *testing.T) {
t.Parallel()
r := NewFromLogger(slog.New(slog.DiscardHandler))
ctx, cancel := context.WithCancel(t.Context())
cancel()
resp := &NameserverResponse{Records: map[string][]string{}, Status: StatusOK}
// The query for an earlier record type timed out.
state := queryState{gotTimeout: true}
_, err := r.querySingleType(
ctx, []string{"192.0.2.1"}, "example.com.", dns.TypeA, resp, &state,
)
require.ErrorIs(t, err, ErrContextCanceled)
assert.Equal(t, queryState{gotTimeout: true}, state)
classifyResponse(resp, state)
assert.Equal(t, StatusTimeout, resp.Status)
assert.Equal(t, "all queries timed out", resp.Error)
}