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