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, 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) }) } }