package checker import ( "context" "errors" "io" "net" "syscall" "testing" "github.com/stretchr/testify/assert" ) // timeoutOnlyError is a minimal net.Error that reports Timeout()=true. // Used to drive the net.Error.Timeout() branch in classifyError. type timeoutOnlyError struct{} func (timeoutOnlyError) Error() string { return "i/o timeout" } func (timeoutOnlyError) Timeout() bool { return true } func (timeoutOnlyError) Temporary() bool { return true } func TestClassifyError(t *testing.T) { cases := []struct { name string err error want Classification }{ {"nil", nil, ClassificationPermanent}, {"context_canceled", context.Canceled, ClassificationPermanent}, {"context_deadline", context.DeadlineExceeded, ClassificationPermanent}, {"econnrefused", syscall.ECONNREFUSED, ClassificationPermanent}, {"econnreset", syscall.ECONNRESET, ClassificationTransient}, {"econnreset_wrapped", &net.OpError{Op: "read", Err: syscall.ECONNRESET}, ClassificationTransient}, {"net_timeout", &timeoutOnlyError{}, ClassificationTransient}, {"dns_temporary", &net.DNSError{IsTemporary: true}, ClassificationTransient}, {"dns_timeout", &net.DNSError{IsTimeout: true}, ClassificationTransient}, {"dns_permanent", &net.DNSError{IsNotFound: true}, ClassificationPermanent}, {"socks5_auth_rejected", ErrAuthRejected, ClassificationPermanent}, {"socks5_bad_version", ErrSocks5BadVersion, ClassificationPermanent}, {"socks5_rejected_all_auth", ErrSocks5RejectedAllAuth, ClassificationPermanent}, {"socks5_credential_too_long", ErrCredentialTooLong, ClassificationPermanent}, {"socks5_host_too_long", ErrHostTooLong, ClassificationPermanent}, {"socks5_unsupported_relay_atyp", ErrUnsupportedRelayATYP, ClassificationPermanent}, {"socks5_short_reply", ErrShortReply, ClassificationPermanent}, {"socks5_reply_general_failure", ErrSocks5Reply{Code: 0x01}, ClassificationPermanent}, {"socks5_reply_not_allowed", ErrSocks5Reply{Code: 0x02}, ClassificationPermanent}, {"socks5_reply_refused", ErrSocks5Reply{Code: 0x05}, ClassificationPermanent}, {"socks5_reply_unsupported", ErrSocks5Reply{Code: 0x07}, ClassificationPermanent}, {"stun_too_short", ErrSTUNTooShort, ClassificationPermanent}, {"stun_bad_magic", ErrSTUNBadMagicCookie, ClassificationPermanent}, {"stun_not_success", ErrSTUNNotSuccess, ClassificationPermanent}, {"stun_txid_mismatch", ErrSTUNTxIDMismatch, ClassificationPermanent}, {"stun_no_mapped", ErrSTUNNoMappedAddress, ClassificationPermanent}, {"stun_unsupported_family", ErrSTUNUnsupportedFamily, ClassificationPermanent}, {"unexpected_eof", io.ErrUnexpectedEOF, ClassificationPermanent}, {"unexpected_eof_in_op", &net.OpError{Op: "read", Err: io.ErrUnexpectedEOF}, ClassificationTransient}, {"joined_canceled_with_timeout", errors.Join(context.Canceled, &timeoutOnlyError{}), ClassificationPermanent}, {"joined_canceled_with_econnreset", errors.Join(context.Canceled, syscall.ECONNRESET), ClassificationPermanent}, {"random", errors.New("ouch"), ClassificationPermanent}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := classifyError(c.err) assert.Equal(t, c.want, got, "err=%v", c.err) }) } } func TestIsContextErr(t *testing.T) { t.Run("nil", func(t *testing.T) { assert.False(t, isContextErr(nil)) }) t.Run("canceled", func(t *testing.T) { assert.True(t, isContextErr(context.Canceled)) }) t.Run("deadline", func(t *testing.T) { assert.True(t, isContextErr(context.DeadlineExceeded)) }) t.Run("joined_canceled_with_econnreset", func(t *testing.T) { assert.True(t, isContextErr(errors.Join(context.Canceled, syscall.ECONNRESET))) }) t.Run("random", func(t *testing.T) { assert.False(t, isContextErr(errors.New("nope"))) }) t.Run("oprrror_wrapping_deadline", func(t *testing.T) { err := &net.OpError{Op: "read", Err: context.DeadlineExceeded} assert.True(t, isContextErr(err)) }) }