1
2
3
4
5
6 package github
7
8 import (
9 "context"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "io/ioutil"
14 "net/http"
15 "net/http/httptest"
16 "net/url"
17 "os"
18 "path"
19 "reflect"
20 "strings"
21 "testing"
22 "time"
23
24 "github.com/google/go-cmp/cmp"
25 )
26
27 const (
28
29
30 baseURLPath = "/api-v3"
31 )
32
33
34
35
36 func setup() (client *Client, mux *http.ServeMux, serverURL string, teardown func()) {
37
38 mux = http.NewServeMux()
39
40
41
42
43 apiHandler := http.NewServeMux()
44 apiHandler.Handle(baseURLPath+"/", http.StripPrefix(baseURLPath, mux))
45 apiHandler.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
46 fmt.Fprintln(os.Stderr, "FAIL: Client.BaseURL path prefix is not preserved in the request URL:")
47 fmt.Fprintln(os.Stderr)
48 fmt.Fprintln(os.Stderr, "\t"+req.URL.String())
49 fmt.Fprintln(os.Stderr)
50 fmt.Fprintln(os.Stderr, "\tDid you accidentally use an absolute endpoint URL rather than relative?")
51 fmt.Fprintln(os.Stderr, "\tSee https://github.com/google/go-github/issues/752 for information.")
52 http.Error(w, "Client.BaseURL path prefix is not preserved in the request URL.", http.StatusInternalServerError)
53 })
54
55
56 server := httptest.NewServer(apiHandler)
57
58
59
60 client = NewClient(nil)
61 url, _ := url.Parse(server.URL + baseURLPath + "/")
62 client.BaseURL = url
63 client.UploadURL = url
64
65 return client, mux, server.URL, server.Close
66 }
67
68
69
70
71
72 func openTestFile(name, content string) (file *os.File, dir string, err error) {
73 dir, err = ioutil.TempDir("", "go-github")
74 if err != nil {
75 return nil, dir, err
76 }
77
78 file, err = os.OpenFile(path.Join(dir, name), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
79 if err != nil {
80 return nil, dir, err
81 }
82
83 fmt.Fprint(file, content)
84
85
86 file.Close()
87 file, err = os.Open(file.Name())
88 if err != nil {
89 return nil, dir, err
90 }
91
92 return file, dir, err
93 }
94
95 func testMethod(t *testing.T, r *http.Request, want string) {
96 t.Helper()
97 if got := r.Method; got != want {
98 t.Errorf("Request method: %v, want %v", got, want)
99 }
100 }
101
102 type values map[string]string
103
104 func testFormValues(t *testing.T, r *http.Request, values values) {
105 t.Helper()
106 want := url.Values{}
107 for k, v := range values {
108 want.Set(k, v)
109 }
110
111 r.ParseForm()
112 if got := r.Form; !cmp.Equal(got, want) {
113 t.Errorf("Request parameters: %v, want %v", got, want)
114 }
115 }
116
117 func testHeader(t *testing.T, r *http.Request, header string, want string) {
118 t.Helper()
119 if got := r.Header.Get(header); got != want {
120 t.Errorf("Header.Get(%q) returned %q, want %q", header, got, want)
121 }
122 }
123
124 func testURLParseError(t *testing.T, err error) {
125 t.Helper()
126 if err == nil {
127 t.Errorf("Expected error to be returned")
128 }
129 if err, ok := err.(*url.Error); !ok || err.Op != "parse" {
130 t.Errorf("Expected URL parse error, got %+v", err)
131 }
132 }
133
134 func testBody(t *testing.T, r *http.Request, want string) {
135 t.Helper()
136 b, err := ioutil.ReadAll(r.Body)
137 if err != nil {
138 t.Errorf("Error reading request body: %v", err)
139 }
140 if got := string(b); got != want {
141 t.Errorf("request Body is %s, want %s", got, want)
142 }
143 }
144
145
146
147 func testJSONMarshal(t *testing.T, v interface{}, want string) {
148 t.Helper()
149
150
151 u := reflect.New(reflect.TypeOf(v)).Interface()
152 if err := json.Unmarshal([]byte(want), &u); err != nil {
153 t.Errorf("Unable to unmarshal JSON for %v: %v", want, err)
154 }
155 w, err := json.Marshal(u)
156 if err != nil {
157 t.Errorf("Unable to marshal JSON for %#v", u)
158 }
159
160
161 j, err := json.Marshal(v)
162 if err != nil {
163 t.Errorf("Unable to marshal JSON for %#v", v)
164 }
165
166 if string(w) != string(j) {
167 t.Errorf("json.Marshal(%q) returned %s, want %s", v, j, w)
168 }
169 }
170
171
172
173 func testBadOptions(t *testing.T, methodName string, f func() error) {
174 t.Helper()
175 if methodName == "" {
176 t.Error("testBadOptions: must supply method methodName")
177 }
178 if err := f(); err == nil {
179 t.Errorf("bad options %v err = nil, want error", methodName)
180 }
181 }
182
183
184
185
186 func testNewRequestAndDoFailure(t *testing.T, methodName string, client *Client, f func() (*Response, error)) {
187 t.Helper()
188 if methodName == "" {
189 t.Error("testNewRequestAndDoFailure: must supply method methodName")
190 }
191
192 client.BaseURL.Path = ""
193 resp, err := f()
194 if resp != nil {
195 t.Errorf("client.BaseURL.Path='' %v resp = %#v, want nil", methodName, resp)
196 }
197 if err == nil {
198 t.Errorf("client.BaseURL.Path='' %v err = nil, want error", methodName)
199 }
200
201 client.BaseURL.Path = "/api-v3/"
202 client.rateLimits[0].Reset.Time = time.Now().Add(10 * time.Minute)
203 resp, err = f()
204 if bypass := resp.Request.Context().Value(bypassRateLimitCheck); bypass != nil {
205 return
206 }
207 if want := http.StatusForbidden; resp == nil || resp.Response.StatusCode != want {
208 if resp != nil {
209 t.Errorf("rate.Reset.Time > now %v resp = %#v, want StatusCode=%v", methodName, resp.Response, want)
210 } else {
211 t.Errorf("rate.Reset.Time > now %v resp = nil, want StatusCode=%v", methodName, want)
212 }
213 }
214 if err == nil {
215 t.Errorf("rate.Reset.Time > now %v err = nil, want error", methodName)
216 }
217 }
218
219
220 func testErrorResponseForStatusCode(t *testing.T, code int) {
221 t.Helper()
222 client, mux, _, teardown := setup()
223 defer teardown()
224
225 mux.HandleFunc("/repos/o/r/hooks", func(w http.ResponseWriter, r *http.Request) {
226 testMethod(t, r, "GET")
227 w.WriteHeader(code)
228 })
229
230 ctx := context.Background()
231 _, _, err := client.Repositories.ListHooks(ctx, "o", "r", nil)
232
233 switch e := err.(type) {
234 case *ErrorResponse:
235 case *RateLimitError:
236 case *AbuseRateLimitError:
237 if code != e.Response.StatusCode {
238 t.Error("Error response does not contain status code")
239 }
240 default:
241 t.Error("Unknown error response type")
242 }
243 }
244
245 func TestNewClient(t *testing.T) {
246 c := NewClient(nil)
247
248 if got, want := c.BaseURL.String(), defaultBaseURL; got != want {
249 t.Errorf("NewClient BaseURL is %v, want %v", got, want)
250 }
251 if got, want := c.UserAgent, userAgent; got != want {
252 t.Errorf("NewClient UserAgent is %v, want %v", got, want)
253 }
254
255 c2 := NewClient(nil)
256 if c.client == c2.client {
257 t.Error("NewClient returned same http.Clients, but they should differ")
258 }
259 }
260
261 func TestClient(t *testing.T) {
262 c := NewClient(nil)
263 c2 := c.Client()
264 if c.client == c2 {
265 t.Error("Client returned same http.Client, but should be different")
266 }
267 }
268
269 func TestNewEnterpriseClient(t *testing.T) {
270 baseURL := "https://custom-url/api/v3/"
271 uploadURL := "https://custom-upload-url/api/uploads/"
272 c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
273 if err != nil {
274 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
275 }
276
277 if got, want := c.BaseURL.String(), baseURL; got != want {
278 t.Errorf("NewClient BaseURL is %v, want %v", got, want)
279 }
280 if got, want := c.UploadURL.String(), uploadURL; got != want {
281 t.Errorf("NewClient UploadURL is %v, want %v", got, want)
282 }
283 }
284
285 func TestNewEnterpriseClient_addsTrailingSlashToURLs(t *testing.T) {
286 baseURL := "https://custom-url/api/v3"
287 uploadURL := "https://custom-upload-url/api/uploads"
288 formattedBaseURL := baseURL + "/"
289 formattedUploadURL := uploadURL + "/"
290
291 c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
292 if err != nil {
293 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
294 }
295
296 if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
297 t.Errorf("NewClient BaseURL is %v, want %v", got, want)
298 }
299 if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
300 t.Errorf("NewClient UploadURL is %v, want %v", got, want)
301 }
302 }
303
304 func TestNewEnterpriseClient_addsEnterpriseSuffixToURLs(t *testing.T) {
305 baseURL := "https://custom-url/"
306 uploadURL := "https://custom-upload-url/"
307 formattedBaseURL := baseURL + "api/v3/"
308 formattedUploadURL := uploadURL + "api/uploads/"
309
310 c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
311 if err != nil {
312 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
313 }
314
315 if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
316 t.Errorf("NewClient BaseURL is %v, want %v", got, want)
317 }
318 if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
319 t.Errorf("NewClient UploadURL is %v, want %v", got, want)
320 }
321 }
322
323 func TestNewEnterpriseClient_addsEnterpriseSuffixAndTrailingSlashToURLs(t *testing.T) {
324 baseURL := "https://custom-url"
325 uploadURL := "https://custom-upload-url"
326 formattedBaseURL := baseURL + "/api/v3/"
327 formattedUploadURL := uploadURL + "/api/uploads/"
328
329 c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
330 if err != nil {
331 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
332 }
333
334 if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
335 t.Errorf("NewClient BaseURL is %v, want %v", got, want)
336 }
337 if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
338 t.Errorf("NewClient UploadURL is %v, want %v", got, want)
339 }
340 }
341
342 func TestNewEnterpriseClient_badBaseURL(t *testing.T) {
343 baseURL := "bogus\nbase\nURL"
344 uploadURL := "https://custom-upload-url/api/uploads/"
345 if _, err := NewEnterpriseClient(baseURL, uploadURL, nil); err == nil {
346 t.Fatal("NewEnterpriseClient returned nil, expected error")
347 }
348 }
349
350 func TestNewEnterpriseClient_badUploadURL(t *testing.T) {
351 baseURL := "https://custom-url/api/v3/"
352 uploadURL := "bogus\nupload\nURL"
353 if _, err := NewEnterpriseClient(baseURL, uploadURL, nil); err == nil {
354 t.Fatal("NewEnterpriseClient returned nil, expected error")
355 }
356 }
357
358 func TestNewEnterpriseClient_URLHasExistingAPIPrefix_AddTrailingSlash(t *testing.T) {
359 baseURL := "https://api.custom-url"
360 uploadURL := "https://api.custom-upload-url"
361 formattedBaseURL := baseURL + "/"
362 formattedUploadURL := uploadURL + "/"
363
364 c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
365 if err != nil {
366 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
367 }
368
369 if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
370 t.Errorf("NewClient BaseURL is %v, want %v", got, want)
371 }
372 if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
373 t.Errorf("NewClient UploadURL is %v, want %v", got, want)
374 }
375 }
376
377 func TestNewEnterpriseClient_URLHasExistingAPIPrefixAndTrailingSlash(t *testing.T) {
378 baseURL := "https://api.custom-url/"
379 uploadURL := "https://api.custom-upload-url/"
380
381 c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
382 if err != nil {
383 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
384 }
385
386 if got, want := c.BaseURL.String(), baseURL; got != want {
387 t.Errorf("NewClient BaseURL is %v, want %v", got, want)
388 }
389 if got, want := c.UploadURL.String(), uploadURL; got != want {
390 t.Errorf("NewClient UploadURL is %v, want %v", got, want)
391 }
392 }
393
394 func TestNewEnterpriseClient_URLHasAPISubdomain_AddTrailingSlash(t *testing.T) {
395 baseURL := "https://catalog.api.custom-url"
396 uploadURL := "https://catalog.api.custom-upload-url"
397 formattedBaseURL := baseURL + "/"
398 formattedUploadURL := uploadURL + "/"
399
400 c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
401 if err != nil {
402 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
403 }
404
405 if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
406 t.Errorf("NewClient BaseURL is %v, want %v", got, want)
407 }
408 if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
409 t.Errorf("NewClient UploadURL is %v, want %v", got, want)
410 }
411 }
412
413 func TestNewEnterpriseClient_URLHasAPISubdomainAndTrailingSlash(t *testing.T) {
414 baseURL := "https://catalog.api.custom-url/"
415 uploadURL := "https://catalog.api.custom-upload-url/"
416
417 c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
418 if err != nil {
419 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
420 }
421
422 if got, want := c.BaseURL.String(), baseURL; got != want {
423 t.Errorf("NewClient BaseURL is %v, want %v", got, want)
424 }
425 if got, want := c.UploadURL.String(), uploadURL; got != want {
426 t.Errorf("NewClient UploadURL is %v, want %v", got, want)
427 }
428 }
429
430 func TestNewEnterpriseClient_URLIsNotAProperAPISubdomain_addsEnterpriseSuffixAndSlash(t *testing.T) {
431 baseURL := "https://cloud-api.custom-url"
432 uploadURL := "https://cloud-api.custom-upload-url"
433 formattedBaseURL := baseURL + "/api/v3/"
434 formattedUploadURL := uploadURL + "/api/uploads/"
435
436 c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
437 if err != nil {
438 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
439 }
440
441 if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
442 t.Errorf("NewClient BaseURL is %v, want %v", got, want)
443 }
444 if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
445 t.Errorf("NewClient UploadURL is %v, want %v", got, want)
446 }
447 }
448
449 func TestNewEnterpriseClient_URLIsNotAProperAPISubdomain_addsEnterpriseSuffix(t *testing.T) {
450 baseURL := "https://cloud-api.custom-url/"
451 uploadURL := "https://cloud-api.custom-upload-url/"
452 formattedBaseURL := baseURL + "api/v3/"
453 formattedUploadURL := uploadURL + "api/uploads/"
454
455 c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
456 if err != nil {
457 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
458 }
459
460 if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
461 t.Errorf("NewClient BaseURL is %v, want %v", got, want)
462 }
463 if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
464 t.Errorf("NewClient UploadURL is %v, want %v", got, want)
465 }
466 }
467
468
469 func TestClient_rateLimits(t *testing.T) {
470 if got, want := len(Client{}.rateLimits), reflect.TypeOf(RateLimits{}).NumField(); got != want {
471 t.Errorf("len(Client{}.rateLimits) is %v, want %v", got, want)
472 }
473 }
474
475 func TestRateLimits_String(t *testing.T) {
476 v := RateLimits{
477 Core: &Rate{},
478 Search: &Rate{},
479 GraphQL: &Rate{},
480 IntegrationManifest: &Rate{},
481 SourceImport: &Rate{},
482 CodeScanningUpload: &Rate{},
483 ActionsRunnerRegistration: &Rate{},
484 SCIM: &Rate{},
485 }
486 want := `github.RateLimits{Core:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, Search:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, GraphQL:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, IntegrationManifest:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, SourceImport:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, CodeScanningUpload:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, ActionsRunnerRegistration:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, SCIM:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}}`
487 if got := v.String(); got != want {
488 t.Errorf("RateLimits.String = %v, want %v", got, want)
489 }
490 }
491
492 func TestNewRequest(t *testing.T) {
493 c := NewClient(nil)
494
495 inURL, outURL := "/foo", defaultBaseURL+"foo"
496 inBody, outBody := &User{Login: String("l")}, `{"login":"l"}`+"\n"
497 req, _ := c.NewRequest("GET", inURL, inBody)
498
499
500 if got, want := req.URL.String(), outURL; got != want {
501 t.Errorf("NewRequest(%q) URL is %v, want %v", inURL, got, want)
502 }
503
504
505 body, _ := ioutil.ReadAll(req.Body)
506 if got, want := string(body), outBody; got != want {
507 t.Errorf("NewRequest(%q) Body is %v, want %v", inBody, got, want)
508 }
509
510
511 if got, want := req.Header.Get("User-Agent"), c.UserAgent; got != want {
512 t.Errorf("NewRequest() User-Agent is %v, want %v", got, want)
513 }
514 }
515
516 func TestNewRequest_invalidJSON(t *testing.T) {
517 c := NewClient(nil)
518
519 type T struct {
520 A map[interface{}]interface{}
521 }
522 _, err := c.NewRequest("GET", ".", &T{})
523
524 if err == nil {
525 t.Error("Expected error to be returned.")
526 }
527 if err, ok := err.(*json.UnsupportedTypeError); !ok {
528 t.Errorf("Expected a JSON error; got %#v.", err)
529 }
530 }
531
532 func TestNewRequest_badURL(t *testing.T) {
533 c := NewClient(nil)
534 _, err := c.NewRequest("GET", ":", nil)
535 testURLParseError(t, err)
536 }
537
538 func TestNewRequest_badMethod(t *testing.T) {
539 c := NewClient(nil)
540 if _, err := c.NewRequest("BOGUS\nMETHOD", ".", nil); err == nil {
541 t.Fatal("NewRequest returned nil; expected error")
542 }
543 }
544
545
546
547 func TestNewRequest_emptyUserAgent(t *testing.T) {
548 c := NewClient(nil)
549 c.UserAgent = ""
550 req, err := c.NewRequest("GET", ".", nil)
551 if err != nil {
552 t.Fatalf("NewRequest returned unexpected error: %v", err)
553 }
554 if _, ok := req.Header["User-Agent"]; ok {
555 t.Fatal("constructed request contains unexpected User-Agent header")
556 }
557 }
558
559
560
561
562
563
564
565 func TestNewRequest_emptyBody(t *testing.T) {
566 c := NewClient(nil)
567 req, err := c.NewRequest("GET", ".", nil)
568 if err != nil {
569 t.Fatalf("NewRequest returned unexpected error: %v", err)
570 }
571 if req.Body != nil {
572 t.Fatalf("constructed request contains a non-nil Body")
573 }
574 }
575
576 func TestNewRequest_errorForNoTrailingSlash(t *testing.T) {
577 tests := []struct {
578 rawurl string
579 wantError bool
580 }{
581 {rawurl: "https://example.com/api/v3", wantError: true},
582 {rawurl: "https://example.com/api/v3/", wantError: false},
583 }
584 c := NewClient(nil)
585 for _, test := range tests {
586 u, err := url.Parse(test.rawurl)
587 if err != nil {
588 t.Fatalf("url.Parse returned unexpected error: %v.", err)
589 }
590 c.BaseURL = u
591 if _, err := c.NewRequest(http.MethodGet, "test", nil); test.wantError && err == nil {
592 t.Fatalf("Expected error to be returned.")
593 } else if !test.wantError && err != nil {
594 t.Fatalf("NewRequest returned unexpected error: %v.", err)
595 }
596 }
597 }
598
599 func TestNewUploadRequest_badURL(t *testing.T) {
600 c := NewClient(nil)
601 _, err := c.NewUploadRequest(":", nil, 0, "")
602 testURLParseError(t, err)
603
604 const methodName = "NewUploadRequest"
605 testBadOptions(t, methodName, func() (err error) {
606 _, err = c.NewUploadRequest("\n", nil, -1, "\n")
607 return err
608 })
609 }
610
611 func TestNewUploadRequest_errorForNoTrailingSlash(t *testing.T) {
612 tests := []struct {
613 rawurl string
614 wantError bool
615 }{
616 {rawurl: "https://example.com/api/uploads", wantError: true},
617 {rawurl: "https://example.com/api/uploads/", wantError: false},
618 }
619 c := NewClient(nil)
620 for _, test := range tests {
621 u, err := url.Parse(test.rawurl)
622 if err != nil {
623 t.Fatalf("url.Parse returned unexpected error: %v.", err)
624 }
625 c.UploadURL = u
626 if _, err = c.NewUploadRequest("test", nil, 0, ""); test.wantError && err == nil {
627 t.Fatalf("Expected error to be returned.")
628 } else if !test.wantError && err != nil {
629 t.Fatalf("NewUploadRequest returned unexpected error: %v.", err)
630 }
631 }
632 }
633
634 func TestResponse_populatePageValues(t *testing.T) {
635 r := http.Response{
636 Header: http.Header{
637 "Link": {`<https://api.github.com/?page=1>; rel="first",` +
638 ` <https://api.github.com/?page=2>; rel="prev",` +
639 ` <https://api.github.com/?page=4>; rel="next",` +
640 ` <https://api.github.com/?page=5>; rel="last"`,
641 },
642 },
643 }
644
645 response := newResponse(&r)
646 if got, want := response.FirstPage, 1; got != want {
647 t.Errorf("response.FirstPage: %v, want %v", got, want)
648 }
649 if got, want := response.PrevPage, 2; want != got {
650 t.Errorf("response.PrevPage: %v, want %v", got, want)
651 }
652 if got, want := response.NextPage, 4; want != got {
653 t.Errorf("response.NextPage: %v, want %v", got, want)
654 }
655 if got, want := response.LastPage, 5; want != got {
656 t.Errorf("response.LastPage: %v, want %v", got, want)
657 }
658 if got, want := response.NextPageToken, ""; want != got {
659 t.Errorf("response.NextPageToken: %v, want %v", got, want)
660 }
661 }
662
663 func TestResponse_populateSinceValues(t *testing.T) {
664 r := http.Response{
665 Header: http.Header{
666 "Link": {`<https://api.github.com/?since=1>; rel="first",` +
667 ` <https://api.github.com/?since=2>; rel="prev",` +
668 ` <https://api.github.com/?since=4>; rel="next",` +
669 ` <https://api.github.com/?since=5>; rel="last"`,
670 },
671 },
672 }
673
674 response := newResponse(&r)
675 if got, want := response.FirstPage, 1; got != want {
676 t.Errorf("response.FirstPage: %v, want %v", got, want)
677 }
678 if got, want := response.PrevPage, 2; want != got {
679 t.Errorf("response.PrevPage: %v, want %v", got, want)
680 }
681 if got, want := response.NextPage, 4; want != got {
682 t.Errorf("response.NextPage: %v, want %v", got, want)
683 }
684 if got, want := response.LastPage, 5; want != got {
685 t.Errorf("response.LastPage: %v, want %v", got, want)
686 }
687 if got, want := response.NextPageToken, ""; want != got {
688 t.Errorf("response.NextPageToken: %v, want %v", got, want)
689 }
690 }
691
692 func TestResponse_SinceWithPage(t *testing.T) {
693 r := http.Response{
694 Header: http.Header{
695 "Link": {`<https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=1>; rel="first",` +
696 ` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=2>; rel="prev",` +
697 ` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=4>; rel="next",` +
698 ` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=5>; rel="last"`,
699 },
700 },
701 }
702
703 response := newResponse(&r)
704 if got, want := response.FirstPage, 1; got != want {
705 t.Errorf("response.FirstPage: %v, want %v", got, want)
706 }
707 if got, want := response.PrevPage, 2; want != got {
708 t.Errorf("response.PrevPage: %v, want %v", got, want)
709 }
710 if got, want := response.NextPage, 4; want != got {
711 t.Errorf("response.NextPage: %v, want %v", got, want)
712 }
713 if got, want := response.LastPage, 5; want != got {
714 t.Errorf("response.LastPage: %v, want %v", got, want)
715 }
716 if got, want := response.NextPageToken, ""; want != got {
717 t.Errorf("response.NextPageToken: %v, want %v", got, want)
718 }
719 }
720
721 func TestResponse_cursorPagination(t *testing.T) {
722 r := http.Response{
723 Header: http.Header{
724 "Status": {"200 OK"},
725 "Link": {`<https://api.github.com/resource?per_page=2&page=url-encoded-next-page-token>; rel="next"`},
726 },
727 }
728
729 response := newResponse(&r)
730 if got, want := response.FirstPage, 0; got != want {
731 t.Errorf("response.FirstPage: %v, want %v", got, want)
732 }
733 if got, want := response.PrevPage, 0; want != got {
734 t.Errorf("response.PrevPage: %v, want %v", got, want)
735 }
736 if got, want := response.NextPage, 0; want != got {
737 t.Errorf("response.NextPage: %v, want %v", got, want)
738 }
739 if got, want := response.LastPage, 0; want != got {
740 t.Errorf("response.LastPage: %v, want %v", got, want)
741 }
742 if got, want := response.NextPageToken, "url-encoded-next-page-token"; want != got {
743 t.Errorf("response.NextPageToken: %v, want %v", got, want)
744 }
745
746
747 r = http.Response{
748 Header: http.Header{
749 "Link": {
750 `<https://api.github.com/?cursor=v1_12345678>; rel="next"`,
751 },
752 },
753 }
754
755 response = newResponse(&r)
756 if got, want := response.Cursor, "v1_12345678"; got != want {
757 t.Errorf("response.Cursor: %v, want %v", got, want)
758 }
759 }
760
761 func TestResponse_beforeAfterPagination(t *testing.T) {
762 r := http.Response{
763 Header: http.Header{
764 "Link": {`<https://api.github.com/?after=a1b2c3&before=>; rel="next",` +
765 ` <https://api.github.com/?after=&before=>; rel="first",` +
766 ` <https://api.github.com/?after=&before=d4e5f6>; rel="prev",`,
767 },
768 },
769 }
770
771 response := newResponse(&r)
772 if got, want := response.Before, "d4e5f6"; got != want {
773 t.Errorf("response.Before: %v, want %v", got, want)
774 }
775 if got, want := response.After, "a1b2c3"; got != want {
776 t.Errorf("response.After: %v, want %v", got, want)
777 }
778 if got, want := response.FirstPage, 0; got != want {
779 t.Errorf("response.FirstPage: %v, want %v", got, want)
780 }
781 if got, want := response.PrevPage, 0; want != got {
782 t.Errorf("response.PrevPage: %v, want %v", got, want)
783 }
784 if got, want := response.NextPage, 0; want != got {
785 t.Errorf("response.NextPage: %v, want %v", got, want)
786 }
787 if got, want := response.LastPage, 0; want != got {
788 t.Errorf("response.LastPage: %v, want %v", got, want)
789 }
790 if got, want := response.NextPageToken, ""; want != got {
791 t.Errorf("response.NextPageToken: %v, want %v", got, want)
792 }
793 }
794
795 func TestResponse_populatePageValues_invalid(t *testing.T) {
796 r := http.Response{
797 Header: http.Header{
798 "Link": {`<https://api.github.com/?page=1>,` +
799 `<https://api.github.com/?page=abc>; rel="first",` +
800 `https://api.github.com/?page=2; rel="prev",` +
801 `<https://api.github.com/>; rel="next",` +
802 `<https://api.github.com/?page=>; rel="last"`,
803 },
804 },
805 }
806
807 response := newResponse(&r)
808 if got, want := response.FirstPage, 0; got != want {
809 t.Errorf("response.FirstPage: %v, want %v", got, want)
810 }
811 if got, want := response.PrevPage, 0; got != want {
812 t.Errorf("response.PrevPage: %v, want %v", got, want)
813 }
814 if got, want := response.NextPage, 0; got != want {
815 t.Errorf("response.NextPage: %v, want %v", got, want)
816 }
817 if got, want := response.LastPage, 0; got != want {
818 t.Errorf("response.LastPage: %v, want %v", got, want)
819 }
820
821
822 r = http.Response{
823 Header: http.Header{
824 "Link": {`<https://api.github.com/%?page=2>; rel="first"`},
825 },
826 }
827
828 response = newResponse(&r)
829 if got, want := response.FirstPage, 0; got != want {
830 t.Errorf("response.FirstPage: %v, want %v", got, want)
831 }
832 }
833
834 func TestResponse_populateSinceValues_invalid(t *testing.T) {
835 r := http.Response{
836 Header: http.Header{
837 "Link": {`<https://api.github.com/?since=1>,` +
838 `<https://api.github.com/?since=abc>; rel="first",` +
839 `https://api.github.com/?since=2; rel="prev",` +
840 `<https://api.github.com/>; rel="next",` +
841 `<https://api.github.com/?since=>; rel="last"`,
842 },
843 },
844 }
845
846 response := newResponse(&r)
847 if got, want := response.FirstPage, 0; got != want {
848 t.Errorf("response.FirstPage: %v, want %v", got, want)
849 }
850 if got, want := response.PrevPage, 0; got != want {
851 t.Errorf("response.PrevPage: %v, want %v", got, want)
852 }
853 if got, want := response.NextPage, 0; got != want {
854 t.Errorf("response.NextPage: %v, want %v", got, want)
855 }
856 if got, want := response.LastPage, 0; got != want {
857 t.Errorf("response.LastPage: %v, want %v", got, want)
858 }
859
860
861 r = http.Response{
862 Header: http.Header{
863 "Link": {`<https://api.github.com/%?since=2>; rel="first"`},
864 },
865 }
866
867 response = newResponse(&r)
868 if got, want := response.FirstPage, 0; got != want {
869 t.Errorf("response.FirstPage: %v, want %v", got, want)
870 }
871 }
872
873 func TestDo(t *testing.T) {
874 client, mux, _, teardown := setup()
875 defer teardown()
876
877 type foo struct {
878 A string
879 }
880
881 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
882 testMethod(t, r, "GET")
883 fmt.Fprint(w, `{"A":"a"}`)
884 })
885
886 req, _ := client.NewRequest("GET", ".", nil)
887 body := new(foo)
888 ctx := context.Background()
889 client.Do(ctx, req, body)
890
891 want := &foo{"a"}
892 if !cmp.Equal(body, want) {
893 t.Errorf("Response body = %v, want %v", body, want)
894 }
895 }
896
897 func TestDo_nilContext(t *testing.T) {
898 client, _, _, teardown := setup()
899 defer teardown()
900
901 req, _ := client.NewRequest("GET", ".", nil)
902 _, err := client.Do(nil, req, nil)
903
904 if !errors.Is(err, errNonNilContext) {
905 t.Errorf("Expected context must be non-nil error")
906 }
907 }
908
909 func TestDo_httpError(t *testing.T) {
910 client, mux, _, teardown := setup()
911 defer teardown()
912
913 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
914 http.Error(w, "Bad Request", 400)
915 })
916
917 req, _ := client.NewRequest("GET", ".", nil)
918 ctx := context.Background()
919 resp, err := client.Do(ctx, req, nil)
920
921 if err == nil {
922 t.Fatal("Expected HTTP 400 error, got no error.")
923 }
924 if resp.StatusCode != 400 {
925 t.Errorf("Expected HTTP 400 error, got %d status code.", resp.StatusCode)
926 }
927 }
928
929
930
931
932 func TestDo_redirectLoop(t *testing.T) {
933 client, mux, _, teardown := setup()
934 defer teardown()
935
936 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
937 http.Redirect(w, r, baseURLPath, http.StatusFound)
938 })
939
940 req, _ := client.NewRequest("GET", ".", nil)
941 ctx := context.Background()
942 _, err := client.Do(ctx, req, nil)
943
944 if err == nil {
945 t.Error("Expected error to be returned.")
946 }
947 if err, ok := err.(*url.Error); !ok {
948 t.Errorf("Expected a URL error; got %#v.", err)
949 }
950 }
951
952
953
954 func TestDo_sanitizeURL(t *testing.T) {
955 tp := &UnauthenticatedRateLimitedTransport{
956 ClientID: "id",
957 ClientSecret: "secret",
958 }
959 unauthedClient := NewClient(tp.Client())
960 unauthedClient.BaseURL = &url.URL{Scheme: "http", Host: "127.0.0.1:0", Path: "/"}
961 req, err := unauthedClient.NewRequest("GET", ".", nil)
962 if err != nil {
963 t.Fatalf("NewRequest returned unexpected error: %v", err)
964 }
965 ctx := context.Background()
966 _, err = unauthedClient.Do(ctx, req, nil)
967 if err == nil {
968 t.Fatal("Expected error to be returned.")
969 }
970 if strings.Contains(err.Error(), "client_secret=secret") {
971 t.Errorf("Do error contains secret, should be redacted:\n%q", err)
972 }
973 }
974
975 func TestDo_rateLimit(t *testing.T) {
976 client, mux, _, teardown := setup()
977 defer teardown()
978
979 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
980 w.Header().Set(headerRateLimit, "60")
981 w.Header().Set(headerRateRemaining, "59")
982 w.Header().Set(headerRateReset, "1372700873")
983 })
984
985 req, _ := client.NewRequest("GET", ".", nil)
986 ctx := context.Background()
987 resp, err := client.Do(ctx, req, nil)
988 if err != nil {
989 t.Errorf("Do returned unexpected error: %v", err)
990 }
991 if got, want := resp.Rate.Limit, 60; got != want {
992 t.Errorf("Client rate limit = %v, want %v", got, want)
993 }
994 if got, want := resp.Rate.Remaining, 59; got != want {
995 t.Errorf("Client rate remaining = %v, want %v", got, want)
996 }
997 reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
998 if resp.Rate.Reset.UTC() != reset {
999 t.Errorf("Client rate reset = %v, want %v", resp.Rate.Reset, reset)
1000 }
1001 }
1002
1003
1004 func TestDo_rateLimit_errorResponse(t *testing.T) {
1005 client, mux, _, teardown := setup()
1006 defer teardown()
1007
1008 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1009 w.Header().Set(headerRateLimit, "60")
1010 w.Header().Set(headerRateRemaining, "59")
1011 w.Header().Set(headerRateReset, "1372700873")
1012 http.Error(w, "Bad Request", 400)
1013 })
1014
1015 req, _ := client.NewRequest("GET", ".", nil)
1016 ctx := context.Background()
1017 resp, err := client.Do(ctx, req, nil)
1018 if err == nil {
1019 t.Error("Expected error to be returned.")
1020 }
1021 if _, ok := err.(*RateLimitError); ok {
1022 t.Errorf("Did not expect a *RateLimitError error; got %#v.", err)
1023 }
1024 if got, want := resp.Rate.Limit, 60; got != want {
1025 t.Errorf("Client rate limit = %v, want %v", got, want)
1026 }
1027 if got, want := resp.Rate.Remaining, 59; got != want {
1028 t.Errorf("Client rate remaining = %v, want %v", got, want)
1029 }
1030 reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
1031 if resp.Rate.Reset.UTC() != reset {
1032 t.Errorf("Client rate reset = %v, want %v", resp.Rate.Reset, reset)
1033 }
1034 }
1035
1036
1037 func TestDo_rateLimit_rateLimitError(t *testing.T) {
1038 client, mux, _, teardown := setup()
1039 defer teardown()
1040
1041 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1042 w.Header().Set(headerRateLimit, "60")
1043 w.Header().Set(headerRateRemaining, "0")
1044 w.Header().Set(headerRateReset, "1372700873")
1045 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1046 w.WriteHeader(http.StatusForbidden)
1047 fmt.Fprintln(w, `{
1048 "message": "API rate limit exceeded for xxx.xxx.xxx.xxx. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
1049 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1050 }`)
1051 })
1052
1053 req, _ := client.NewRequest("GET", ".", nil)
1054 ctx := context.Background()
1055 _, err := client.Do(ctx, req, nil)
1056
1057 if err == nil {
1058 t.Error("Expected error to be returned.")
1059 }
1060 rateLimitErr, ok := err.(*RateLimitError)
1061 if !ok {
1062 t.Fatalf("Expected a *RateLimitError error; got %#v.", err)
1063 }
1064 if got, want := rateLimitErr.Rate.Limit, 60; got != want {
1065 t.Errorf("rateLimitErr rate limit = %v, want %v", got, want)
1066 }
1067 if got, want := rateLimitErr.Rate.Remaining, 0; got != want {
1068 t.Errorf("rateLimitErr rate remaining = %v, want %v", got, want)
1069 }
1070 reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
1071 if rateLimitErr.Rate.Reset.UTC() != reset {
1072 t.Errorf("rateLimitErr rate reset = %v, want %v", rateLimitErr.Rate.Reset.UTC(), reset)
1073 }
1074 }
1075
1076
1077 func TestDo_rateLimit_noNetworkCall(t *testing.T) {
1078 client, mux, _, teardown := setup()
1079 defer teardown()
1080
1081 reset := time.Now().UTC().Add(time.Minute).Round(time.Second)
1082
1083 mux.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) {
1084 w.Header().Set(headerRateLimit, "60")
1085 w.Header().Set(headerRateRemaining, "0")
1086 w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix()))
1087 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1088 w.WriteHeader(http.StatusForbidden)
1089 fmt.Fprintln(w, `{
1090 "message": "API rate limit exceeded for xxx.xxx.xxx.xxx. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
1091 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1092 }`)
1093 })
1094
1095 madeNetworkCall := false
1096 mux.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) {
1097 madeNetworkCall = true
1098 })
1099
1100
1101 req, _ := client.NewRequest("GET", "first", nil)
1102 ctx := context.Background()
1103 client.Do(ctx, req, nil)
1104
1105
1106 req, _ = client.NewRequest("GET", "second", nil)
1107 _, err := client.Do(ctx, req, nil)
1108
1109 if madeNetworkCall {
1110 t.Fatal("Network call was made, even though rate limit is known to still be exceeded.")
1111 }
1112
1113 if err == nil {
1114 t.Error("Expected error to be returned.")
1115 }
1116 rateLimitErr, ok := err.(*RateLimitError)
1117 if !ok {
1118 t.Fatalf("Expected a *RateLimitError error; got %#v.", err)
1119 }
1120 if got, want := rateLimitErr.Rate.Limit, 60; got != want {
1121 t.Errorf("rateLimitErr rate limit = %v, want %v", got, want)
1122 }
1123 if got, want := rateLimitErr.Rate.Remaining, 0; got != want {
1124 t.Errorf("rateLimitErr rate remaining = %v, want %v", got, want)
1125 }
1126 if rateLimitErr.Rate.Reset.UTC() != reset {
1127 t.Errorf("rateLimitErr rate reset = %v, want %v", rateLimitErr.Rate.Reset.UTC(), reset)
1128 }
1129 }
1130
1131
1132 func TestDo_rateLimit_ignoredFromCache(t *testing.T) {
1133 client, mux, _, teardown := setup()
1134 defer teardown()
1135
1136 reset := time.Now().UTC().Add(time.Minute).Round(time.Second)
1137
1138
1139 mux.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) {
1140 w.Header().Set("X-From-Cache", "1")
1141 w.Header().Set(headerRateLimit, "60")
1142 w.Header().Set(headerRateRemaining, "0")
1143 w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix()))
1144 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1145 w.WriteHeader(http.StatusForbidden)
1146 fmt.Fprintln(w, `{
1147 "message": "API rate limit exceeded for xxx.xxx.xxx.xxx. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
1148 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1149 }`)
1150 })
1151
1152 madeNetworkCall := false
1153 mux.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) {
1154 madeNetworkCall = true
1155 })
1156
1157
1158 req, _ := client.NewRequest("GET", "first", nil)
1159 ctx := context.Background()
1160 client.Do(ctx, req, nil)
1161
1162
1163 req, _ = client.NewRequest("GET", "second", nil)
1164 _, err := client.Do(ctx, req, nil)
1165
1166 if err != nil {
1167 t.Fatalf("Second request failed, even though the rate limits from the cache should've been ignored: %v", err)
1168 }
1169 if !madeNetworkCall {
1170 t.Fatal("Network call was not made, even though the rate limits from the cache should've been ignored")
1171 }
1172 }
1173
1174
1175
1176 func TestDo_rateLimit_abuseRateLimitError(t *testing.T) {
1177 client, mux, _, teardown := setup()
1178 defer teardown()
1179
1180 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1181 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1182 w.WriteHeader(http.StatusForbidden)
1183
1184
1185 fmt.Fprintln(w, `{
1186 "message": "You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.",
1187 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1188 }`)
1189 })
1190
1191 req, _ := client.NewRequest("GET", ".", nil)
1192 ctx := context.Background()
1193 _, err := client.Do(ctx, req, nil)
1194
1195 if err == nil {
1196 t.Error("Expected error to be returned.")
1197 }
1198 abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
1199 if !ok {
1200 t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
1201 }
1202 if got, want := abuseRateLimitErr.RetryAfter, (*time.Duration)(nil); got != want {
1203 t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
1204 }
1205 }
1206
1207
1208
1209 func TestDo_rateLimit_abuseRateLimitErrorEnterprise(t *testing.T) {
1210 client, mux, _, teardown := setup()
1211 defer teardown()
1212
1213 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1214 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1215 w.WriteHeader(http.StatusForbidden)
1216
1217
1218
1219
1220 fmt.Fprintln(w, `{
1221 "message": "You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.",
1222 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1223 }`)
1224 })
1225
1226 req, _ := client.NewRequest("GET", ".", nil)
1227 ctx := context.Background()
1228 _, err := client.Do(ctx, req, nil)
1229
1230 if err == nil {
1231 t.Error("Expected error to be returned.")
1232 }
1233 abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
1234 if !ok {
1235 t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
1236 }
1237 if got, want := abuseRateLimitErr.RetryAfter, (*time.Duration)(nil); got != want {
1238 t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
1239 }
1240 }
1241
1242
1243 func TestDo_rateLimit_abuseRateLimitError_retryAfter(t *testing.T) {
1244 client, mux, _, teardown := setup()
1245 defer teardown()
1246
1247 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1248 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1249 w.Header().Set("Retry-After", "123")
1250 w.WriteHeader(http.StatusForbidden)
1251 fmt.Fprintln(w, `{
1252 "message": "You have triggered an abuse detection mechanism ...",
1253 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1254 }`)
1255 })
1256
1257 req, _ := client.NewRequest("GET", ".", nil)
1258 ctx := context.Background()
1259 _, err := client.Do(ctx, req, nil)
1260
1261 if err == nil {
1262 t.Error("Expected error to be returned.")
1263 }
1264 abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
1265 if !ok {
1266 t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
1267 }
1268 if abuseRateLimitErr.RetryAfter == nil {
1269 t.Fatalf("abuseRateLimitErr RetryAfter is nil, expected not-nil")
1270 }
1271 if got, want := *abuseRateLimitErr.RetryAfter, 123*time.Second; got != want {
1272 t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
1273 }
1274 }
1275
1276 func TestDo_noContent(t *testing.T) {
1277 client, mux, _, teardown := setup()
1278 defer teardown()
1279
1280 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1281 w.WriteHeader(http.StatusNoContent)
1282 })
1283
1284 var body json.RawMessage
1285
1286 req, _ := client.NewRequest("GET", ".", nil)
1287 ctx := context.Background()
1288 _, err := client.Do(ctx, req, &body)
1289 if err != nil {
1290 t.Fatalf("Do returned unexpected error: %v", err)
1291 }
1292 }
1293
1294 func TestSanitizeURL(t *testing.T) {
1295 tests := []struct {
1296 in, want string
1297 }{
1298 {"/?a=b", "/?a=b"},
1299 {"/?a=b&client_secret=secret", "/?a=b&client_secret=REDACTED"},
1300 {"/?a=b&client_id=id&client_secret=secret", "/?a=b&client_id=id&client_secret=REDACTED"},
1301 }
1302
1303 for _, tt := range tests {
1304 inURL, _ := url.Parse(tt.in)
1305 want, _ := url.Parse(tt.want)
1306
1307 if got := sanitizeURL(inURL); !cmp.Equal(got, want) {
1308 t.Errorf("sanitizeURL(%v) returned %v, want %v", tt.in, got, want)
1309 }
1310 }
1311 }
1312
1313 func TestCheckResponse(t *testing.T) {
1314 res := &http.Response{
1315 Request: &http.Request{},
1316 StatusCode: http.StatusBadRequest,
1317 Body: ioutil.NopCloser(strings.NewReader(`{"message":"m",
1318 "errors": [{"resource": "r", "field": "f", "code": "c"}],
1319 "block": {"reason": "dmca", "created_at": "2016-03-17T15:39:46Z"}}`)),
1320 }
1321 err := CheckResponse(res).(*ErrorResponse)
1322
1323 if err == nil {
1324 t.Errorf("Expected error response.")
1325 }
1326
1327 want := &ErrorResponse{
1328 Response: res,
1329 Message: "m",
1330 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1331 Block: &ErrorBlock{
1332 Reason: "dmca",
1333 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1334 },
1335 }
1336 if !errors.Is(err, want) {
1337 t.Errorf("Error = %#v, want %#v", err, want)
1338 }
1339 }
1340
1341 func TestCheckResponse_RateLimit(t *testing.T) {
1342 res := &http.Response{
1343 Request: &http.Request{},
1344 StatusCode: http.StatusForbidden,
1345 Header: http.Header{},
1346 Body: ioutil.NopCloser(strings.NewReader(`{"message":"m",
1347 "documentation_url": "url"}`)),
1348 }
1349 res.Header.Set(headerRateLimit, "60")
1350 res.Header.Set(headerRateRemaining, "0")
1351 res.Header.Set(headerRateReset, "243424")
1352
1353 err := CheckResponse(res).(*RateLimitError)
1354
1355 if err == nil {
1356 t.Errorf("Expected error response.")
1357 }
1358
1359 want := &RateLimitError{
1360 Rate: parseRate(res),
1361 Response: res,
1362 Message: "m",
1363 }
1364 if !errors.Is(err, want) {
1365 t.Errorf("Error = %#v, want %#v", err, want)
1366 }
1367 }
1368
1369 func TestCheckResponse_AbuseRateLimit(t *testing.T) {
1370 res := &http.Response{
1371 Request: &http.Request{},
1372 StatusCode: http.StatusForbidden,
1373 Body: ioutil.NopCloser(strings.NewReader(`{"message":"m",
1374 "documentation_url": "docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"}`)),
1375 }
1376 err := CheckResponse(res).(*AbuseRateLimitError)
1377
1378 if err == nil {
1379 t.Errorf("Expected error response.")
1380 }
1381
1382 want := &AbuseRateLimitError{
1383 Response: res,
1384 Message: "m",
1385 }
1386 if !errors.Is(err, want) {
1387 t.Errorf("Error = %#v, want %#v", err, want)
1388 }
1389 }
1390
1391 func TestCompareHttpResponse(t *testing.T) {
1392 testcases := map[string]struct {
1393 h1 *http.Response
1394 h2 *http.Response
1395 expected bool
1396 }{
1397 "both are nil": {
1398 expected: true,
1399 },
1400 "both are non nil - same StatusCode": {
1401 expected: true,
1402 h1: &http.Response{StatusCode: 200},
1403 h2: &http.Response{StatusCode: 200},
1404 },
1405 "both are non nil - different StatusCode": {
1406 expected: false,
1407 h1: &http.Response{StatusCode: 200},
1408 h2: &http.Response{StatusCode: 404},
1409 },
1410 "one is nil, other is not": {
1411 expected: false,
1412 h2: &http.Response{},
1413 },
1414 }
1415
1416 for name, tc := range testcases {
1417 t.Run(name, func(t *testing.T) {
1418 v := compareHTTPResponse(tc.h1, tc.h2)
1419 if tc.expected != v {
1420 t.Errorf("Expected %t, got %t for (%#v, %#v)", tc.expected, v, tc.h1, tc.h2)
1421 }
1422 })
1423 }
1424 }
1425
1426 func TestErrorResponse_Is(t *testing.T) {
1427 err := &ErrorResponse{
1428 Response: &http.Response{},
1429 Message: "m",
1430 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1431 Block: &ErrorBlock{
1432 Reason: "r",
1433 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1434 },
1435 DocumentationURL: "https://github.com",
1436 }
1437 testcases := map[string]struct {
1438 wantSame bool
1439 otherError error
1440 }{
1441 "errors are same": {
1442 wantSame: true,
1443 otherError: &ErrorResponse{
1444 Response: &http.Response{},
1445 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1446 Message: "m",
1447 Block: &ErrorBlock{
1448 Reason: "r",
1449 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1450 },
1451 DocumentationURL: "https://github.com",
1452 },
1453 },
1454 "errors have different values - Message": {
1455 wantSame: false,
1456 otherError: &ErrorResponse{
1457 Response: &http.Response{},
1458 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1459 Message: "m1",
1460 Block: &ErrorBlock{
1461 Reason: "r",
1462 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1463 },
1464 DocumentationURL: "https://github.com",
1465 },
1466 },
1467 "errors have different values - DocumentationURL": {
1468 wantSame: false,
1469 otherError: &ErrorResponse{
1470 Response: &http.Response{},
1471 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1472 Message: "m",
1473 Block: &ErrorBlock{
1474 Reason: "r",
1475 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1476 },
1477 DocumentationURL: "https://google.com",
1478 },
1479 },
1480 "errors have different values - Response is nil": {
1481 wantSame: false,
1482 otherError: &ErrorResponse{
1483 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1484 Message: "m",
1485 Block: &ErrorBlock{
1486 Reason: "r",
1487 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1488 },
1489 DocumentationURL: "https://github.com",
1490 },
1491 },
1492 "errors have different values - Errors": {
1493 wantSame: false,
1494 otherError: &ErrorResponse{
1495 Response: &http.Response{},
1496 Errors: []Error{{Resource: "r1", Field: "f1", Code: "c1"}},
1497 Message: "m",
1498 Block: &ErrorBlock{
1499 Reason: "r",
1500 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1501 },
1502 DocumentationURL: "https://github.com",
1503 },
1504 },
1505 "errors have different values - Errors have different length": {
1506 wantSame: false,
1507 otherError: &ErrorResponse{
1508 Response: &http.Response{},
1509 Errors: []Error{},
1510 Message: "m",
1511 Block: &ErrorBlock{
1512 Reason: "r",
1513 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1514 },
1515 DocumentationURL: "https://github.com",
1516 },
1517 },
1518 "errors have different values - Block - one is nil, other is not": {
1519 wantSame: false,
1520 otherError: &ErrorResponse{
1521 Response: &http.Response{},
1522 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1523 Message: "m",
1524 DocumentationURL: "https://github.com",
1525 },
1526 },
1527 "errors have different values - Block - different Reason": {
1528 wantSame: false,
1529 otherError: &ErrorResponse{
1530 Response: &http.Response{},
1531 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1532 Message: "m",
1533 Block: &ErrorBlock{
1534 Reason: "r1",
1535 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1536 },
1537 DocumentationURL: "https://github.com",
1538 },
1539 },
1540 "errors have different values - Block - different CreatedAt #1": {
1541 wantSame: false,
1542 otherError: &ErrorResponse{
1543 Response: &http.Response{},
1544 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1545 Message: "m",
1546 Block: &ErrorBlock{
1547 Reason: "r",
1548 CreatedAt: nil,
1549 },
1550 DocumentationURL: "https://github.com",
1551 },
1552 },
1553 "errors have different values - Block - different CreatedAt #2": {
1554 wantSame: false,
1555 otherError: &ErrorResponse{
1556 Response: &http.Response{},
1557 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1558 Message: "m",
1559 Block: &ErrorBlock{
1560 Reason: "r",
1561 CreatedAt: &Timestamp{time.Date(2017, time.March, 17, 15, 39, 46, 0, time.UTC)},
1562 },
1563 DocumentationURL: "https://github.com",
1564 },
1565 },
1566 "errors have different types": {
1567 wantSame: false,
1568 otherError: errors.New("Github"),
1569 },
1570 }
1571
1572 for name, tc := range testcases {
1573 t.Run(name, func(t *testing.T) {
1574 if tc.wantSame != err.Is(tc.otherError) {
1575 t.Errorf("Error = %#v, want %#v", err, tc.otherError)
1576 }
1577 })
1578 }
1579 }
1580
1581 func TestRateLimitError_Is(t *testing.T) {
1582 err := &RateLimitError{
1583 Response: &http.Response{},
1584 Message: "Github",
1585 }
1586 testcases := map[string]struct {
1587 wantSame bool
1588 err *RateLimitError
1589 otherError error
1590 }{
1591 "errors are same": {
1592 wantSame: true,
1593 err: err,
1594 otherError: &RateLimitError{
1595 Response: &http.Response{},
1596 Message: "Github",
1597 },
1598 },
1599 "errors are same - Response is nil": {
1600 wantSame: true,
1601 err: &RateLimitError{
1602 Message: "Github",
1603 },
1604 otherError: &RateLimitError{
1605 Message: "Github",
1606 },
1607 },
1608 "errors have different values - Rate": {
1609 wantSame: false,
1610 err: err,
1611 otherError: &RateLimitError{
1612 Rate: Rate{Limit: 10},
1613 Response: &http.Response{},
1614 Message: "Gitlab",
1615 },
1616 },
1617 "errors have different values - Response is nil": {
1618 wantSame: false,
1619 err: err,
1620 otherError: &RateLimitError{
1621 Message: "Github",
1622 },
1623 },
1624 "errors have different values - StatusCode": {
1625 wantSame: false,
1626 err: err,
1627 otherError: &RateLimitError{
1628 Response: &http.Response{StatusCode: 200},
1629 Message: "Github",
1630 },
1631 },
1632 "errors have different types": {
1633 wantSame: false,
1634 err: err,
1635 otherError: errors.New("Github"),
1636 },
1637 }
1638
1639 for name, tc := range testcases {
1640 t.Run(name, func(t *testing.T) {
1641 if tc.wantSame != tc.err.Is(tc.otherError) {
1642 t.Errorf("Error = %#v, want %#v", tc.err, tc.otherError)
1643 }
1644 })
1645 }
1646 }
1647
1648 func TestAbuseRateLimitError_Is(t *testing.T) {
1649 t1 := 1 * time.Second
1650 t2 := 2 * time.Second
1651 err := &AbuseRateLimitError{
1652 Response: &http.Response{},
1653 Message: "Github",
1654 RetryAfter: &t1,
1655 }
1656 testcases := map[string]struct {
1657 wantSame bool
1658 err *AbuseRateLimitError
1659 otherError error
1660 }{
1661 "errors are same": {
1662 wantSame: true,
1663 err: err,
1664 otherError: &AbuseRateLimitError{
1665 Response: &http.Response{},
1666 Message: "Github",
1667 RetryAfter: &t1,
1668 },
1669 },
1670 "errors are same - Response is nil": {
1671 wantSame: true,
1672 err: &AbuseRateLimitError{
1673 Message: "Github",
1674 RetryAfter: &t1,
1675 },
1676 otherError: &AbuseRateLimitError{
1677 Message: "Github",
1678 RetryAfter: &t1,
1679 },
1680 },
1681 "errors have different values - Message": {
1682 wantSame: false,
1683 err: err,
1684 otherError: &AbuseRateLimitError{
1685 Response: &http.Response{},
1686 Message: "Gitlab",
1687 RetryAfter: nil,
1688 },
1689 },
1690 "errors have different values - RetryAfter": {
1691 wantSame: false,
1692 err: err,
1693 otherError: &AbuseRateLimitError{
1694 Response: &http.Response{},
1695 Message: "Github",
1696 RetryAfter: &t2,
1697 },
1698 },
1699 "errors have different values - Response is nil": {
1700 wantSame: false,
1701 err: err,
1702 otherError: &AbuseRateLimitError{
1703 Message: "Github",
1704 RetryAfter: &t1,
1705 },
1706 },
1707 "errors have different values - StatusCode": {
1708 wantSame: false,
1709 err: err,
1710 otherError: &AbuseRateLimitError{
1711 Response: &http.Response{StatusCode: 200},
1712 Message: "Github",
1713 RetryAfter: &t1,
1714 },
1715 },
1716 "errors have different types": {
1717 wantSame: false,
1718 err: err,
1719 otherError: errors.New("Github"),
1720 },
1721 }
1722
1723 for name, tc := range testcases {
1724 t.Run(name, func(t *testing.T) {
1725 if tc.wantSame != tc.err.Is(tc.otherError) {
1726 t.Errorf("Error = %#v, want %#v", tc.err, tc.otherError)
1727 }
1728 })
1729 }
1730 }
1731
1732 func TestAcceptedError_Is(t *testing.T) {
1733 err := &AcceptedError{Raw: []byte("Github")}
1734 testcases := map[string]struct {
1735 wantSame bool
1736 otherError error
1737 }{
1738 "errors are same": {
1739 wantSame: true,
1740 otherError: &AcceptedError{Raw: []byte("Github")},
1741 },
1742 "errors have different values": {
1743 wantSame: false,
1744 otherError: &AcceptedError{Raw: []byte("Gitlab")},
1745 },
1746 "errors have different types": {
1747 wantSame: false,
1748 otherError: errors.New("Github"),
1749 },
1750 }
1751
1752 for name, tc := range testcases {
1753 t.Run(name, func(t *testing.T) {
1754 if tc.wantSame != err.Is(tc.otherError) {
1755 t.Errorf("Error = %#v, want %#v", err, tc.otherError)
1756 }
1757 })
1758 }
1759 }
1760
1761
1762 func TestCheckResponse_noBody(t *testing.T) {
1763 res := &http.Response{
1764 Request: &http.Request{},
1765 StatusCode: http.StatusBadRequest,
1766 Body: ioutil.NopCloser(strings.NewReader("")),
1767 }
1768 err := CheckResponse(res).(*ErrorResponse)
1769
1770 if err == nil {
1771 t.Errorf("Expected error response.")
1772 }
1773
1774 want := &ErrorResponse{
1775 Response: res,
1776 }
1777 if !errors.Is(err, want) {
1778 t.Errorf("Error = %#v, want %#v", err, want)
1779 }
1780 }
1781
1782 func TestCheckResponse_unexpectedErrorStructure(t *testing.T) {
1783 httpBody := `{"message":"m", "errors": ["error 1"]}`
1784 res := &http.Response{
1785 Request: &http.Request{},
1786 StatusCode: http.StatusBadRequest,
1787 Body: ioutil.NopCloser(strings.NewReader(httpBody)),
1788 }
1789 err := CheckResponse(res).(*ErrorResponse)
1790
1791 if err == nil {
1792 t.Errorf("Expected error response.")
1793 }
1794
1795 want := &ErrorResponse{
1796 Response: res,
1797 Message: "m",
1798 Errors: []Error{{Message: "error 1"}},
1799 }
1800 if !errors.Is(err, want) {
1801 t.Errorf("Error = %#v, want %#v", err, want)
1802 }
1803 data, err2 := ioutil.ReadAll(err.Response.Body)
1804 if err2 != nil {
1805 t.Fatalf("failed to read response body: %v", err)
1806 }
1807 if got := string(data); got != httpBody {
1808 t.Errorf("ErrorResponse.Response.Body = %q, want %q", got, httpBody)
1809 }
1810 }
1811
1812 func TestParseBooleanResponse_true(t *testing.T) {
1813 result, err := parseBoolResponse(nil)
1814 if err != nil {
1815 t.Errorf("parseBoolResponse returned error: %+v", err)
1816 }
1817
1818 if want := true; result != want {
1819 t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want)
1820 }
1821 }
1822
1823 func TestParseBooleanResponse_false(t *testing.T) {
1824 v := &ErrorResponse{Response: &http.Response{StatusCode: http.StatusNotFound}}
1825 result, err := parseBoolResponse(v)
1826 if err != nil {
1827 t.Errorf("parseBoolResponse returned error: %+v", err)
1828 }
1829
1830 if want := false; result != want {
1831 t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want)
1832 }
1833 }
1834
1835 func TestParseBooleanResponse_error(t *testing.T) {
1836 v := &ErrorResponse{Response: &http.Response{StatusCode: http.StatusBadRequest}}
1837 result, err := parseBoolResponse(v)
1838
1839 if err == nil {
1840 t.Errorf("Expected error to be returned.")
1841 }
1842
1843 if want := false; result != want {
1844 t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want)
1845 }
1846 }
1847
1848 func TestErrorResponse_Error(t *testing.T) {
1849 res := &http.Response{Request: &http.Request{}}
1850 err := ErrorResponse{Message: "m", Response: res}
1851 if err.Error() == "" {
1852 t.Errorf("Expected non-empty ErrorResponse.Error()")
1853 }
1854 }
1855
1856 func TestError_Error(t *testing.T) {
1857 err := Error{}
1858 if err.Error() == "" {
1859 t.Errorf("Expected non-empty Error.Error()")
1860 }
1861 }
1862
1863 func TestRateLimits(t *testing.T) {
1864 client, mux, _, teardown := setup()
1865 defer teardown()
1866
1867 mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) {
1868 testMethod(t, r, "GET")
1869 fmt.Fprint(w, `{"resources":{
1870 "core": {"limit":2,"remaining":1,"reset":1372700873},
1871 "search": {"limit":3,"remaining":2,"reset":1372700874},
1872 "graphql": {"limit":4,"remaining":3,"reset":1372700875},
1873 "integration_manifest": {"limit":5,"remaining":4,"reset":1372700876},
1874 "source_import": {"limit":6,"remaining":5,"reset":1372700877},
1875 "code_scanning_upload": {"limit":7,"remaining":6,"reset":1372700878},
1876 "actions_runner_registration": {"limit":8,"remaining":7,"reset":1372700879},
1877 "scim": {"limit":9,"remaining":8,"reset":1372700880}
1878 }}`)
1879 })
1880
1881 ctx := context.Background()
1882 rate, _, err := client.RateLimits(ctx)
1883 if err != nil {
1884 t.Errorf("RateLimits returned error: %v", err)
1885 }
1886
1887 want := &RateLimits{
1888 Core: &Rate{
1889 Limit: 2,
1890 Remaining: 1,
1891 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC).Local()},
1892 },
1893 Search: &Rate{
1894 Limit: 3,
1895 Remaining: 2,
1896 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()},
1897 },
1898 GraphQL: &Rate{
1899 Limit: 4,
1900 Remaining: 3,
1901 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 55, 0, time.UTC).Local()},
1902 },
1903 IntegrationManifest: &Rate{
1904 Limit: 5,
1905 Remaining: 4,
1906 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 56, 0, time.UTC).Local()},
1907 },
1908 SourceImport: &Rate{
1909 Limit: 6,
1910 Remaining: 5,
1911 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 57, 0, time.UTC).Local()},
1912 },
1913 CodeScanningUpload: &Rate{
1914 Limit: 7,
1915 Remaining: 6,
1916 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 58, 0, time.UTC).Local()},
1917 },
1918 ActionsRunnerRegistration: &Rate{
1919 Limit: 8,
1920 Remaining: 7,
1921 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 59, 0, time.UTC).Local()},
1922 },
1923 SCIM: &Rate{
1924 Limit: 9,
1925 Remaining: 8,
1926 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 48, 00, 0, time.UTC).Local()},
1927 },
1928 }
1929 if !cmp.Equal(rate, want) {
1930 t.Errorf("RateLimits returned %+v, want %+v", rate, want)
1931 }
1932
1933 if got, want := client.rateLimits[coreCategory], *want.Core; got != want {
1934 t.Errorf("client.rateLimits[coreCategory] is %+v, want %+v", got, want)
1935 }
1936 if got, want := client.rateLimits[searchCategory], *want.Search; got != want {
1937 t.Errorf("client.rateLimits[searchCategory] is %+v, want %+v", got, want)
1938 }
1939 if got, want := client.rateLimits[graphqlCategory], *want.GraphQL; got != want {
1940 t.Errorf("client.rateLimits[graphqlCategory] is %+v, want %+v", got, want)
1941 }
1942 if got, want := client.rateLimits[integrationManifestCategory], *want.IntegrationManifest; got != want {
1943 t.Errorf("client.rateLimits[integrationManifestCategory] is %+v, want %+v", got, want)
1944 }
1945 if got, want := client.rateLimits[sourceImportCategory], *want.SourceImport; got != want {
1946 t.Errorf("client.rateLimits[sourceImportCategory] is %+v, want %+v", got, want)
1947 }
1948 if got, want := client.rateLimits[codeScanningUploadCategory], *want.CodeScanningUpload; got != want {
1949 t.Errorf("client.rateLimits[codeScanningUploadCategory] is %+v, want %+v", got, want)
1950 }
1951 if got, want := client.rateLimits[actionsRunnerRegistrationCategory], *want.ActionsRunnerRegistration; got != want {
1952 t.Errorf("client.rateLimits[actionsRunnerRegistrationCategory] is %+v, want %+v", got, want)
1953 }
1954 if got, want := client.rateLimits[scimCategory], *want.SCIM; got != want {
1955 t.Errorf("client.rateLimits[scimCategory] is %+v, want %+v", got, want)
1956 }
1957 }
1958
1959 func TestRateLimits_coverage(t *testing.T) {
1960 client, _, _, teardown := setup()
1961 defer teardown()
1962
1963 ctx := context.Background()
1964
1965 const methodName = "RateLimits"
1966 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
1967 _, resp, err := client.RateLimits(ctx)
1968 return resp, err
1969 })
1970 }
1971
1972 func TestRateLimits_overQuota(t *testing.T) {
1973 client, mux, _, teardown := setup()
1974 defer teardown()
1975
1976 client.rateLimits[coreCategory] = Rate{
1977 Limit: 1,
1978 Remaining: 0,
1979 Reset: Timestamp{time.Now().Add(time.Hour).Local()},
1980 }
1981 mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) {
1982 fmt.Fprint(w, `{"resources":{
1983 "core": {"limit":2,"remaining":1,"reset":1372700873},
1984 "search": {"limit":3,"remaining":2,"reset":1372700874}
1985 }}`)
1986 })
1987
1988 ctx := context.Background()
1989 rate, _, err := client.RateLimits(ctx)
1990 if err != nil {
1991 t.Errorf("RateLimits returned error: %v", err)
1992 }
1993
1994 want := &RateLimits{
1995 Core: &Rate{
1996 Limit: 2,
1997 Remaining: 1,
1998 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC).Local()},
1999 },
2000 Search: &Rate{
2001 Limit: 3,
2002 Remaining: 2,
2003 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()},
2004 },
2005 }
2006 if !cmp.Equal(rate, want) {
2007 t.Errorf("RateLimits returned %+v, want %+v", rate, want)
2008 }
2009
2010 if got, want := client.rateLimits[coreCategory], *want.Core; got != want {
2011 t.Errorf("client.rateLimits[coreCategory] is %+v, want %+v", got, want)
2012 }
2013 if got, want := client.rateLimits[searchCategory], *want.Search; got != want {
2014 t.Errorf("client.rateLimits[searchCategory] is %+v, want %+v", got, want)
2015 }
2016 }
2017
2018 func TestSetCredentialsAsHeaders(t *testing.T) {
2019 req := new(http.Request)
2020 id, secret := "id", "secret"
2021 modifiedRequest := setCredentialsAsHeaders(req, id, secret)
2022
2023 actualID, actualSecret, ok := modifiedRequest.BasicAuth()
2024 if !ok {
2025 t.Errorf("request does not contain basic credentials")
2026 }
2027
2028 if actualID != id {
2029 t.Errorf("id is %s, want %s", actualID, id)
2030 }
2031
2032 if actualSecret != secret {
2033 t.Errorf("secret is %s, want %s", actualSecret, secret)
2034 }
2035 }
2036
2037 func TestUnauthenticatedRateLimitedTransport(t *testing.T) {
2038 client, mux, _, teardown := setup()
2039 defer teardown()
2040
2041 clientID, clientSecret := "id", "secret"
2042 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
2043 id, secret, ok := r.BasicAuth()
2044 if !ok {
2045 t.Errorf("request does not contain basic auth credentials")
2046 }
2047 if id != clientID {
2048 t.Errorf("request contained basic auth username %q, want %q", id, clientID)
2049 }
2050 if secret != clientSecret {
2051 t.Errorf("request contained basic auth password %q, want %q", secret, clientSecret)
2052 }
2053 })
2054
2055 tp := &UnauthenticatedRateLimitedTransport{
2056 ClientID: clientID,
2057 ClientSecret: clientSecret,
2058 }
2059 unauthedClient := NewClient(tp.Client())
2060 unauthedClient.BaseURL = client.BaseURL
2061 req, _ := unauthedClient.NewRequest("GET", ".", nil)
2062 ctx := context.Background()
2063 unauthedClient.Do(ctx, req, nil)
2064 }
2065
2066 func TestUnauthenticatedRateLimitedTransport_missingFields(t *testing.T) {
2067
2068 tp := &UnauthenticatedRateLimitedTransport{
2069 ClientSecret: "secret",
2070 }
2071 _, err := tp.RoundTrip(nil)
2072 if err == nil {
2073 t.Errorf("Expected error to be returned")
2074 }
2075
2076
2077 tp = &UnauthenticatedRateLimitedTransport{
2078 ClientID: "id",
2079 }
2080 _, err = tp.RoundTrip(nil)
2081 if err == nil {
2082 t.Errorf("Expected error to be returned")
2083 }
2084 }
2085
2086 func TestUnauthenticatedRateLimitedTransport_transport(t *testing.T) {
2087
2088 tp := &UnauthenticatedRateLimitedTransport{
2089 ClientID: "id",
2090 ClientSecret: "secret",
2091 }
2092 if tp.transport() != http.DefaultTransport {
2093 t.Errorf("Expected http.DefaultTransport to be used.")
2094 }
2095
2096
2097 tp = &UnauthenticatedRateLimitedTransport{
2098 ClientID: "id",
2099 ClientSecret: "secret",
2100 Transport: &http.Transport{},
2101 }
2102 if tp.transport() == http.DefaultTransport {
2103 t.Errorf("Expected custom transport to be used.")
2104 }
2105 }
2106
2107 func TestBasicAuthTransport(t *testing.T) {
2108 client, mux, _, teardown := setup()
2109 defer teardown()
2110
2111 username, password, otp := "u", "p", "123456"
2112
2113 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
2114 u, p, ok := r.BasicAuth()
2115 if !ok {
2116 t.Errorf("request does not contain basic auth credentials")
2117 }
2118 if u != username {
2119 t.Errorf("request contained basic auth username %q, want %q", u, username)
2120 }
2121 if p != password {
2122 t.Errorf("request contained basic auth password %q, want %q", p, password)
2123 }
2124 if got, want := r.Header.Get(headerOTP), otp; got != want {
2125 t.Errorf("request contained OTP %q, want %q", got, want)
2126 }
2127 })
2128
2129 tp := &BasicAuthTransport{
2130 Username: username,
2131 Password: password,
2132 OTP: otp,
2133 }
2134 basicAuthClient := NewClient(tp.Client())
2135 basicAuthClient.BaseURL = client.BaseURL
2136 req, _ := basicAuthClient.NewRequest("GET", ".", nil)
2137 ctx := context.Background()
2138 basicAuthClient.Do(ctx, req, nil)
2139 }
2140
2141 func TestBasicAuthTransport_transport(t *testing.T) {
2142
2143 tp := &BasicAuthTransport{}
2144 if tp.transport() != http.DefaultTransport {
2145 t.Errorf("Expected http.DefaultTransport to be used.")
2146 }
2147
2148
2149 tp = &BasicAuthTransport{
2150 Transport: &http.Transport{},
2151 }
2152 if tp.transport() == http.DefaultTransport {
2153 t.Errorf("Expected custom transport to be used.")
2154 }
2155 }
2156
2157 func TestFormatRateReset(t *testing.T) {
2158 d := 120*time.Minute + 12*time.Second
2159 got := formatRateReset(d)
2160 want := "[rate reset in 120m12s]"
2161 if got != want {
2162 t.Errorf("Format is wrong. got: %v, want: %v", got, want)
2163 }
2164
2165 d = 14*time.Minute + 2*time.Second
2166 got = formatRateReset(d)
2167 want = "[rate reset in 14m02s]"
2168 if got != want {
2169 t.Errorf("Format is wrong. got: %v, want: %v", got, want)
2170 }
2171
2172 d = 2*time.Minute + 2*time.Second
2173 got = formatRateReset(d)
2174 want = "[rate reset in 2m02s]"
2175 if got != want {
2176 t.Errorf("Format is wrong. got: %v, want: %v", got, want)
2177 }
2178
2179 d = 12 * time.Second
2180 got = formatRateReset(d)
2181 want = "[rate reset in 12s]"
2182 if got != want {
2183 t.Errorf("Format is wrong. got: %v, want: %v", got, want)
2184 }
2185
2186 d = -1 * (2*time.Hour + 2*time.Second)
2187 got = formatRateReset(d)
2188 want = "[rate limit was reset 120m02s ago]"
2189 if got != want {
2190 t.Errorf("Format is wrong. got: %v, want: %v", got, want)
2191 }
2192 }
2193
2194 func TestNestedStructAccessorNoPanic(t *testing.T) {
2195 issue := &Issue{User: nil}
2196 got := issue.GetUser().GetPlan().GetName()
2197 want := ""
2198 if got != want {
2199 t.Errorf("Issues.Get.GetUser().GetPlan().GetName() returned %+v, want %+v", got, want)
2200 }
2201 }
2202
2203 func TestTwoFactorAuthError(t *testing.T) {
2204 u, err := url.Parse("https://example.com")
2205 if err != nil {
2206 t.Fatal(err)
2207 }
2208
2209 e := &TwoFactorAuthError{
2210 Response: &http.Response{
2211 Request: &http.Request{Method: "PUT", URL: u},
2212 StatusCode: http.StatusTooManyRequests,
2213 },
2214 Message: "<msg>",
2215 }
2216 if got, want := e.Error(), "PUT https://example.com: 429 <msg> []"; got != want {
2217 t.Errorf("TwoFactorAuthError = %q, want %q", got, want)
2218 }
2219 }
2220
2221 func TestRateLimitError(t *testing.T) {
2222 u, err := url.Parse("https://example.com")
2223 if err != nil {
2224 t.Fatal(err)
2225 }
2226
2227 r := &RateLimitError{
2228 Response: &http.Response{
2229 Request: &http.Request{Method: "PUT", URL: u},
2230 StatusCode: http.StatusTooManyRequests,
2231 },
2232 Message: "<msg>",
2233 }
2234 if got, want := r.Error(), "PUT https://example.com: 429 <msg> [rate limit was reset"; !strings.Contains(got, want) {
2235 t.Errorf("RateLimitError = %q, want %q", got, want)
2236 }
2237 }
2238
2239 func TestAcceptedError(t *testing.T) {
2240 a := &AcceptedError{}
2241 if got, want := a.Error(), "try again later"; !strings.Contains(got, want) {
2242 t.Errorf("AcceptedError = %q, want %q", got, want)
2243 }
2244 }
2245
2246 func TestAbuseRateLimitError(t *testing.T) {
2247 u, err := url.Parse("https://example.com")
2248 if err != nil {
2249 t.Fatal(err)
2250 }
2251
2252 r := &AbuseRateLimitError{
2253 Response: &http.Response{
2254 Request: &http.Request{Method: "PUT", URL: u},
2255 StatusCode: http.StatusTooManyRequests,
2256 },
2257 Message: "<msg>",
2258 }
2259 if got, want := r.Error(), "PUT https://example.com: 429 <msg>"; got != want {
2260 t.Errorf("AbuseRateLimitError = %q, want %q", got, want)
2261 }
2262 }
2263
2264 func TestAddOptions_QueryValues(t *testing.T) {
2265 if _, err := addOptions("yo", ""); err == nil {
2266 t.Error("addOptions err = nil, want error")
2267 }
2268 }
2269
2270 func TestBareDo_returnsOpenBody(t *testing.T) {
2271 client, mux, _, teardown := setup()
2272 defer teardown()
2273
2274 expectedBody := "Hello from the other side !"
2275
2276 mux.HandleFunc("/test-url", func(w http.ResponseWriter, r *http.Request) {
2277 testMethod(t, r, "GET")
2278 fmt.Fprint(w, expectedBody)
2279 })
2280
2281 ctx := context.Background()
2282 req, err := client.NewRequest("GET", "test-url", nil)
2283 if err != nil {
2284 t.Fatalf("client.NewRequest returned error: %v", err)
2285 }
2286
2287 resp, err := client.BareDo(ctx, req)
2288 if err != nil {
2289 t.Fatalf("client.BareDo returned error: %v", err)
2290 }
2291
2292 got, err := ioutil.ReadAll(resp.Body)
2293 if err != nil {
2294 t.Fatalf("ioutil.ReadAll returned error: %v", err)
2295 }
2296 if string(got) != expectedBody {
2297 t.Fatalf("Expected %q, got %q", expectedBody, string(got))
2298 }
2299 if err := resp.Body.Close(); err != nil {
2300 t.Fatalf("resp.Body.Close() returned error: %v", err)
2301 }
2302 }
2303
2304
2305 type roundTripperFunc func(*http.Request) (*http.Response, error)
2306
2307 func (fn roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
2308 return fn(r)
2309 }
2310
2311 func TestErrorResponse_Marshal(t *testing.T) {
2312 testJSONMarshal(t, &ErrorResponse{}, "{}")
2313
2314 u := &ErrorResponse{
2315 Message: "msg",
2316 Errors: []Error{
2317 {
2318 Resource: "res",
2319 Field: "f",
2320 Code: "c",
2321 Message: "msg",
2322 },
2323 },
2324 Block: &ErrorBlock{
2325 Reason: "reason",
2326 CreatedAt: &Timestamp{referenceTime},
2327 },
2328 DocumentationURL: "doc",
2329 }
2330
2331 want := `{
2332 "message": "msg",
2333 "errors": [
2334 {
2335 "resource": "res",
2336 "field": "f",
2337 "code": "c",
2338 "message": "msg"
2339 }
2340 ],
2341 "block": {
2342 "reason": "reason",
2343 "created_at": ` + referenceTimeStr + `
2344 },
2345 "documentation_url": "doc"
2346 }`
2347
2348 testJSONMarshal(t, u, want)
2349 }
2350
2351 func TestErrorBlock_Marshal(t *testing.T) {
2352 testJSONMarshal(t, &ErrorBlock{}, "{}")
2353
2354 u := &ErrorBlock{
2355 Reason: "reason",
2356 CreatedAt: &Timestamp{referenceTime},
2357 }
2358
2359 want := `{
2360 "reason": "reason",
2361 "created_at": ` + referenceTimeStr + `
2362 }`
2363
2364 testJSONMarshal(t, u, want)
2365 }
2366
2367 func TestRateLimitError_Marshal(t *testing.T) {
2368 testJSONMarshal(t, &RateLimitError{}, "{}")
2369
2370 u := &RateLimitError{
2371 Rate: Rate{
2372 Limit: 1,
2373 Remaining: 1,
2374 Reset: Timestamp{referenceTime},
2375 },
2376 Message: "msg",
2377 }
2378
2379 want := `{
2380 "Rate": {
2381 "limit": 1,
2382 "remaining": 1,
2383 "reset": ` + referenceTimeStr + `
2384 },
2385 "message": "msg"
2386 }`
2387
2388 testJSONMarshal(t, u, want)
2389 }
2390
2391 func TestAbuseRateLimitError_Marshal(t *testing.T) {
2392 testJSONMarshal(t, &AbuseRateLimitError{}, "{}")
2393
2394 u := &AbuseRateLimitError{
2395 Message: "msg",
2396 }
2397
2398 want := `{
2399 "message": "msg"
2400 }`
2401
2402 testJSONMarshal(t, u, want)
2403 }
2404
2405 func TestError_Marshal(t *testing.T) {
2406 testJSONMarshal(t, &Error{}, "{}")
2407
2408 u := &Error{
2409 Resource: "res",
2410 Field: "field",
2411 Code: "code",
2412 Message: "msg",
2413 }
2414
2415 want := `{
2416 "resource": "res",
2417 "field": "field",
2418 "code": "code",
2419 "message": "msg"
2420 }`
2421
2422 testJSONMarshal(t, u, want)
2423 }
2424
2425 func TestRate_Marshal(t *testing.T) {
2426 testJSONMarshal(t, &Rate{}, "{}")
2427
2428 u := &Rate{
2429 Limit: 1,
2430 Remaining: 1,
2431 Reset: Timestamp{referenceTime},
2432 }
2433
2434 want := `{
2435 "limit": 1,
2436 "remaining": 1,
2437 "reset": ` + referenceTimeStr + `
2438 }`
2439
2440 testJSONMarshal(t, u, want)
2441 }
2442
2443 func TestRateLimits_Marshal(t *testing.T) {
2444 testJSONMarshal(t, &RateLimits{}, "{}")
2445
2446 u := &RateLimits{
2447 Core: &Rate{
2448 Limit: 1,
2449 Remaining: 1,
2450 Reset: Timestamp{referenceTime},
2451 },
2452 Search: &Rate{
2453 Limit: 1,
2454 Remaining: 1,
2455 Reset: Timestamp{referenceTime},
2456 },
2457 GraphQL: &Rate{
2458 Limit: 1,
2459 Remaining: 1,
2460 Reset: Timestamp{referenceTime},
2461 },
2462 IntegrationManifest: &Rate{
2463 Limit: 1,
2464 Remaining: 1,
2465 Reset: Timestamp{referenceTime},
2466 },
2467 SourceImport: &Rate{
2468 Limit: 1,
2469 Remaining: 1,
2470 Reset: Timestamp{referenceTime},
2471 },
2472 CodeScanningUpload: &Rate{
2473 Limit: 1,
2474 Remaining: 1,
2475 Reset: Timestamp{referenceTime},
2476 },
2477 ActionsRunnerRegistration: &Rate{
2478 Limit: 1,
2479 Remaining: 1,
2480 Reset: Timestamp{referenceTime},
2481 },
2482 SCIM: &Rate{
2483 Limit: 1,
2484 Remaining: 1,
2485 Reset: Timestamp{referenceTime},
2486 },
2487 }
2488
2489 want := `{
2490 "core": {
2491 "limit": 1,
2492 "remaining": 1,
2493 "reset": ` + referenceTimeStr + `
2494 },
2495 "search": {
2496 "limit": 1,
2497 "remaining": 1,
2498 "reset": ` + referenceTimeStr + `
2499 },
2500 "graphql": {
2501 "limit": 1,
2502 "remaining": 1,
2503 "reset": ` + referenceTimeStr + `
2504 },
2505 "integration_manifest": {
2506 "limit": 1,
2507 "remaining": 1,
2508 "reset": ` + referenceTimeStr + `
2509 },
2510 "source_import": {
2511 "limit": 1,
2512 "remaining": 1,
2513 "reset": ` + referenceTimeStr + `
2514 },
2515 "code_scanning_upload": {
2516 "limit": 1,
2517 "remaining": 1,
2518 "reset": ` + referenceTimeStr + `
2519 },
2520 "actions_runner_registration": {
2521 "limit": 1,
2522 "remaining": 1,
2523 "reset": ` + referenceTimeStr + `
2524 },
2525 "scim": {
2526 "limit": 1,
2527 "remaining": 1,
2528 "reset": ` + referenceTimeStr + `
2529 }
2530 }`
2531
2532 testJSONMarshal(t, u, want)
2533 }
2534
2535 func TestParseTokenExpiration(t *testing.T) {
2536 tests := []struct {
2537 header string
2538 want Timestamp
2539 }{
2540 {
2541 header: "",
2542 want: Timestamp{},
2543 },
2544 {
2545 header: "this is a garbage",
2546 want: Timestamp{},
2547 },
2548 {
2549 header: "2021-09-03 02:34:04 UTC",
2550 want: Timestamp{time.Date(2021, time.September, 3, 2, 34, 4, 0, time.UTC)},
2551 },
2552 }
2553
2554 for _, tt := range tests {
2555 res := &http.Response{
2556 Request: &http.Request{},
2557 Header: http.Header{},
2558 }
2559
2560 res.Header.Set(headerTokenExpiration, tt.header)
2561 exp := parseTokenExpiration(res)
2562 if !exp.Equal(tt.want) {
2563 t.Errorf("parseTokenExpiration returned %#v, want %#v", exp, tt.want)
2564 }
2565 }
2566 }
2567
View as plain text