check / check (push) Successful in 1m16s
classifyResponse set nodata when every record type that answered had no records, even when another type's query got no usable reply. That type is listed in FailedTypes and its records are unknown, so the nameserver has not said it has none. It now stays ok, the status README describes for a nameserver with a failed type, and nodata is set only when no type failed. The watcher saved nodata as ok already, so saved state is unchanged; the live test that rejects nodata no longer fails when one of a nameserver's queries is lost. Model: opus-5-5
150 lines
4.0 KiB
Go
150 lines
4.0 KiB
Go
package resolver
|
|
|
|
import (
|
|
"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. 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",
|
|
},
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|