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