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