Files
dnswatcher/internal/resolver/iterative_internal_test.go
T
sneak c0647eef26
check / check (push) Canceled after 0s
resolver, watcher: a record type whose query fails keeps its previous records (closes #231)
The resolver lists in FailedTypes each record type whose query to a
nameserver got no usable reply (no reply, a code other than NOERROR or
NXDOMAIN, a referral, or a truncated reply whose TCP retry failed) and
logs it unless shutdown cut it short. A nameserver that answered no
type has failed.
The watcher saves such a type in failedTypes with the previous check's
records, leaves it out of the comparison with other nameservers on that
check, and compares it with the next answer. When the previous check
did not know its records either, it is also in unknownTypes and not
compared until it answers. A nameserver whose A, AAAA or CNAME query
failed is no answer when following a CNAME or resolving addresses.

Model: opus-5-5
2026-10-02 07:53:15 +00:00

117 lines
2.8 KiB
Go

package resolver
import (
"syscall"
"testing"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
// TestClassifyResponse sets a nameserver's status from the results of
// its queries, 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; one whose every query got none
// has.
func TestClassifyResponse(t *testing.T) {
t.Parallel()
tests := []struct {
name string
results queryState
wantStatus string
wantError string
}{
{
"some types answered with no records, another timed out",
queryState{answered: true, gotTimeout: true},
StatusNoData, "",
},
{
"some types answered with no records, another got SERVFAIL",
queryState{answered: true, errorReply: "SERVFAIL"},
StatusNoData, "",
},
{
"some types answered with no records, another was refused",
queryState{answered: true, gotRefused: true},
StatusNoData, "",
},
{
"some types answered with no records, another got a network error",
queryState{answered: true, netErr: syscall.ECONNREFUSED},
StatusNoData, "",
},
{
"some types answered with no records, another's reply was " +
"truncated and its retry over TCP failed",
queryState{answered: true, netErr: ErrTruncated},
StatusNoData, "",
},
{
"some types answered with no records, another got a referral",
queryState{answered: true, gotReferral: true},
StatusNoData, "",
},
{
"every query timed out",
queryState{gotTimeout: true},
StatusTimeout, "all queries timed out",
},
{
"every query got NOTIMP",
queryState{errorReply: "NOTIMP"},
StatusError, "server returned NOTIMP",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
resp := &NameserverResponse{Status: StatusOK}
classifyResponse(resp, tt.results)
assert.Equal(t, tt.wantStatus, resp.Status)
assert.Equal(t, tt.wantError, resp.Error)
})
}
}
// 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.
func TestReadReply(t *testing.T) {
t.Parallel()
tests := []struct {
rcode int
answered bool
}{
{dns.RcodeSuccess, true},
{dns.RcodeNameError, true},
{dns.RcodeServerFailure, false},
{dns.RcodeNotImplemented, false},
{dns.RcodeFormatError, false},
}
for _, tt := range tests {
t.Run(dns.RcodeToString[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{}}
err := readReply(msg, resp, &queryState{})
if tt.answered {
assert.NoError(t, err)
} else {
assert.ErrorIs(t, err, ErrUnusableReply)
}
})
}
}