Repository navigation
Expand file tree
/
Copy patherrors_test.go
More file actions
121 lines (111 loc) · 4.36 KB
/
Copy patherrors_test.go
File metadata and controls
121 lines (111 loc) · 4.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package b24gosdk
import (
"errors"
"fmt"
"testing"
)
// The point of the whole file: a code comparison that the compiler helps with,
// instead of a string literal that silently takes the wrong branch when mistyped.
func TestErrorsIsMatchesByCode(t *testing.T) {
err := error(&APIError{Code: "ERROR_METHOD_NOT_FOUND", Description: "nope", HTTPStatus: 400})
if !errors.Is(err, ErrMethodNotFound) {
t.Error("errors.Is did not match the named sentinel")
}
if errors.Is(err, ErrAccessDenied) {
t.Error("errors.Is matched a different code")
}
// A code the SDK has never heard of is still expressible.
if !errors.Is(error(&APIError{Code: "CREATE_DYNAMIC_TYPE_RESTRICTED"}),
Code("CREATE_DYNAMIC_TYPE_RESTRICTED")) {
t.Error("an unknown code did not match its own sentinel")
}
}
// Bitrix24 mixes cases: REST errors arrive upper-cased, OAuth ones lower-cased,
// and the docs show the same code both ways. A caller must not have to guess.
func TestCodeMatchingIsCaseInsensitive(t *testing.T) {
for _, spelling := range []string{"expired_token", "EXPIRED_TOKEN", "Expired_Token"} {
err := error(&APIError{Code: spelling})
if !errors.Is(err, ErrExpiredToken) {
t.Errorf("code %q did not match ErrExpiredToken", spelling)
}
}
// And the sentinel itself normalizes, whichever way it was built.
if !errors.Is(error(&APIError{Code: "ACCESS_DENIED"}), Code("access_denied")) {
t.Error("a lower-cased sentinel did not match an upper-cased code")
}
}
// A wrapped error must still match, or the sentinel is useless the moment a
// caller adds context.
func TestErrorsIsThroughWrapping(t *testing.T) {
err := fmt.Errorf("loading deals: %w", error(&APIError{Code: "ACCESS_DENIED"}))
if !errors.Is(err, ErrAccessDenied) {
t.Error("a wrapped APIError did not match")
}
}
// A 5xx from a proxy carries no code. It must not match anything, or a caller
// branching on "is this access denied" would take that branch on a gateway
// error.
func TestErrorWithoutACodeMatchesNothing(t *testing.T) {
err := error(&APIError{HTTPStatus: 502, RawBody: "<html>gateway</html>"})
for _, sentinel := range []error{ErrAccessDenied, ErrMethodNotFound, ErrQueryLimitExceeded} {
if errors.Is(err, sentinel) {
t.Errorf("a codeless error matched %v", sentinel)
}
}
if _, ok := CodeOf(err); ok {
t.Error("CodeOf reported a code for a codeless error")
}
}
// Matching is on the code ALONE. OVERLOAD_LIMIT and QUERY_LIMIT_EXCEEDED both
// arrive as HTTP 503, so a status-based match would treat a manual block as a
// rate limit and retry something that must not be retried.
func TestStatusDoesNotDecideTheMatch(t *testing.T) {
overload := error(&APIError{Code: "OVERLOAD_LIMIT", HTTPStatus: 503})
if errors.Is(overload, ErrQueryLimitExceeded) {
t.Error("OVERLOAD_LIMIT matched the rate limit; both are 503, the codes differ")
}
if !errors.Is(overload, Code(CodeOverloadLimit)) {
t.Error("OVERLOAD_LIMIT did not match its own code")
}
}
func TestCodeOf(t *testing.T) {
got, ok := CodeOf(error(&APIError{Code: "expired_token"}))
if !ok || got != "EXPIRED_TOKEN" {
t.Errorf("CodeOf = %q, %v; want the normalized code", got, ok)
}
if _, ok := CodeOf(errors.New("plain")); ok {
t.Error("CodeOf reported a code for a non-API error")
}
// A sentinel itself carries its code, which is what lets a caller log it.
if got, ok := CodeOf(ErrAccessDenied); !ok || got != "ACCESS_DENIED" {
t.Errorf("CodeOf(sentinel) = %q, %v", got, ok)
}
}
// The named sentinels must not accidentally be the same value.
func TestSentinelsAreDistinct(t *testing.T) {
all := map[string]error{
"queryLimit": ErrQueryLimitExceeded, "operatingLimit": ErrOperationTimeLimit,
"expired": ErrExpiredToken, "invalidToken": ErrInvalidToken,
"invalidGrant": ErrInvalidGrant, "scope": ErrInsufficientScope,
"notFound": ErrMethodNotFound, "denied": ErrAccessDenied,
"payment": ErrPaymentRequired,
}
for na, a := range all {
for nb, b := range all {
if na == nb {
continue
}
if errors.Is(a, b) {
t.Errorf("sentinels %s and %s match each other", na, nb)
}
}
}
}
// The retry path classifies by code too; a caller and the SDK must agree on what
// a rate limit is.
func TestRateLimitSentinelMatchesWhatTheSDKRetries(t *testing.T) {
err := error(&APIError{Code: "QUERY_LIMIT_EXCEEDED", HTTPStatus: 503})
if !errors.Is(err, ErrQueryLimitExceeded) {
t.Error("the code the SDK retries on does not match its own sentinel")
}
}