blob: 79095ccae6eb55db2e8857978bb4b0d91f1a9a24 [file] [log] [blame]
Alex Vaghindf8d4712018-04-26 21:26:21 +02001// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package acme
6
7import (
8 "context"
9 "fmt"
10 "io/ioutil"
11 "net/http"
12 "net/http/httptest"
13 "reflect"
14 "strings"
15 "testing"
16 "time"
17)
18
19func TestDefaultBackoff(t *testing.T) {
20 tt := []struct {
21 nretry int
22 retryAfter string // Retry-After header
23 out time.Duration // expected min; max = min + jitter
24 }{
25 {-1, "", time.Second}, // verify the lower bound is 1
26 {0, "", time.Second}, // verify the lower bound is 1
27 {100, "", 10 * time.Second}, // verify the ceiling
28 {1, "3600", time.Hour}, // verify the header value is used
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 // + max jitter
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
48func 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: ioutil.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
79func 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 // We expect the client to do 2 head requests to fetch
86 // nonces, one to start and another after getting badNonce
87 return
88 }
89
Alex Vaghina8328652019-08-28 23:16:55 +020090 head, err := decodeJWSHead(r.Body)
Alex Vaghina49355c2018-06-20 11:14:27 +020091 switch {
92 case err != nil:
Alex Vaghindf8d4712018-04-26 21:26:21 +020093 t.Errorf("decodeJWSHead: %v", err)
Alex Vaghina49355c2018-06-20 11:14:27 +020094 case head.Nonce == "":
Alex Vaghindf8d4712018-04-26 21:26:21 +020095 t.Error("head.Nonce is empty")
Alex Vaghina49355c2018-06-20 11:14:27 +020096 case head.Nonce == "nonce1":
97 // Return a badNonce error to force the call to retry.
98 w.Header().Set("Retry-After", "0")
Alex Vaghindf8d4712018-04-26 21:26:21 +020099 w.WriteHeader(http.StatusBadRequest)
100 w.Write([]byte(`{"type":"urn:ietf:params:acme:error:badNonce"}`))
101 return
102 }
103 // Make client.Authorize happy; we're not testing its result.
104 w.WriteHeader(http.StatusCreated)
105 w.Write([]byte(`{"status":"valid"}`))
106 }))
107 defer ts.Close()
108
Alex Vaghina4c6cb32019-02-12 18:56:05 +0100109 client := &Client{
110 Key: testKey,
111 DirectoryURL: ts.URL,
112 dir: &Directory{AuthzURL: ts.URL},
113 }
Alex Vaghindf8d4712018-04-26 21:26:21 +0200114 // This call will fail with badNonce, causing a retry
115 if _, err := client.Authorize(context.Background(), "example.com"); err != nil {
116 t.Errorf("client.Authorize 1: %v", err)
117 }
118 if count != 4 {
119 t.Errorf("total requests count: %d; want 4", count)
120 }
121}
122
Alex Vaghina49355c2018-06-20 11:14:27 +0200123func 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 // Do no retries.
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
154func 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
Alex Vaghindf8d4712018-04-26 21:26:21 +0200172func 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 // Canceled in backoff.
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}
Filippo Valsordacc06ce42019-06-20 17:50:31 -0400214
215func 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(`{}`))
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}