1
2
3
4
5 package acme
6
7 import (
8 "context"
9 "fmt"
10 "io"
11 "net/http"
12 "net/http/httptest"
13 "reflect"
14 "strings"
15 "testing"
16 "time"
17 )
18
19 func TestDefaultBackoff(t *testing.T) {
20 tt := []struct {
21 nretry int
22 retryAfter string
23 out time.Duration
24 }{
25 {-1, "", time.Second},
26 {0, "", time.Second},
27 {100, "", 10 * time.Second},
28 {1, "3600", time.Hour},
29 {1, "", 1 * time.Second},
30 {2, "", 2 * time.Second},
31 {3, "", 4 * time.Second},
32 {4, "", 8 * time.Second},
33 }
34 for i, test := range tt {
35 r := httptest.NewRequest("GET", "/", nil)
36 resp := &http.Response{Header: http.Header{}}
37 if test.retryAfter != "" {
38 resp.Header.Set("Retry-After", test.retryAfter)
39 }
40 d := defaultBackoff(test.nretry, r, resp)
41 max := test.out + time.Second
42 if d < test.out || max < d {
43 t.Errorf("%d: defaultBackoff(%v) = %v; want between %v and %v", i, test.nretry, d, test.out, max)
44 }
45 }
46 }
47
48 func TestErrorResponse(t *testing.T) {
49 s := `{
50 "status": 400,
51 "type": "urn:acme:error:xxx",
52 "detail": "text"
53 }`
54 res := &http.Response{
55 StatusCode: 400,
56 Status: "400 Bad Request",
57 Body: io.NopCloser(strings.NewReader(s)),
58 Header: http.Header{"X-Foo": {"bar"}},
59 }
60 err := responseError(res)
61 v, ok := err.(*Error)
62 if !ok {
63 t.Fatalf("err = %+v (%T); want *Error type", err, err)
64 }
65 if v.StatusCode != 400 {
66 t.Errorf("v.StatusCode = %v; want 400", v.StatusCode)
67 }
68 if v.ProblemType != "urn:acme:error:xxx" {
69 t.Errorf("v.ProblemType = %q; want urn:acme:error:xxx", v.ProblemType)
70 }
71 if v.Detail != "text" {
72 t.Errorf("v.Detail = %q; want text", v.Detail)
73 }
74 if !reflect.DeepEqual(v.Header, res.Header) {
75 t.Errorf("v.Header = %+v; want %+v", v.Header, res.Header)
76 }
77 }
78
79 func TestPostWithRetries(t *testing.T) {
80 var count int
81 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
82 count++
83 w.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", count))
84 if r.Method == "HEAD" {
85
86
87 return
88 }
89
90 head, err := decodeJWSHead(r.Body)
91 switch {
92 case err != nil:
93 t.Errorf("decodeJWSHead: %v", err)
94 case head.Nonce == "":
95 t.Error("head.Nonce is empty")
96 case head.Nonce == "nonce1":
97
98 w.Header().Set("Retry-After", "0")
99 w.WriteHeader(http.StatusBadRequest)
100 w.Write([]byte(`{"type":"urn:ietf:params:acme:error:badNonce"}`))
101 return
102 }
103
104 w.WriteHeader(http.StatusCreated)
105 w.Write([]byte(`{"status":"valid"}`))
106 }))
107 defer ts.Close()
108
109 client := &Client{
110 Key: testKey,
111 DirectoryURL: ts.URL,
112 dir: &Directory{AuthzURL: ts.URL},
113 }
114
115 if _, err := client.Authorize(context.Background(), "example.com"); err != nil {
116 t.Errorf("client.Authorize 1: %v", err)
117 }
118 if count != 3 {
119 t.Errorf("total requests count: %d; want 3", count)
120 }
121 }
122
123 func TestRetryErrorType(t *testing.T) {
124 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
125 w.Header().Set("Replay-Nonce", "nonce")
126 w.WriteHeader(http.StatusTooManyRequests)
127 w.Write([]byte(`{"type":"rateLimited"}`))
128 }))
129 defer ts.Close()
130
131 client := &Client{
132 Key: testKey,
133 RetryBackoff: func(n int, r *http.Request, res *http.Response) time.Duration {
134
135 return 0
136 },
137 dir: &Directory{AuthzURL: ts.URL},
138 }
139
140 t.Run("post", func(t *testing.T) {
141 testRetryErrorType(t, func() error {
142 _, err := client.Authorize(context.Background(), "example.com")
143 return err
144 })
145 })
146 t.Run("get", func(t *testing.T) {
147 testRetryErrorType(t, func() error {
148 _, err := client.GetAuthorization(context.Background(), ts.URL)
149 return err
150 })
151 })
152 }
153
154 func testRetryErrorType(t *testing.T, callClient func() error) {
155 t.Helper()
156 err := callClient()
157 if err == nil {
158 t.Fatal("client.Authorize returned nil error")
159 }
160 acmeErr, ok := err.(*Error)
161 if !ok {
162 t.Fatalf("err is %v (%T); want *Error", err, err)
163 }
164 if acmeErr.StatusCode != http.StatusTooManyRequests {
165 t.Errorf("acmeErr.StatusCode = %d; want %d", acmeErr.StatusCode, http.StatusTooManyRequests)
166 }
167 if acmeErr.ProblemType != "rateLimited" {
168 t.Errorf("acmeErr.ProblemType = %q; want 'rateLimited'", acmeErr.ProblemType)
169 }
170 }
171
172 func TestRetryBackoffArgs(t *testing.T) {
173 const resCode = http.StatusInternalServerError
174 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
175 w.Header().Set("Replay-Nonce", "test-nonce")
176 w.WriteHeader(resCode)
177 }))
178 defer ts.Close()
179
180
181 ctx, cancel := context.WithCancel(context.Background())
182
183 var nretry int
184 backoff := func(n int, r *http.Request, res *http.Response) time.Duration {
185 nretry++
186 if n != nretry {
187 t.Errorf("n = %d; want %d", n, nretry)
188 }
189 if nretry == 3 {
190 cancel()
191 }
192
193 if r == nil {
194 t.Error("r is nil")
195 }
196 if res.StatusCode != resCode {
197 t.Errorf("res.StatusCode = %d; want %d", res.StatusCode, resCode)
198 }
199 return time.Millisecond
200 }
201
202 client := &Client{
203 Key: testKey,
204 RetryBackoff: backoff,
205 dir: &Directory{AuthzURL: ts.URL},
206 }
207 if _, err := client.Authorize(ctx, "example.com"); err == nil {
208 t.Error("err is nil")
209 }
210 if nretry != 3 {
211 t.Errorf("nretry = %d; want 3", nretry)
212 }
213 }
214
215 func TestUserAgent(t *testing.T) {
216 for _, custom := range []string{"", "CUSTOM_UA"} {
217 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
218 t.Log(r.UserAgent())
219 if s := "golang.org/x/crypto/acme"; !strings.Contains(r.UserAgent(), s) {
220 t.Errorf("expected User-Agent to contain %q, got %q", s, r.UserAgent())
221 }
222 if !strings.Contains(r.UserAgent(), custom) {
223 t.Errorf("expected User-Agent to contain %q, got %q", custom, r.UserAgent())
224 }
225
226 w.WriteHeader(http.StatusOK)
227 w.Write([]byte(`{"newOrder": "sure"}`))
228 }))
229 defer ts.Close()
230
231 client := &Client{
232 Key: testKey,
233 DirectoryURL: ts.URL,
234 UserAgent: custom,
235 }
236 if _, err := client.Discover(context.Background()); err != nil {
237 t.Errorf("client.Discover: %v", err)
238 }
239 }
240 }
241
242 func TestAccountKidLoop(t *testing.T) {
243
244
245
246 client := &Client{dir: &Directory{OrderURL: ":)"}}
247 _, _, err := client.postNoRetry(context.Background(), nil, "", nil)
248 if err == nil {
249 t.Fatal("Client.postNoRetry didn't fail with a nil key")
250 }
251 expected := "acme: Client.Key must be populated to make POST requests"
252 if err.Error() != expected {
253 t.Fatalf("Unexpected error returned: wanted %q, got %q", expected, err.Error())
254 }
255 }
256
View as plain text