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