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, defaultUserAgent; 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 userAgent := req.Header.Get("User-Agent")
511
512
513 if got, want := userAgent, c.UserAgent; got != want {
514 t.Errorf("NewRequest() User-Agent is %v, want %v", got, want)
515 }
516
517 if !strings.Contains(userAgent, Version) {
518 t.Errorf("NewRequest() User-Agent should contain %v, found %v", Version, userAgent)
519 }
520 }
521
522 func TestNewRequest_invalidJSON(t *testing.T) {
523 c := NewClient(nil)
524
525 type T struct {
526 A map[interface{}]interface{}
527 }
528 _, err := c.NewRequest("GET", ".", &T{})
529
530 if err == nil {
531 t.Error("Expected error to be returned.")
532 }
533 if err, ok := err.(*json.UnsupportedTypeError); !ok {
534 t.Errorf("Expected a JSON error; got %#v.", err)
535 }
536 }
537
538 func TestNewRequest_badURL(t *testing.T) {
539 c := NewClient(nil)
540 _, err := c.NewRequest("GET", ":", nil)
541 testURLParseError(t, err)
542 }
543
544 func TestNewRequest_badMethod(t *testing.T) {
545 c := NewClient(nil)
546 if _, err := c.NewRequest("BOGUS\nMETHOD", ".", nil); err == nil {
547 t.Fatal("NewRequest returned nil; expected error")
548 }
549 }
550
551
552
553 func TestNewRequest_emptyUserAgent(t *testing.T) {
554 c := NewClient(nil)
555 c.UserAgent = ""
556 req, err := c.NewRequest("GET", ".", nil)
557 if err != nil {
558 t.Fatalf("NewRequest returned unexpected error: %v", err)
559 }
560 if _, ok := req.Header["User-Agent"]; ok {
561 t.Fatal("constructed request contains unexpected User-Agent header")
562 }
563 }
564
565
566
567
568
569
570
571 func TestNewRequest_emptyBody(t *testing.T) {
572 c := NewClient(nil)
573 req, err := c.NewRequest("GET", ".", nil)
574 if err != nil {
575 t.Fatalf("NewRequest returned unexpected error: %v", err)
576 }
577 if req.Body != nil {
578 t.Fatalf("constructed request contains a non-nil Body")
579 }
580 }
581
582 func TestNewRequest_errorForNoTrailingSlash(t *testing.T) {
583 tests := []struct {
584 rawurl string
585 wantError bool
586 }{
587 {rawurl: "https://example.com/api/v3", wantError: true},
588 {rawurl: "https://example.com/api/v3/", wantError: false},
589 }
590 c := NewClient(nil)
591 for _, test := range tests {
592 u, err := url.Parse(test.rawurl)
593 if err != nil {
594 t.Fatalf("url.Parse returned unexpected error: %v.", err)
595 }
596 c.BaseURL = u
597 if _, err := c.NewRequest(http.MethodGet, "test", nil); test.wantError && err == nil {
598 t.Fatalf("Expected error to be returned.")
599 } else if !test.wantError && err != nil {
600 t.Fatalf("NewRequest returned unexpected error: %v.", err)
601 }
602 }
603 }
604
605 func TestNewFormRequest(t *testing.T) {
606 c := NewClient(nil)
607
608 inURL, outURL := "/foo", defaultBaseURL+"foo"
609 form := url.Values{}
610 form.Add("login", "l")
611 inBody, outBody := strings.NewReader(form.Encode()), "login=l"
612 req, _ := c.NewFormRequest(inURL, inBody)
613
614
615 if got, want := req.URL.String(), outURL; got != want {
616 t.Errorf("NewFormRequest(%q) URL is %v, want %v", inURL, got, want)
617 }
618
619
620 body, _ := ioutil.ReadAll(req.Body)
621 if got, want := string(body), outBody; got != want {
622 t.Errorf("NewFormRequest(%q) Body is %v, want %v", inBody, got, want)
623 }
624
625
626 if got, want := req.Header.Get("User-Agent"), c.UserAgent; got != want {
627 t.Errorf("NewFormRequest() User-Agent is %v, want %v", got, want)
628 }
629 }
630
631 func TestNewFormRequest_badURL(t *testing.T) {
632 c := NewClient(nil)
633 _, err := c.NewFormRequest(":", nil)
634 testURLParseError(t, err)
635 }
636
637 func TestNewFormRequest_emptyUserAgent(t *testing.T) {
638 c := NewClient(nil)
639 c.UserAgent = ""
640 req, err := c.NewFormRequest(".", nil)
641 if err != nil {
642 t.Fatalf("NewFormRequest returned unexpected error: %v", err)
643 }
644 if _, ok := req.Header["User-Agent"]; ok {
645 t.Fatal("constructed request contains unexpected User-Agent header")
646 }
647 }
648
649 func TestNewFormRequest_emptyBody(t *testing.T) {
650 c := NewClient(nil)
651 req, err := c.NewFormRequest(".", nil)
652 if err != nil {
653 t.Fatalf("NewFormRequest returned unexpected error: %v", err)
654 }
655 if req.Body != nil {
656 t.Fatalf("constructed request contains a non-nil Body")
657 }
658 }
659
660 func TestNewFormRequest_errorForNoTrailingSlash(t *testing.T) {
661 tests := []struct {
662 rawURL string
663 wantError bool
664 }{
665 {rawURL: "https://example.com/api/v3", wantError: true},
666 {rawURL: "https://example.com/api/v3/", wantError: false},
667 }
668 c := NewClient(nil)
669 for _, test := range tests {
670 u, err := url.Parse(test.rawURL)
671 if err != nil {
672 t.Fatalf("url.Parse returned unexpected error: %v.", err)
673 }
674 c.BaseURL = u
675 if _, err := c.NewFormRequest("test", nil); test.wantError && err == nil {
676 t.Fatalf("Expected error to be returned.")
677 } else if !test.wantError && err != nil {
678 t.Fatalf("NewFormRequest returned unexpected error: %v.", err)
679 }
680 }
681 }
682
683 func TestNewUploadRequest_badURL(t *testing.T) {
684 c := NewClient(nil)
685 _, err := c.NewUploadRequest(":", nil, 0, "")
686 testURLParseError(t, err)
687
688 const methodName = "NewUploadRequest"
689 testBadOptions(t, methodName, func() (err error) {
690 _, err = c.NewUploadRequest("\n", nil, -1, "\n")
691 return err
692 })
693 }
694
695 func TestNewUploadRequest_errorForNoTrailingSlash(t *testing.T) {
696 tests := []struct {
697 rawurl string
698 wantError bool
699 }{
700 {rawurl: "https://example.com/api/uploads", wantError: true},
701 {rawurl: "https://example.com/api/uploads/", wantError: false},
702 }
703 c := NewClient(nil)
704 for _, test := range tests {
705 u, err := url.Parse(test.rawurl)
706 if err != nil {
707 t.Fatalf("url.Parse returned unexpected error: %v.", err)
708 }
709 c.UploadURL = u
710 if _, err = c.NewUploadRequest("test", nil, 0, ""); test.wantError && err == nil {
711 t.Fatalf("Expected error to be returned.")
712 } else if !test.wantError && err != nil {
713 t.Fatalf("NewUploadRequest returned unexpected error: %v.", err)
714 }
715 }
716 }
717
718 func TestResponse_populatePageValues(t *testing.T) {
719 r := http.Response{
720 Header: http.Header{
721 "Link": {`<https://api.github.com/?page=1>; rel="first",` +
722 ` <https://api.github.com/?page=2>; rel="prev",` +
723 ` <https://api.github.com/?page=4>; rel="next",` +
724 ` <https://api.github.com/?page=5>; rel="last"`,
725 },
726 },
727 }
728
729 response := newResponse(&r)
730 if got, want := response.FirstPage, 1; got != want {
731 t.Errorf("response.FirstPage: %v, want %v", got, want)
732 }
733 if got, want := response.PrevPage, 2; want != got {
734 t.Errorf("response.PrevPage: %v, want %v", got, want)
735 }
736 if got, want := response.NextPage, 4; want != got {
737 t.Errorf("response.NextPage: %v, want %v", got, want)
738 }
739 if got, want := response.LastPage, 5; want != got {
740 t.Errorf("response.LastPage: %v, want %v", got, want)
741 }
742 if got, want := response.NextPageToken, ""; want != got {
743 t.Errorf("response.NextPageToken: %v, want %v", got, want)
744 }
745 }
746
747 func TestResponse_populateSinceValues(t *testing.T) {
748 r := http.Response{
749 Header: http.Header{
750 "Link": {`<https://api.github.com/?since=1>; rel="first",` +
751 ` <https://api.github.com/?since=2>; rel="prev",` +
752 ` <https://api.github.com/?since=4>; rel="next",` +
753 ` <https://api.github.com/?since=5>; rel="last"`,
754 },
755 },
756 }
757
758 response := newResponse(&r)
759 if got, want := response.FirstPage, 1; got != want {
760 t.Errorf("response.FirstPage: %v, want %v", got, want)
761 }
762 if got, want := response.PrevPage, 2; want != got {
763 t.Errorf("response.PrevPage: %v, want %v", got, want)
764 }
765 if got, want := response.NextPage, 4; want != got {
766 t.Errorf("response.NextPage: %v, want %v", got, want)
767 }
768 if got, want := response.LastPage, 5; want != got {
769 t.Errorf("response.LastPage: %v, want %v", got, want)
770 }
771 if got, want := response.NextPageToken, ""; want != got {
772 t.Errorf("response.NextPageToken: %v, want %v", got, want)
773 }
774 }
775
776 func TestResponse_SinceWithPage(t *testing.T) {
777 r := http.Response{
778 Header: http.Header{
779 "Link": {`<https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=1>; rel="first",` +
780 ` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=2>; rel="prev",` +
781 ` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=4>; rel="next",` +
782 ` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=5>; rel="last"`,
783 },
784 },
785 }
786
787 response := newResponse(&r)
788 if got, want := response.FirstPage, 1; got != want {
789 t.Errorf("response.FirstPage: %v, want %v", got, want)
790 }
791 if got, want := response.PrevPage, 2; want != got {
792 t.Errorf("response.PrevPage: %v, want %v", got, want)
793 }
794 if got, want := response.NextPage, 4; want != got {
795 t.Errorf("response.NextPage: %v, want %v", got, want)
796 }
797 if got, want := response.LastPage, 5; want != got {
798 t.Errorf("response.LastPage: %v, want %v", got, want)
799 }
800 if got, want := response.NextPageToken, ""; want != got {
801 t.Errorf("response.NextPageToken: %v, want %v", got, want)
802 }
803 }
804
805 func TestResponse_cursorPagination(t *testing.T) {
806 r := http.Response{
807 Header: http.Header{
808 "Status": {"200 OK"},
809 "Link": {`<https://api.github.com/resource?per_page=2&page=url-encoded-next-page-token>; rel="next"`},
810 },
811 }
812
813 response := newResponse(&r)
814 if got, want := response.FirstPage, 0; got != want {
815 t.Errorf("response.FirstPage: %v, want %v", got, want)
816 }
817 if got, want := response.PrevPage, 0; want != got {
818 t.Errorf("response.PrevPage: %v, want %v", got, want)
819 }
820 if got, want := response.NextPage, 0; want != got {
821 t.Errorf("response.NextPage: %v, want %v", got, want)
822 }
823 if got, want := response.LastPage, 0; want != got {
824 t.Errorf("response.LastPage: %v, want %v", got, want)
825 }
826 if got, want := response.NextPageToken, "url-encoded-next-page-token"; want != got {
827 t.Errorf("response.NextPageToken: %v, want %v", got, want)
828 }
829
830
831 r = http.Response{
832 Header: http.Header{
833 "Link": {
834 `<https://api.github.com/?cursor=v1_12345678>; rel="next"`,
835 },
836 },
837 }
838
839 response = newResponse(&r)
840 if got, want := response.Cursor, "v1_12345678"; got != want {
841 t.Errorf("response.Cursor: %v, want %v", got, want)
842 }
843 }
844
845 func TestResponse_beforeAfterPagination(t *testing.T) {
846 r := http.Response{
847 Header: http.Header{
848 "Link": {`<https://api.github.com/?after=a1b2c3&before=>; rel="next",` +
849 ` <https://api.github.com/?after=&before=>; rel="first",` +
850 ` <https://api.github.com/?after=&before=d4e5f6>; rel="prev",`,
851 },
852 },
853 }
854
855 response := newResponse(&r)
856 if got, want := response.Before, "d4e5f6"; got != want {
857 t.Errorf("response.Before: %v, want %v", got, want)
858 }
859 if got, want := response.After, "a1b2c3"; got != want {
860 t.Errorf("response.After: %v, want %v", got, want)
861 }
862 if got, want := response.FirstPage, 0; got != want {
863 t.Errorf("response.FirstPage: %v, want %v", got, want)
864 }
865 if got, want := response.PrevPage, 0; want != got {
866 t.Errorf("response.PrevPage: %v, want %v", got, want)
867 }
868 if got, want := response.NextPage, 0; want != got {
869 t.Errorf("response.NextPage: %v, want %v", got, want)
870 }
871 if got, want := response.LastPage, 0; want != got {
872 t.Errorf("response.LastPage: %v, want %v", got, want)
873 }
874 if got, want := response.NextPageToken, ""; want != got {
875 t.Errorf("response.NextPageToken: %v, want %v", got, want)
876 }
877 }
878
879 func TestResponse_populatePageValues_invalid(t *testing.T) {
880 r := http.Response{
881 Header: http.Header{
882 "Link": {`<https://api.github.com/?page=1>,` +
883 `<https://api.github.com/?page=abc>; rel="first",` +
884 `https://api.github.com/?page=2; rel="prev",` +
885 `<https://api.github.com/>; rel="next",` +
886 `<https://api.github.com/?page=>; rel="last"`,
887 },
888 },
889 }
890
891 response := newResponse(&r)
892 if got, want := response.FirstPage, 0; got != want {
893 t.Errorf("response.FirstPage: %v, want %v", got, want)
894 }
895 if got, want := response.PrevPage, 0; got != want {
896 t.Errorf("response.PrevPage: %v, want %v", got, want)
897 }
898 if got, want := response.NextPage, 0; got != want {
899 t.Errorf("response.NextPage: %v, want %v", got, want)
900 }
901 if got, want := response.LastPage, 0; got != want {
902 t.Errorf("response.LastPage: %v, want %v", got, want)
903 }
904
905
906 r = http.Response{
907 Header: http.Header{
908 "Link": {`<https://api.github.com/%?page=2>; rel="first"`},
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 }
917
918 func TestResponse_populateSinceValues_invalid(t *testing.T) {
919 r := http.Response{
920 Header: http.Header{
921 "Link": {`<https://api.github.com/?since=1>,` +
922 `<https://api.github.com/?since=abc>; rel="first",` +
923 `https://api.github.com/?since=2; rel="prev",` +
924 `<https://api.github.com/>; rel="next",` +
925 `<https://api.github.com/?since=>; rel="last"`,
926 },
927 },
928 }
929
930 response := newResponse(&r)
931 if got, want := response.FirstPage, 0; got != want {
932 t.Errorf("response.FirstPage: %v, want %v", got, want)
933 }
934 if got, want := response.PrevPage, 0; got != want {
935 t.Errorf("response.PrevPage: %v, want %v", got, want)
936 }
937 if got, want := response.NextPage, 0; got != want {
938 t.Errorf("response.NextPage: %v, want %v", got, want)
939 }
940 if got, want := response.LastPage, 0; got != want {
941 t.Errorf("response.LastPage: %v, want %v", got, want)
942 }
943
944
945 r = http.Response{
946 Header: http.Header{
947 "Link": {`<https://api.github.com/%?since=2>; rel="first"`},
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 }
956
957 func TestDo(t *testing.T) {
958 client, mux, _, teardown := setup()
959 defer teardown()
960
961 type foo struct {
962 A string
963 }
964
965 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
966 testMethod(t, r, "GET")
967 fmt.Fprint(w, `{"A":"a"}`)
968 })
969
970 req, _ := client.NewRequest("GET", ".", nil)
971 body := new(foo)
972 ctx := context.Background()
973 client.Do(ctx, req, body)
974
975 want := &foo{"a"}
976 if !cmp.Equal(body, want) {
977 t.Errorf("Response body = %v, want %v", body, want)
978 }
979 }
980
981 func TestDo_nilContext(t *testing.T) {
982 client, _, _, teardown := setup()
983 defer teardown()
984
985 req, _ := client.NewRequest("GET", ".", nil)
986 _, err := client.Do(nil, req, nil)
987
988 if !errors.Is(err, errNonNilContext) {
989 t.Errorf("Expected context must be non-nil error")
990 }
991 }
992
993 func TestDo_httpError(t *testing.T) {
994 client, mux, _, teardown := setup()
995 defer teardown()
996
997 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
998 http.Error(w, "Bad Request", 400)
999 })
1000
1001 req, _ := client.NewRequest("GET", ".", nil)
1002 ctx := context.Background()
1003 resp, err := client.Do(ctx, req, nil)
1004
1005 if err == nil {
1006 t.Fatal("Expected HTTP 400 error, got no error.")
1007 }
1008 if resp.StatusCode != 400 {
1009 t.Errorf("Expected HTTP 400 error, got %d status code.", resp.StatusCode)
1010 }
1011 }
1012
1013
1014
1015
1016 func TestDo_redirectLoop(t *testing.T) {
1017 client, mux, _, teardown := setup()
1018 defer teardown()
1019
1020 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1021 http.Redirect(w, r, baseURLPath, http.StatusFound)
1022 })
1023
1024 req, _ := client.NewRequest("GET", ".", nil)
1025 ctx := context.Background()
1026 _, err := client.Do(ctx, req, nil)
1027
1028 if err == nil {
1029 t.Error("Expected error to be returned.")
1030 }
1031 if err, ok := err.(*url.Error); !ok {
1032 t.Errorf("Expected a URL error; got %#v.", err)
1033 }
1034 }
1035
1036
1037
1038 func TestDo_sanitizeURL(t *testing.T) {
1039 tp := &UnauthenticatedRateLimitedTransport{
1040 ClientID: "id",
1041 ClientSecret: "secret",
1042 }
1043 unauthedClient := NewClient(tp.Client())
1044 unauthedClient.BaseURL = &url.URL{Scheme: "http", Host: "127.0.0.1:0", Path: "/"}
1045 req, err := unauthedClient.NewRequest("GET", ".", nil)
1046 if err != nil {
1047 t.Fatalf("NewRequest returned unexpected error: %v", err)
1048 }
1049 ctx := context.Background()
1050 _, err = unauthedClient.Do(ctx, req, nil)
1051 if err == nil {
1052 t.Fatal("Expected error to be returned.")
1053 }
1054 if strings.Contains(err.Error(), "client_secret=secret") {
1055 t.Errorf("Do error contains secret, should be redacted:\n%q", err)
1056 }
1057 }
1058
1059 func TestDo_rateLimit(t *testing.T) {
1060 client, mux, _, teardown := setup()
1061 defer teardown()
1062
1063 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1064 w.Header().Set(headerRateLimit, "60")
1065 w.Header().Set(headerRateRemaining, "59")
1066 w.Header().Set(headerRateReset, "1372700873")
1067 })
1068
1069 req, _ := client.NewRequest("GET", ".", nil)
1070 ctx := context.Background()
1071 resp, err := client.Do(ctx, req, nil)
1072 if err != nil {
1073 t.Errorf("Do returned unexpected error: %v", err)
1074 }
1075 if got, want := resp.Rate.Limit, 60; got != want {
1076 t.Errorf("Client rate limit = %v, want %v", got, want)
1077 }
1078 if got, want := resp.Rate.Remaining, 59; got != want {
1079 t.Errorf("Client rate remaining = %v, want %v", got, want)
1080 }
1081 reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
1082 if resp.Rate.Reset.UTC() != reset {
1083 t.Errorf("Client rate reset = %v, want %v", resp.Rate.Reset, reset)
1084 }
1085 }
1086
1087
1088 func TestDo_rateLimit_errorResponse(t *testing.T) {
1089 client, mux, _, teardown := setup()
1090 defer teardown()
1091
1092 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1093 w.Header().Set(headerRateLimit, "60")
1094 w.Header().Set(headerRateRemaining, "59")
1095 w.Header().Set(headerRateReset, "1372700873")
1096 http.Error(w, "Bad Request", 400)
1097 })
1098
1099 req, _ := client.NewRequest("GET", ".", nil)
1100 ctx := context.Background()
1101 resp, err := client.Do(ctx, req, nil)
1102 if err == nil {
1103 t.Error("Expected error to be returned.")
1104 }
1105 if _, ok := err.(*RateLimitError); ok {
1106 t.Errorf("Did not expect a *RateLimitError error; got %#v.", err)
1107 }
1108 if got, want := resp.Rate.Limit, 60; got != want {
1109 t.Errorf("Client rate limit = %v, want %v", got, want)
1110 }
1111 if got, want := resp.Rate.Remaining, 59; got != want {
1112 t.Errorf("Client rate remaining = %v, want %v", got, want)
1113 }
1114 reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
1115 if resp.Rate.Reset.UTC() != reset {
1116 t.Errorf("Client rate reset = %v, want %v", resp.Rate.Reset, reset)
1117 }
1118 }
1119
1120
1121 func TestDo_rateLimit_rateLimitError(t *testing.T) {
1122 client, mux, _, teardown := setup()
1123 defer teardown()
1124
1125 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1126 w.Header().Set(headerRateLimit, "60")
1127 w.Header().Set(headerRateRemaining, "0")
1128 w.Header().Set(headerRateReset, "1372700873")
1129 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1130 w.WriteHeader(http.StatusForbidden)
1131 fmt.Fprintln(w, `{
1132 "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.)",
1133 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1134 }`)
1135 })
1136
1137 req, _ := client.NewRequest("GET", ".", nil)
1138 ctx := context.Background()
1139 _, err := client.Do(ctx, req, nil)
1140
1141 if err == nil {
1142 t.Error("Expected error to be returned.")
1143 }
1144 rateLimitErr, ok := err.(*RateLimitError)
1145 if !ok {
1146 t.Fatalf("Expected a *RateLimitError error; got %#v.", err)
1147 }
1148 if got, want := rateLimitErr.Rate.Limit, 60; got != want {
1149 t.Errorf("rateLimitErr rate limit = %v, want %v", got, want)
1150 }
1151 if got, want := rateLimitErr.Rate.Remaining, 0; got != want {
1152 t.Errorf("rateLimitErr rate remaining = %v, want %v", got, want)
1153 }
1154 reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
1155 if rateLimitErr.Rate.Reset.UTC() != reset {
1156 t.Errorf("rateLimitErr rate reset = %v, want %v", rateLimitErr.Rate.Reset.UTC(), reset)
1157 }
1158 }
1159
1160
1161 func TestDo_rateLimit_noNetworkCall(t *testing.T) {
1162 client, mux, _, teardown := setup()
1163 defer teardown()
1164
1165 reset := time.Now().UTC().Add(time.Minute).Round(time.Second)
1166
1167 mux.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) {
1168 w.Header().Set(headerRateLimit, "60")
1169 w.Header().Set(headerRateRemaining, "0")
1170 w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix()))
1171 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1172 w.WriteHeader(http.StatusForbidden)
1173 fmt.Fprintln(w, `{
1174 "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.)",
1175 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1176 }`)
1177 })
1178
1179 madeNetworkCall := false
1180 mux.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) {
1181 madeNetworkCall = true
1182 })
1183
1184
1185 req, _ := client.NewRequest("GET", "first", nil)
1186 ctx := context.Background()
1187 client.Do(ctx, req, nil)
1188
1189
1190 req, _ = client.NewRequest("GET", "second", nil)
1191 _, err := client.Do(ctx, req, nil)
1192
1193 if madeNetworkCall {
1194 t.Fatal("Network call was made, even though rate limit is known to still be exceeded.")
1195 }
1196
1197 if err == nil {
1198 t.Error("Expected error to be returned.")
1199 }
1200 rateLimitErr, ok := err.(*RateLimitError)
1201 if !ok {
1202 t.Fatalf("Expected a *RateLimitError error; got %#v.", err)
1203 }
1204 if got, want := rateLimitErr.Rate.Limit, 60; got != want {
1205 t.Errorf("rateLimitErr rate limit = %v, want %v", got, want)
1206 }
1207 if got, want := rateLimitErr.Rate.Remaining, 0; got != want {
1208 t.Errorf("rateLimitErr rate remaining = %v, want %v", got, want)
1209 }
1210 if rateLimitErr.Rate.Reset.UTC() != reset {
1211 t.Errorf("rateLimitErr rate reset = %v, want %v", rateLimitErr.Rate.Reset.UTC(), reset)
1212 }
1213 }
1214
1215
1216 func TestDo_rateLimit_ignoredFromCache(t *testing.T) {
1217 client, mux, _, teardown := setup()
1218 defer teardown()
1219
1220 reset := time.Now().UTC().Add(time.Minute).Round(time.Second)
1221
1222
1223 mux.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) {
1224 w.Header().Set("X-From-Cache", "1")
1225 w.Header().Set(headerRateLimit, "60")
1226 w.Header().Set(headerRateRemaining, "0")
1227 w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix()))
1228 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1229 w.WriteHeader(http.StatusForbidden)
1230 fmt.Fprintln(w, `{
1231 "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.)",
1232 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1233 }`)
1234 })
1235
1236 madeNetworkCall := false
1237 mux.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) {
1238 madeNetworkCall = true
1239 })
1240
1241
1242 req, _ := client.NewRequest("GET", "first", nil)
1243 ctx := context.Background()
1244 client.Do(ctx, req, nil)
1245
1246
1247 req, _ = client.NewRequest("GET", "second", nil)
1248 _, err := client.Do(ctx, req, nil)
1249
1250 if err != nil {
1251 t.Fatalf("Second request failed, even though the rate limits from the cache should've been ignored: %v", err)
1252 }
1253 if !madeNetworkCall {
1254 t.Fatal("Network call was not made, even though the rate limits from the cache should've been ignored")
1255 }
1256 }
1257
1258
1259
1260 func TestDo_rateLimit_abuseRateLimitError(t *testing.T) {
1261 client, mux, _, teardown := setup()
1262 defer teardown()
1263
1264 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1265 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1266 w.WriteHeader(http.StatusForbidden)
1267
1268
1269 fmt.Fprintln(w, `{
1270 "message": "You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.",
1271 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1272 }`)
1273 })
1274
1275 req, _ := client.NewRequest("GET", ".", nil)
1276 ctx := context.Background()
1277 _, err := client.Do(ctx, req, nil)
1278
1279 if err == nil {
1280 t.Error("Expected error to be returned.")
1281 }
1282 abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
1283 if !ok {
1284 t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
1285 }
1286 if got, want := abuseRateLimitErr.RetryAfter, (*time.Duration)(nil); got != want {
1287 t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
1288 }
1289 }
1290
1291
1292
1293 func TestDo_rateLimit_abuseRateLimitErrorEnterprise(t *testing.T) {
1294 client, mux, _, teardown := setup()
1295 defer teardown()
1296
1297 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1298 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1299 w.WriteHeader(http.StatusForbidden)
1300
1301
1302
1303
1304 fmt.Fprintln(w, `{
1305 "message": "You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.",
1306 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1307 }`)
1308 })
1309
1310 req, _ := client.NewRequest("GET", ".", nil)
1311 ctx := context.Background()
1312 _, err := client.Do(ctx, req, nil)
1313
1314 if err == nil {
1315 t.Error("Expected error to be returned.")
1316 }
1317 abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
1318 if !ok {
1319 t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
1320 }
1321 if got, want := abuseRateLimitErr.RetryAfter, (*time.Duration)(nil); got != want {
1322 t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
1323 }
1324 }
1325
1326
1327 func TestDo_rateLimit_abuseRateLimitError_retryAfter(t *testing.T) {
1328 client, mux, _, teardown := setup()
1329 defer teardown()
1330
1331 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1332 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1333 w.Header().Set("Retry-After", "123")
1334 w.WriteHeader(http.StatusForbidden)
1335 fmt.Fprintln(w, `{
1336 "message": "You have triggered an abuse detection mechanism ...",
1337 "documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1338 }`)
1339 })
1340
1341 req, _ := client.NewRequest("GET", ".", nil)
1342 ctx := context.Background()
1343 _, err := client.Do(ctx, req, nil)
1344
1345 if err == nil {
1346 t.Error("Expected error to be returned.")
1347 }
1348 abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
1349 if !ok {
1350 t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
1351 }
1352 if abuseRateLimitErr.RetryAfter == nil {
1353 t.Fatalf("abuseRateLimitErr RetryAfter is nil, expected not-nil")
1354 }
1355 if got, want := *abuseRateLimitErr.RetryAfter, 123*time.Second; got != want {
1356 t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
1357 }
1358 }
1359
1360 func TestDo_noContent(t *testing.T) {
1361 client, mux, _, teardown := setup()
1362 defer teardown()
1363
1364 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1365 w.WriteHeader(http.StatusNoContent)
1366 })
1367
1368 var body json.RawMessage
1369
1370 req, _ := client.NewRequest("GET", ".", nil)
1371 ctx := context.Background()
1372 _, err := client.Do(ctx, req, &body)
1373 if err != nil {
1374 t.Fatalf("Do returned unexpected error: %v", err)
1375 }
1376 }
1377
1378 func TestSanitizeURL(t *testing.T) {
1379 tests := []struct {
1380 in, want string
1381 }{
1382 {"/?a=b", "/?a=b"},
1383 {"/?a=b&client_secret=secret", "/?a=b&client_secret=REDACTED"},
1384 {"/?a=b&client_id=id&client_secret=secret", "/?a=b&client_id=id&client_secret=REDACTED"},
1385 }
1386
1387 for _, tt := range tests {
1388 inURL, _ := url.Parse(tt.in)
1389 want, _ := url.Parse(tt.want)
1390
1391 if got := sanitizeURL(inURL); !cmp.Equal(got, want) {
1392 t.Errorf("sanitizeURL(%v) returned %v, want %v", tt.in, got, want)
1393 }
1394 }
1395 }
1396
1397 func TestCheckResponse(t *testing.T) {
1398 res := &http.Response{
1399 Request: &http.Request{},
1400 StatusCode: http.StatusBadRequest,
1401 Body: ioutil.NopCloser(strings.NewReader(`{"message":"m",
1402 "errors": [{"resource": "r", "field": "f", "code": "c"}],
1403 "block": {"reason": "dmca", "created_at": "2016-03-17T15:39:46Z"}}`)),
1404 }
1405 err := CheckResponse(res).(*ErrorResponse)
1406
1407 if err == nil {
1408 t.Errorf("Expected error response.")
1409 }
1410
1411 want := &ErrorResponse{
1412 Response: res,
1413 Message: "m",
1414 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1415 Block: &ErrorBlock{
1416 Reason: "dmca",
1417 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1418 },
1419 }
1420 if !errors.Is(err, want) {
1421 t.Errorf("Error = %#v, want %#v", err, want)
1422 }
1423 }
1424
1425 func TestCheckResponse_RateLimit(t *testing.T) {
1426 res := &http.Response{
1427 Request: &http.Request{},
1428 StatusCode: http.StatusForbidden,
1429 Header: http.Header{},
1430 Body: ioutil.NopCloser(strings.NewReader(`{"message":"m",
1431 "documentation_url": "url"}`)),
1432 }
1433 res.Header.Set(headerRateLimit, "60")
1434 res.Header.Set(headerRateRemaining, "0")
1435 res.Header.Set(headerRateReset, "243424")
1436
1437 err := CheckResponse(res).(*RateLimitError)
1438
1439 if err == nil {
1440 t.Errorf("Expected error response.")
1441 }
1442
1443 want := &RateLimitError{
1444 Rate: parseRate(res),
1445 Response: res,
1446 Message: "m",
1447 }
1448 if !errors.Is(err, want) {
1449 t.Errorf("Error = %#v, want %#v", err, want)
1450 }
1451 }
1452
1453 func TestCheckResponse_AbuseRateLimit(t *testing.T) {
1454 res := &http.Response{
1455 Request: &http.Request{},
1456 StatusCode: http.StatusForbidden,
1457 Body: ioutil.NopCloser(strings.NewReader(`{"message":"m",
1458 "documentation_url": "docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"}`)),
1459 }
1460 err := CheckResponse(res).(*AbuseRateLimitError)
1461
1462 if err == nil {
1463 t.Errorf("Expected error response.")
1464 }
1465
1466 want := &AbuseRateLimitError{
1467 Response: res,
1468 Message: "m",
1469 }
1470 if !errors.Is(err, want) {
1471 t.Errorf("Error = %#v, want %#v", err, want)
1472 }
1473 }
1474
1475 func TestCompareHttpResponse(t *testing.T) {
1476 testcases := map[string]struct {
1477 h1 *http.Response
1478 h2 *http.Response
1479 expected bool
1480 }{
1481 "both are nil": {
1482 expected: true,
1483 },
1484 "both are non nil - same StatusCode": {
1485 expected: true,
1486 h1: &http.Response{StatusCode: 200},
1487 h2: &http.Response{StatusCode: 200},
1488 },
1489 "both are non nil - different StatusCode": {
1490 expected: false,
1491 h1: &http.Response{StatusCode: 200},
1492 h2: &http.Response{StatusCode: 404},
1493 },
1494 "one is nil, other is not": {
1495 expected: false,
1496 h2: &http.Response{},
1497 },
1498 }
1499
1500 for name, tc := range testcases {
1501 t.Run(name, func(t *testing.T) {
1502 v := compareHTTPResponse(tc.h1, tc.h2)
1503 if tc.expected != v {
1504 t.Errorf("Expected %t, got %t for (%#v, %#v)", tc.expected, v, tc.h1, tc.h2)
1505 }
1506 })
1507 }
1508 }
1509
1510 func TestErrorResponse_Is(t *testing.T) {
1511 err := &ErrorResponse{
1512 Response: &http.Response{},
1513 Message: "m",
1514 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1515 Block: &ErrorBlock{
1516 Reason: "r",
1517 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1518 },
1519 DocumentationURL: "https://github.com",
1520 }
1521 testcases := map[string]struct {
1522 wantSame bool
1523 otherError error
1524 }{
1525 "errors are same": {
1526 wantSame: true,
1527 otherError: &ErrorResponse{
1528 Response: &http.Response{},
1529 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1530 Message: "m",
1531 Block: &ErrorBlock{
1532 Reason: "r",
1533 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1534 },
1535 DocumentationURL: "https://github.com",
1536 },
1537 },
1538 "errors have different values - Message": {
1539 wantSame: false,
1540 otherError: &ErrorResponse{
1541 Response: &http.Response{},
1542 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1543 Message: "m1",
1544 Block: &ErrorBlock{
1545 Reason: "r",
1546 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1547 },
1548 DocumentationURL: "https://github.com",
1549 },
1550 },
1551 "errors have different values - DocumentationURL": {
1552 wantSame: false,
1553 otherError: &ErrorResponse{
1554 Response: &http.Response{},
1555 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1556 Message: "m",
1557 Block: &ErrorBlock{
1558 Reason: "r",
1559 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1560 },
1561 DocumentationURL: "https://google.com",
1562 },
1563 },
1564 "errors have different values - Response is nil": {
1565 wantSame: false,
1566 otherError: &ErrorResponse{
1567 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1568 Message: "m",
1569 Block: &ErrorBlock{
1570 Reason: "r",
1571 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1572 },
1573 DocumentationURL: "https://github.com",
1574 },
1575 },
1576 "errors have different values - Errors": {
1577 wantSame: false,
1578 otherError: &ErrorResponse{
1579 Response: &http.Response{},
1580 Errors: []Error{{Resource: "r1", Field: "f1", Code: "c1"}},
1581 Message: "m",
1582 Block: &ErrorBlock{
1583 Reason: "r",
1584 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1585 },
1586 DocumentationURL: "https://github.com",
1587 },
1588 },
1589 "errors have different values - Errors have different length": {
1590 wantSame: false,
1591 otherError: &ErrorResponse{
1592 Response: &http.Response{},
1593 Errors: []Error{},
1594 Message: "m",
1595 Block: &ErrorBlock{
1596 Reason: "r",
1597 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1598 },
1599 DocumentationURL: "https://github.com",
1600 },
1601 },
1602 "errors have different values - Block - one is nil, other is not": {
1603 wantSame: false,
1604 otherError: &ErrorResponse{
1605 Response: &http.Response{},
1606 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1607 Message: "m",
1608 DocumentationURL: "https://github.com",
1609 },
1610 },
1611 "errors have different values - Block - different Reason": {
1612 wantSame: false,
1613 otherError: &ErrorResponse{
1614 Response: &http.Response{},
1615 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1616 Message: "m",
1617 Block: &ErrorBlock{
1618 Reason: "r1",
1619 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
1620 },
1621 DocumentationURL: "https://github.com",
1622 },
1623 },
1624 "errors have different values - Block - different CreatedAt #1": {
1625 wantSame: false,
1626 otherError: &ErrorResponse{
1627 Response: &http.Response{},
1628 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1629 Message: "m",
1630 Block: &ErrorBlock{
1631 Reason: "r",
1632 CreatedAt: nil,
1633 },
1634 DocumentationURL: "https://github.com",
1635 },
1636 },
1637 "errors have different values - Block - different CreatedAt #2": {
1638 wantSame: false,
1639 otherError: &ErrorResponse{
1640 Response: &http.Response{},
1641 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}},
1642 Message: "m",
1643 Block: &ErrorBlock{
1644 Reason: "r",
1645 CreatedAt: &Timestamp{time.Date(2017, time.March, 17, 15, 39, 46, 0, time.UTC)},
1646 },
1647 DocumentationURL: "https://github.com",
1648 },
1649 },
1650 "errors have different types": {
1651 wantSame: false,
1652 otherError: errors.New("Github"),
1653 },
1654 }
1655
1656 for name, tc := range testcases {
1657 t.Run(name, func(t *testing.T) {
1658 if tc.wantSame != err.Is(tc.otherError) {
1659 t.Errorf("Error = %#v, want %#v", err, tc.otherError)
1660 }
1661 })
1662 }
1663 }
1664
1665 func TestRateLimitError_Is(t *testing.T) {
1666 err := &RateLimitError{
1667 Response: &http.Response{},
1668 Message: "Github",
1669 }
1670 testcases := map[string]struct {
1671 wantSame bool
1672 err *RateLimitError
1673 otherError error
1674 }{
1675 "errors are same": {
1676 wantSame: true,
1677 err: err,
1678 otherError: &RateLimitError{
1679 Response: &http.Response{},
1680 Message: "Github",
1681 },
1682 },
1683 "errors are same - Response is nil": {
1684 wantSame: true,
1685 err: &RateLimitError{
1686 Message: "Github",
1687 },
1688 otherError: &RateLimitError{
1689 Message: "Github",
1690 },
1691 },
1692 "errors have different values - Rate": {
1693 wantSame: false,
1694 err: err,
1695 otherError: &RateLimitError{
1696 Rate: Rate{Limit: 10},
1697 Response: &http.Response{},
1698 Message: "Gitlab",
1699 },
1700 },
1701 "errors have different values - Response is nil": {
1702 wantSame: false,
1703 err: err,
1704 otherError: &RateLimitError{
1705 Message: "Github",
1706 },
1707 },
1708 "errors have different values - StatusCode": {
1709 wantSame: false,
1710 err: err,
1711 otherError: &RateLimitError{
1712 Response: &http.Response{StatusCode: 200},
1713 Message: "Github",
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 TestAbuseRateLimitError_Is(t *testing.T) {
1733 t1 := 1 * time.Second
1734 t2 := 2 * time.Second
1735 err := &AbuseRateLimitError{
1736 Response: &http.Response{},
1737 Message: "Github",
1738 RetryAfter: &t1,
1739 }
1740 testcases := map[string]struct {
1741 wantSame bool
1742 err *AbuseRateLimitError
1743 otherError error
1744 }{
1745 "errors are same": {
1746 wantSame: true,
1747 err: err,
1748 otherError: &AbuseRateLimitError{
1749 Response: &http.Response{},
1750 Message: "Github",
1751 RetryAfter: &t1,
1752 },
1753 },
1754 "errors are same - Response is nil": {
1755 wantSame: true,
1756 err: &AbuseRateLimitError{
1757 Message: "Github",
1758 RetryAfter: &t1,
1759 },
1760 otherError: &AbuseRateLimitError{
1761 Message: "Github",
1762 RetryAfter: &t1,
1763 },
1764 },
1765 "errors have different values - Message": {
1766 wantSame: false,
1767 err: err,
1768 otherError: &AbuseRateLimitError{
1769 Response: &http.Response{},
1770 Message: "Gitlab",
1771 RetryAfter: nil,
1772 },
1773 },
1774 "errors have different values - RetryAfter": {
1775 wantSame: false,
1776 err: err,
1777 otherError: &AbuseRateLimitError{
1778 Response: &http.Response{},
1779 Message: "Github",
1780 RetryAfter: &t2,
1781 },
1782 },
1783 "errors have different values - Response is nil": {
1784 wantSame: false,
1785 err: err,
1786 otherError: &AbuseRateLimitError{
1787 Message: "Github",
1788 RetryAfter: &t1,
1789 },
1790 },
1791 "errors have different values - StatusCode": {
1792 wantSame: false,
1793 err: err,
1794 otherError: &AbuseRateLimitError{
1795 Response: &http.Response{StatusCode: 200},
1796 Message: "Github",
1797 RetryAfter: &t1,
1798 },
1799 },
1800 "errors have different types": {
1801 wantSame: false,
1802 err: err,
1803 otherError: errors.New("Github"),
1804 },
1805 }
1806
1807 for name, tc := range testcases {
1808 t.Run(name, func(t *testing.T) {
1809 if tc.wantSame != tc.err.Is(tc.otherError) {
1810 t.Errorf("Error = %#v, want %#v", tc.err, tc.otherError)
1811 }
1812 })
1813 }
1814 }
1815
1816 func TestAcceptedError_Is(t *testing.T) {
1817 err := &AcceptedError{Raw: []byte("Github")}
1818 testcases := map[string]struct {
1819 wantSame bool
1820 otherError error
1821 }{
1822 "errors are same": {
1823 wantSame: true,
1824 otherError: &AcceptedError{Raw: []byte("Github")},
1825 },
1826 "errors have different values": {
1827 wantSame: false,
1828 otherError: &AcceptedError{Raw: []byte("Gitlab")},
1829 },
1830 "errors have different types": {
1831 wantSame: false,
1832 otherError: errors.New("Github"),
1833 },
1834 }
1835
1836 for name, tc := range testcases {
1837 t.Run(name, func(t *testing.T) {
1838 if tc.wantSame != err.Is(tc.otherError) {
1839 t.Errorf("Error = %#v, want %#v", err, tc.otherError)
1840 }
1841 })
1842 }
1843 }
1844
1845
1846 func TestCheckResponse_noBody(t *testing.T) {
1847 res := &http.Response{
1848 Request: &http.Request{},
1849 StatusCode: http.StatusBadRequest,
1850 Body: ioutil.NopCloser(strings.NewReader("")),
1851 }
1852 err := CheckResponse(res).(*ErrorResponse)
1853
1854 if err == nil {
1855 t.Errorf("Expected error response.")
1856 }
1857
1858 want := &ErrorResponse{
1859 Response: res,
1860 }
1861 if !errors.Is(err, want) {
1862 t.Errorf("Error = %#v, want %#v", err, want)
1863 }
1864 }
1865
1866 func TestCheckResponse_unexpectedErrorStructure(t *testing.T) {
1867 httpBody := `{"message":"m", "errors": ["error 1"]}`
1868 res := &http.Response{
1869 Request: &http.Request{},
1870 StatusCode: http.StatusBadRequest,
1871 Body: ioutil.NopCloser(strings.NewReader(httpBody)),
1872 }
1873 err := CheckResponse(res).(*ErrorResponse)
1874
1875 if err == nil {
1876 t.Errorf("Expected error response.")
1877 }
1878
1879 want := &ErrorResponse{
1880 Response: res,
1881 Message: "m",
1882 Errors: []Error{{Message: "error 1"}},
1883 }
1884 if !errors.Is(err, want) {
1885 t.Errorf("Error = %#v, want %#v", err, want)
1886 }
1887 data, err2 := ioutil.ReadAll(err.Response.Body)
1888 if err2 != nil {
1889 t.Fatalf("failed to read response body: %v", err)
1890 }
1891 if got := string(data); got != httpBody {
1892 t.Errorf("ErrorResponse.Response.Body = %q, want %q", got, httpBody)
1893 }
1894 }
1895
1896 func TestParseBooleanResponse_true(t *testing.T) {
1897 result, err := parseBoolResponse(nil)
1898 if err != nil {
1899 t.Errorf("parseBoolResponse returned error: %+v", err)
1900 }
1901
1902 if want := true; result != want {
1903 t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want)
1904 }
1905 }
1906
1907 func TestParseBooleanResponse_false(t *testing.T) {
1908 v := &ErrorResponse{Response: &http.Response{StatusCode: http.StatusNotFound}}
1909 result, err := parseBoolResponse(v)
1910 if err != nil {
1911 t.Errorf("parseBoolResponse returned error: %+v", err)
1912 }
1913
1914 if want := false; result != want {
1915 t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want)
1916 }
1917 }
1918
1919 func TestParseBooleanResponse_error(t *testing.T) {
1920 v := &ErrorResponse{Response: &http.Response{StatusCode: http.StatusBadRequest}}
1921 result, err := parseBoolResponse(v)
1922
1923 if err == nil {
1924 t.Errorf("Expected error to be returned.")
1925 }
1926
1927 if want := false; result != want {
1928 t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want)
1929 }
1930 }
1931
1932 func TestErrorResponse_Error(t *testing.T) {
1933 res := &http.Response{Request: &http.Request{}}
1934 err := ErrorResponse{Message: "m", Response: res}
1935 if err.Error() == "" {
1936 t.Errorf("Expected non-empty ErrorResponse.Error()")
1937 }
1938 }
1939
1940 func TestError_Error(t *testing.T) {
1941 err := Error{}
1942 if err.Error() == "" {
1943 t.Errorf("Expected non-empty Error.Error()")
1944 }
1945 }
1946
1947 func TestRateLimits(t *testing.T) {
1948 client, mux, _, teardown := setup()
1949 defer teardown()
1950
1951 mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) {
1952 testMethod(t, r, "GET")
1953 fmt.Fprint(w, `{"resources":{
1954 "core": {"limit":2,"remaining":1,"reset":1372700873},
1955 "search": {"limit":3,"remaining":2,"reset":1372700874},
1956 "graphql": {"limit":4,"remaining":3,"reset":1372700875},
1957 "integration_manifest": {"limit":5,"remaining":4,"reset":1372700876},
1958 "source_import": {"limit":6,"remaining":5,"reset":1372700877},
1959 "code_scanning_upload": {"limit":7,"remaining":6,"reset":1372700878},
1960 "actions_runner_registration": {"limit":8,"remaining":7,"reset":1372700879},
1961 "scim": {"limit":9,"remaining":8,"reset":1372700880}
1962 }}`)
1963 })
1964
1965 ctx := context.Background()
1966 rate, _, err := client.RateLimits(ctx)
1967 if err != nil {
1968 t.Errorf("RateLimits returned error: %v", err)
1969 }
1970
1971 want := &RateLimits{
1972 Core: &Rate{
1973 Limit: 2,
1974 Remaining: 1,
1975 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC).Local()},
1976 },
1977 Search: &Rate{
1978 Limit: 3,
1979 Remaining: 2,
1980 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()},
1981 },
1982 GraphQL: &Rate{
1983 Limit: 4,
1984 Remaining: 3,
1985 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 55, 0, time.UTC).Local()},
1986 },
1987 IntegrationManifest: &Rate{
1988 Limit: 5,
1989 Remaining: 4,
1990 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 56, 0, time.UTC).Local()},
1991 },
1992 SourceImport: &Rate{
1993 Limit: 6,
1994 Remaining: 5,
1995 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 57, 0, time.UTC).Local()},
1996 },
1997 CodeScanningUpload: &Rate{
1998 Limit: 7,
1999 Remaining: 6,
2000 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 58, 0, time.UTC).Local()},
2001 },
2002 ActionsRunnerRegistration: &Rate{
2003 Limit: 8,
2004 Remaining: 7,
2005 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 59, 0, time.UTC).Local()},
2006 },
2007 SCIM: &Rate{
2008 Limit: 9,
2009 Remaining: 8,
2010 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 48, 00, 0, time.UTC).Local()},
2011 },
2012 }
2013 if !cmp.Equal(rate, want) {
2014 t.Errorf("RateLimits returned %+v, want %+v", rate, want)
2015 }
2016
2017 if got, want := client.rateLimits[coreCategory], *want.Core; got != want {
2018 t.Errorf("client.rateLimits[coreCategory] is %+v, want %+v", got, want)
2019 }
2020 if got, want := client.rateLimits[searchCategory], *want.Search; got != want {
2021 t.Errorf("client.rateLimits[searchCategory] is %+v, want %+v", got, want)
2022 }
2023 if got, want := client.rateLimits[graphqlCategory], *want.GraphQL; got != want {
2024 t.Errorf("client.rateLimits[graphqlCategory] is %+v, want %+v", got, want)
2025 }
2026 if got, want := client.rateLimits[integrationManifestCategory], *want.IntegrationManifest; got != want {
2027 t.Errorf("client.rateLimits[integrationManifestCategory] is %+v, want %+v", got, want)
2028 }
2029 if got, want := client.rateLimits[sourceImportCategory], *want.SourceImport; got != want {
2030 t.Errorf("client.rateLimits[sourceImportCategory] is %+v, want %+v", got, want)
2031 }
2032 if got, want := client.rateLimits[codeScanningUploadCategory], *want.CodeScanningUpload; got != want {
2033 t.Errorf("client.rateLimits[codeScanningUploadCategory] is %+v, want %+v", got, want)
2034 }
2035 if got, want := client.rateLimits[actionsRunnerRegistrationCategory], *want.ActionsRunnerRegistration; got != want {
2036 t.Errorf("client.rateLimits[actionsRunnerRegistrationCategory] is %+v, want %+v", got, want)
2037 }
2038 if got, want := client.rateLimits[scimCategory], *want.SCIM; got != want {
2039 t.Errorf("client.rateLimits[scimCategory] is %+v, want %+v", got, want)
2040 }
2041 }
2042
2043 func TestRateLimits_coverage(t *testing.T) {
2044 client, _, _, teardown := setup()
2045 defer teardown()
2046
2047 ctx := context.Background()
2048
2049 const methodName = "RateLimits"
2050 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
2051 _, resp, err := client.RateLimits(ctx)
2052 return resp, err
2053 })
2054 }
2055
2056 func TestRateLimits_overQuota(t *testing.T) {
2057 client, mux, _, teardown := setup()
2058 defer teardown()
2059
2060 client.rateLimits[coreCategory] = Rate{
2061 Limit: 1,
2062 Remaining: 0,
2063 Reset: Timestamp{time.Now().Add(time.Hour).Local()},
2064 }
2065 mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) {
2066 fmt.Fprint(w, `{"resources":{
2067 "core": {"limit":2,"remaining":1,"reset":1372700873},
2068 "search": {"limit":3,"remaining":2,"reset":1372700874}
2069 }}`)
2070 })
2071
2072 ctx := context.Background()
2073 rate, _, err := client.RateLimits(ctx)
2074 if err != nil {
2075 t.Errorf("RateLimits returned error: %v", err)
2076 }
2077
2078 want := &RateLimits{
2079 Core: &Rate{
2080 Limit: 2,
2081 Remaining: 1,
2082 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC).Local()},
2083 },
2084 Search: &Rate{
2085 Limit: 3,
2086 Remaining: 2,
2087 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()},
2088 },
2089 }
2090 if !cmp.Equal(rate, want) {
2091 t.Errorf("RateLimits returned %+v, want %+v", rate, want)
2092 }
2093
2094 if got, want := client.rateLimits[coreCategory], *want.Core; got != want {
2095 t.Errorf("client.rateLimits[coreCategory] is %+v, want %+v", got, want)
2096 }
2097 if got, want := client.rateLimits[searchCategory], *want.Search; got != want {
2098 t.Errorf("client.rateLimits[searchCategory] is %+v, want %+v", got, want)
2099 }
2100 }
2101
2102 func TestSetCredentialsAsHeaders(t *testing.T) {
2103 req := new(http.Request)
2104 id, secret := "id", "secret"
2105 modifiedRequest := setCredentialsAsHeaders(req, id, secret)
2106
2107 actualID, actualSecret, ok := modifiedRequest.BasicAuth()
2108 if !ok {
2109 t.Errorf("request does not contain basic credentials")
2110 }
2111
2112 if actualID != id {
2113 t.Errorf("id is %s, want %s", actualID, id)
2114 }
2115
2116 if actualSecret != secret {
2117 t.Errorf("secret is %s, want %s", actualSecret, secret)
2118 }
2119 }
2120
2121 func TestUnauthenticatedRateLimitedTransport(t *testing.T) {
2122 client, mux, _, teardown := setup()
2123 defer teardown()
2124
2125 clientID, clientSecret := "id", "secret"
2126 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
2127 id, secret, ok := r.BasicAuth()
2128 if !ok {
2129 t.Errorf("request does not contain basic auth credentials")
2130 }
2131 if id != clientID {
2132 t.Errorf("request contained basic auth username %q, want %q", id, clientID)
2133 }
2134 if secret != clientSecret {
2135 t.Errorf("request contained basic auth password %q, want %q", secret, clientSecret)
2136 }
2137 })
2138
2139 tp := &UnauthenticatedRateLimitedTransport{
2140 ClientID: clientID,
2141 ClientSecret: clientSecret,
2142 }
2143 unauthedClient := NewClient(tp.Client())
2144 unauthedClient.BaseURL = client.BaseURL
2145 req, _ := unauthedClient.NewRequest("GET", ".", nil)
2146 ctx := context.Background()
2147 unauthedClient.Do(ctx, req, nil)
2148 }
2149
2150 func TestUnauthenticatedRateLimitedTransport_missingFields(t *testing.T) {
2151
2152 tp := &UnauthenticatedRateLimitedTransport{
2153 ClientSecret: "secret",
2154 }
2155 _, err := tp.RoundTrip(nil)
2156 if err == nil {
2157 t.Errorf("Expected error to be returned")
2158 }
2159
2160
2161 tp = &UnauthenticatedRateLimitedTransport{
2162 ClientID: "id",
2163 }
2164 _, err = tp.RoundTrip(nil)
2165 if err == nil {
2166 t.Errorf("Expected error to be returned")
2167 }
2168 }
2169
2170 func TestUnauthenticatedRateLimitedTransport_transport(t *testing.T) {
2171
2172 tp := &UnauthenticatedRateLimitedTransport{
2173 ClientID: "id",
2174 ClientSecret: "secret",
2175 }
2176 if tp.transport() != http.DefaultTransport {
2177 t.Errorf("Expected http.DefaultTransport to be used.")
2178 }
2179
2180
2181 tp = &UnauthenticatedRateLimitedTransport{
2182 ClientID: "id",
2183 ClientSecret: "secret",
2184 Transport: &http.Transport{},
2185 }
2186 if tp.transport() == http.DefaultTransport {
2187 t.Errorf("Expected custom transport to be used.")
2188 }
2189 }
2190
2191 func TestBasicAuthTransport(t *testing.T) {
2192 client, mux, _, teardown := setup()
2193 defer teardown()
2194
2195 username, password, otp := "u", "p", "123456"
2196
2197 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
2198 u, p, ok := r.BasicAuth()
2199 if !ok {
2200 t.Errorf("request does not contain basic auth credentials")
2201 }
2202 if u != username {
2203 t.Errorf("request contained basic auth username %q, want %q", u, username)
2204 }
2205 if p != password {
2206 t.Errorf("request contained basic auth password %q, want %q", p, password)
2207 }
2208 if got, want := r.Header.Get(headerOTP), otp; got != want {
2209 t.Errorf("request contained OTP %q, want %q", got, want)
2210 }
2211 })
2212
2213 tp := &BasicAuthTransport{
2214 Username: username,
2215 Password: password,
2216 OTP: otp,
2217 }
2218 basicAuthClient := NewClient(tp.Client())
2219 basicAuthClient.BaseURL = client.BaseURL
2220 req, _ := basicAuthClient.NewRequest("GET", ".", nil)
2221 ctx := context.Background()
2222 basicAuthClient.Do(ctx, req, nil)
2223 }
2224
2225 func TestBasicAuthTransport_transport(t *testing.T) {
2226
2227 tp := &BasicAuthTransport{}
2228 if tp.transport() != http.DefaultTransport {
2229 t.Errorf("Expected http.DefaultTransport to be used.")
2230 }
2231
2232
2233 tp = &BasicAuthTransport{
2234 Transport: &http.Transport{},
2235 }
2236 if tp.transport() == http.DefaultTransport {
2237 t.Errorf("Expected custom transport to be used.")
2238 }
2239 }
2240
2241 func TestFormatRateReset(t *testing.T) {
2242 d := 120*time.Minute + 12*time.Second
2243 got := formatRateReset(d)
2244 want := "[rate reset in 120m12s]"
2245 if got != want {
2246 t.Errorf("Format is wrong. got: %v, want: %v", got, want)
2247 }
2248
2249 d = 14*time.Minute + 2*time.Second
2250 got = formatRateReset(d)
2251 want = "[rate reset in 14m02s]"
2252 if got != want {
2253 t.Errorf("Format is wrong. got: %v, want: %v", got, want)
2254 }
2255
2256 d = 2*time.Minute + 2*time.Second
2257 got = formatRateReset(d)
2258 want = "[rate reset in 2m02s]"
2259 if got != want {
2260 t.Errorf("Format is wrong. got: %v, want: %v", got, want)
2261 }
2262
2263 d = 12 * time.Second
2264 got = formatRateReset(d)
2265 want = "[rate reset in 12s]"
2266 if got != want {
2267 t.Errorf("Format is wrong. got: %v, want: %v", got, want)
2268 }
2269
2270 d = -1 * (2*time.Hour + 2*time.Second)
2271 got = formatRateReset(d)
2272 want = "[rate limit was reset 120m02s ago]"
2273 if got != want {
2274 t.Errorf("Format is wrong. got: %v, want: %v", got, want)
2275 }
2276 }
2277
2278 func TestNestedStructAccessorNoPanic(t *testing.T) {
2279 issue := &Issue{User: nil}
2280 got := issue.GetUser().GetPlan().GetName()
2281 want := ""
2282 if got != want {
2283 t.Errorf("Issues.Get.GetUser().GetPlan().GetName() returned %+v, want %+v", got, want)
2284 }
2285 }
2286
2287 func TestTwoFactorAuthError(t *testing.T) {
2288 u, err := url.Parse("https://example.com")
2289 if err != nil {
2290 t.Fatal(err)
2291 }
2292
2293 e := &TwoFactorAuthError{
2294 Response: &http.Response{
2295 Request: &http.Request{Method: "PUT", URL: u},
2296 StatusCode: http.StatusTooManyRequests,
2297 },
2298 Message: "<msg>",
2299 }
2300 if got, want := e.Error(), "PUT https://example.com: 429 <msg> []"; got != want {
2301 t.Errorf("TwoFactorAuthError = %q, want %q", got, want)
2302 }
2303 }
2304
2305 func TestRateLimitError(t *testing.T) {
2306 u, err := url.Parse("https://example.com")
2307 if err != nil {
2308 t.Fatal(err)
2309 }
2310
2311 r := &RateLimitError{
2312 Response: &http.Response{
2313 Request: &http.Request{Method: "PUT", URL: u},
2314 StatusCode: http.StatusTooManyRequests,
2315 },
2316 Message: "<msg>",
2317 }
2318 if got, want := r.Error(), "PUT https://example.com: 429 <msg> [rate limit was reset"; !strings.Contains(got, want) {
2319 t.Errorf("RateLimitError = %q, want %q", got, want)
2320 }
2321 }
2322
2323 func TestAcceptedError(t *testing.T) {
2324 a := &AcceptedError{}
2325 if got, want := a.Error(), "try again later"; !strings.Contains(got, want) {
2326 t.Errorf("AcceptedError = %q, want %q", got, want)
2327 }
2328 }
2329
2330 func TestAbuseRateLimitError(t *testing.T) {
2331 u, err := url.Parse("https://example.com")
2332 if err != nil {
2333 t.Fatal(err)
2334 }
2335
2336 r := &AbuseRateLimitError{
2337 Response: &http.Response{
2338 Request: &http.Request{Method: "PUT", URL: u},
2339 StatusCode: http.StatusTooManyRequests,
2340 },
2341 Message: "<msg>",
2342 }
2343 if got, want := r.Error(), "PUT https://example.com: 429 <msg>"; got != want {
2344 t.Errorf("AbuseRateLimitError = %q, want %q", got, want)
2345 }
2346 }
2347
2348 func TestAddOptions_QueryValues(t *testing.T) {
2349 if _, err := addOptions("yo", ""); err == nil {
2350 t.Error("addOptions err = nil, want error")
2351 }
2352 }
2353
2354 func TestBareDo_returnsOpenBody(t *testing.T) {
2355 client, mux, _, teardown := setup()
2356 defer teardown()
2357
2358 expectedBody := "Hello from the other side !"
2359
2360 mux.HandleFunc("/test-url", func(w http.ResponseWriter, r *http.Request) {
2361 testMethod(t, r, "GET")
2362 fmt.Fprint(w, expectedBody)
2363 })
2364
2365 ctx := context.Background()
2366 req, err := client.NewRequest("GET", "test-url", nil)
2367 if err != nil {
2368 t.Fatalf("client.NewRequest returned error: %v", err)
2369 }
2370
2371 resp, err := client.BareDo(ctx, req)
2372 if err != nil {
2373 t.Fatalf("client.BareDo returned error: %v", err)
2374 }
2375
2376 got, err := ioutil.ReadAll(resp.Body)
2377 if err != nil {
2378 t.Fatalf("ioutil.ReadAll returned error: %v", err)
2379 }
2380 if string(got) != expectedBody {
2381 t.Fatalf("Expected %q, got %q", expectedBody, string(got))
2382 }
2383 if err := resp.Body.Close(); err != nil {
2384 t.Fatalf("resp.Body.Close() returned error: %v", err)
2385 }
2386 }
2387
2388
2389 type roundTripperFunc func(*http.Request) (*http.Response, error)
2390
2391 func (fn roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
2392 return fn(r)
2393 }
2394
2395 func TestErrorResponse_Marshal(t *testing.T) {
2396 testJSONMarshal(t, &ErrorResponse{}, "{}")
2397
2398 u := &ErrorResponse{
2399 Message: "msg",
2400 Errors: []Error{
2401 {
2402 Resource: "res",
2403 Field: "f",
2404 Code: "c",
2405 Message: "msg",
2406 },
2407 },
2408 Block: &ErrorBlock{
2409 Reason: "reason",
2410 CreatedAt: &Timestamp{referenceTime},
2411 },
2412 DocumentationURL: "doc",
2413 }
2414
2415 want := `{
2416 "message": "msg",
2417 "errors": [
2418 {
2419 "resource": "res",
2420 "field": "f",
2421 "code": "c",
2422 "message": "msg"
2423 }
2424 ],
2425 "block": {
2426 "reason": "reason",
2427 "created_at": ` + referenceTimeStr + `
2428 },
2429 "documentation_url": "doc"
2430 }`
2431
2432 testJSONMarshal(t, u, want)
2433 }
2434
2435 func TestErrorBlock_Marshal(t *testing.T) {
2436 testJSONMarshal(t, &ErrorBlock{}, "{}")
2437
2438 u := &ErrorBlock{
2439 Reason: "reason",
2440 CreatedAt: &Timestamp{referenceTime},
2441 }
2442
2443 want := `{
2444 "reason": "reason",
2445 "created_at": ` + referenceTimeStr + `
2446 }`
2447
2448 testJSONMarshal(t, u, want)
2449 }
2450
2451 func TestRateLimitError_Marshal(t *testing.T) {
2452 testJSONMarshal(t, &RateLimitError{}, "{}")
2453
2454 u := &RateLimitError{
2455 Rate: Rate{
2456 Limit: 1,
2457 Remaining: 1,
2458 Reset: Timestamp{referenceTime},
2459 },
2460 Message: "msg",
2461 }
2462
2463 want := `{
2464 "Rate": {
2465 "limit": 1,
2466 "remaining": 1,
2467 "reset": ` + referenceTimeStr + `
2468 },
2469 "message": "msg"
2470 }`
2471
2472 testJSONMarshal(t, u, want)
2473 }
2474
2475 func TestAbuseRateLimitError_Marshal(t *testing.T) {
2476 testJSONMarshal(t, &AbuseRateLimitError{}, "{}")
2477
2478 u := &AbuseRateLimitError{
2479 Message: "msg",
2480 }
2481
2482 want := `{
2483 "message": "msg"
2484 }`
2485
2486 testJSONMarshal(t, u, want)
2487 }
2488
2489 func TestError_Marshal(t *testing.T) {
2490 testJSONMarshal(t, &Error{}, "{}")
2491
2492 u := &Error{
2493 Resource: "res",
2494 Field: "field",
2495 Code: "code",
2496 Message: "msg",
2497 }
2498
2499 want := `{
2500 "resource": "res",
2501 "field": "field",
2502 "code": "code",
2503 "message": "msg"
2504 }`
2505
2506 testJSONMarshal(t, u, want)
2507 }
2508
2509 func TestRate_Marshal(t *testing.T) {
2510 testJSONMarshal(t, &Rate{}, "{}")
2511
2512 u := &Rate{
2513 Limit: 1,
2514 Remaining: 1,
2515 Reset: Timestamp{referenceTime},
2516 }
2517
2518 want := `{
2519 "limit": 1,
2520 "remaining": 1,
2521 "reset": ` + referenceTimeStr + `
2522 }`
2523
2524 testJSONMarshal(t, u, want)
2525 }
2526
2527 func TestRateLimits_Marshal(t *testing.T) {
2528 testJSONMarshal(t, &RateLimits{}, "{}")
2529
2530 u := &RateLimits{
2531 Core: &Rate{
2532 Limit: 1,
2533 Remaining: 1,
2534 Reset: Timestamp{referenceTime},
2535 },
2536 Search: &Rate{
2537 Limit: 1,
2538 Remaining: 1,
2539 Reset: Timestamp{referenceTime},
2540 },
2541 GraphQL: &Rate{
2542 Limit: 1,
2543 Remaining: 1,
2544 Reset: Timestamp{referenceTime},
2545 },
2546 IntegrationManifest: &Rate{
2547 Limit: 1,
2548 Remaining: 1,
2549 Reset: Timestamp{referenceTime},
2550 },
2551 SourceImport: &Rate{
2552 Limit: 1,
2553 Remaining: 1,
2554 Reset: Timestamp{referenceTime},
2555 },
2556 CodeScanningUpload: &Rate{
2557 Limit: 1,
2558 Remaining: 1,
2559 Reset: Timestamp{referenceTime},
2560 },
2561 ActionsRunnerRegistration: &Rate{
2562 Limit: 1,
2563 Remaining: 1,
2564 Reset: Timestamp{referenceTime},
2565 },
2566 SCIM: &Rate{
2567 Limit: 1,
2568 Remaining: 1,
2569 Reset: Timestamp{referenceTime},
2570 },
2571 }
2572
2573 want := `{
2574 "core": {
2575 "limit": 1,
2576 "remaining": 1,
2577 "reset": ` + referenceTimeStr + `
2578 },
2579 "search": {
2580 "limit": 1,
2581 "remaining": 1,
2582 "reset": ` + referenceTimeStr + `
2583 },
2584 "graphql": {
2585 "limit": 1,
2586 "remaining": 1,
2587 "reset": ` + referenceTimeStr + `
2588 },
2589 "integration_manifest": {
2590 "limit": 1,
2591 "remaining": 1,
2592 "reset": ` + referenceTimeStr + `
2593 },
2594 "source_import": {
2595 "limit": 1,
2596 "remaining": 1,
2597 "reset": ` + referenceTimeStr + `
2598 },
2599 "code_scanning_upload": {
2600 "limit": 1,
2601 "remaining": 1,
2602 "reset": ` + referenceTimeStr + `
2603 },
2604 "actions_runner_registration": {
2605 "limit": 1,
2606 "remaining": 1,
2607 "reset": ` + referenceTimeStr + `
2608 },
2609 "scim": {
2610 "limit": 1,
2611 "remaining": 1,
2612 "reset": ` + referenceTimeStr + `
2613 }
2614 }`
2615
2616 testJSONMarshal(t, u, want)
2617 }
2618
2619 func TestParseTokenExpiration(t *testing.T) {
2620 tests := []struct {
2621 header string
2622 want Timestamp
2623 }{
2624 {
2625 header: "",
2626 want: Timestamp{},
2627 },
2628 {
2629 header: "this is a garbage",
2630 want: Timestamp{},
2631 },
2632 {
2633 header: "2021-09-03 02:34:04 UTC",
2634 want: Timestamp{time.Date(2021, time.September, 3, 2, 34, 4, 0, time.UTC)},
2635 },
2636 }
2637
2638 for _, tt := range tests {
2639 res := &http.Response{
2640 Request: &http.Request{},
2641 Header: http.Header{},
2642 }
2643
2644 res.Header.Set(headerTokenExpiration, tt.header)
2645 exp := parseTokenExpiration(res)
2646 if !exp.Equal(tt.want) {
2647 t.Errorf("parseTokenExpiration returned %#v, want %#v", exp, tt.want)
2648 }
2649 }
2650 }
2651
View as plain text