github.com/google/go-github/v33@v33.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/ioutil" 14 "net/http" 15 "net/http/httptest" 16 "net/url" 17 "os" 18 "path" 19 "reflect" 20 "strings" 21 "testing" 22 "time" 23 ) 24 25 const ( 26 // baseURLPath is a non-empty Client.BaseURL path to use during tests, 27 // to ensure relative URLs are used for all endpoints. See issue #752. 28 baseURLPath = "/api-v3" 29 ) 30 31 // setup sets up a test HTTP server along with a github.Client that is 32 // configured to talk to that test server. Tests should register handlers on 33 // mux which provide mock responses for the API method being tested. 34 func setup() (client *Client, mux *http.ServeMux, serverURL string, teardown func()) { 35 // mux is the HTTP request multiplexer used with the test server. 36 mux = http.NewServeMux() 37 38 // We want to ensure that tests catch mistakes where the endpoint URL is 39 // specified as absolute rather than relative. It only makes a difference 40 // when there's a non-empty base URL path. So, use that. See issue #752. 41 apiHandler := http.NewServeMux() 42 apiHandler.Handle(baseURLPath+"/", http.StripPrefix(baseURLPath, mux)) 43 apiHandler.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 44 fmt.Fprintln(os.Stderr, "FAIL: Client.BaseURL path prefix is not preserved in the request URL:") 45 fmt.Fprintln(os.Stderr) 46 fmt.Fprintln(os.Stderr, "\t"+req.URL.String()) 47 fmt.Fprintln(os.Stderr) 48 fmt.Fprintln(os.Stderr, "\tDid you accidentally use an absolute endpoint URL rather than relative?") 49 fmt.Fprintln(os.Stderr, "\tSee https://github.com/google/go-github/issues/752 for information.") 50 http.Error(w, "Client.BaseURL path prefix is not preserved in the request URL.", http.StatusInternalServerError) 51 }) 52 53 // server is a test HTTP server used to provide mock API responses. 54 server := httptest.NewServer(apiHandler) 55 56 // client is the GitHub client being tested and is 57 // configured to use test server. 58 client = NewClient(nil) 59 url, _ := url.Parse(server.URL + baseURLPath + "/") 60 client.BaseURL = url 61 client.UploadURL = url 62 63 return client, mux, server.URL, server.Close 64 } 65 66 // openTestFile creates a new file with the given name and content for testing. 67 // In order to ensure the exact file name, this function will create a new temp 68 // directory, and create the file in that directory. It is the caller's 69 // responsibility to remove the directory and its contents when no longer needed. 70 func openTestFile(name, content string) (file *os.File, dir string, err error) { 71 dir, err = ioutil.TempDir("", "go-github") 72 if err != nil { 73 return nil, dir, err 74 } 75 76 file, err = os.OpenFile(path.Join(dir, name), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) 77 if err != nil { 78 return nil, dir, err 79 } 80 81 fmt.Fprint(file, content) 82 83 // close and re-open the file to keep file.Stat() happy 84 file.Close() 85 file, err = os.Open(file.Name()) 86 if err != nil { 87 return nil, dir, err 88 } 89 90 return file, dir, err 91 } 92 93 func testMethod(t *testing.T, r *http.Request, want string) { 94 t.Helper() 95 if got := r.Method; got != want { 96 t.Errorf("Request method: %v, want %v", got, want) 97 } 98 } 99 100 type values map[string]string 101 102 func testFormValues(t *testing.T, r *http.Request, values values) { 103 t.Helper() 104 want := url.Values{} 105 for k, v := range values { 106 want.Set(k, v) 107 } 108 109 r.ParseForm() 110 if got := r.Form; !reflect.DeepEqual(got, want) { 111 t.Errorf("Request parameters: %v, want %v", got, want) 112 } 113 } 114 115 func testHeader(t *testing.T, r *http.Request, header string, want string) { 116 t.Helper() 117 if got := r.Header.Get(header); got != want { 118 t.Errorf("Header.Get(%q) returned %q, want %q", header, got, want) 119 } 120 } 121 122 func testURLParseError(t *testing.T, err error) { 123 t.Helper() 124 if err == nil { 125 t.Errorf("Expected error to be returned") 126 } 127 if err, ok := err.(*url.Error); !ok || err.Op != "parse" { 128 t.Errorf("Expected URL parse error, got %+v", err) 129 } 130 } 131 132 func testBody(t *testing.T, r *http.Request, want string) { 133 t.Helper() 134 b, err := ioutil.ReadAll(r.Body) 135 if err != nil { 136 t.Errorf("Error reading request body: %v", err) 137 } 138 if got := string(b); got != want { 139 t.Errorf("request Body is %s, want %s", got, want) 140 } 141 } 142 143 // Test whether the marshaling of v produces JSON that corresponds 144 // to the want string. 145 func testJSONMarshal(t *testing.T, v interface{}, want string) { 146 t.Helper() 147 // Unmarshal the wanted JSON, to verify its correctness, and marshal it back 148 // to sort the keys. 149 u := reflect.New(reflect.TypeOf(v)).Interface() 150 if err := json.Unmarshal([]byte(want), &u); err != nil { 151 t.Errorf("Unable to unmarshal JSON for %v: %v", want, err) 152 } 153 w, err := json.Marshal(u) 154 if err != nil { 155 t.Errorf("Unable to marshal JSON for %#v", u) 156 } 157 158 // Marshal the target value. 159 j, err := json.Marshal(v) 160 if err != nil { 161 t.Errorf("Unable to marshal JSON for %#v", v) 162 } 163 164 if string(w) != string(j) { 165 t.Errorf("json.Marshal(%q) returned %s, want %s", v, j, w) 166 } 167 } 168 169 func TestNewClient(t *testing.T) { 170 c := NewClient(nil) 171 172 if got, want := c.BaseURL.String(), defaultBaseURL; got != want { 173 t.Errorf("NewClient BaseURL is %v, want %v", got, want) 174 } 175 if got, want := c.UserAgent, userAgent; got != want { 176 t.Errorf("NewClient UserAgent is %v, want %v", got, want) 177 } 178 179 c2 := NewClient(nil) 180 if c.client == c2.client { 181 t.Error("NewClient returned same http.Clients, but they should differ") 182 } 183 } 184 185 func TestNewEnterpriseClient(t *testing.T) { 186 baseURL := "https://custom-url/api/v3/" 187 uploadURL := "https://custom-upload-url/api/uploads/" 188 c, err := NewEnterpriseClient(baseURL, uploadURL, nil) 189 if err != nil { 190 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err) 191 } 192 193 if got, want := c.BaseURL.String(), baseURL; got != want { 194 t.Errorf("NewClient BaseURL is %v, want %v", got, want) 195 } 196 if got, want := c.UploadURL.String(), uploadURL; got != want { 197 t.Errorf("NewClient UploadURL is %v, want %v", got, want) 198 } 199 } 200 201 func TestNewEnterpriseClient_addsTrailingSlashToURLs(t *testing.T) { 202 baseURL := "https://custom-url/api/v3" 203 uploadURL := "https://custom-upload-url/api/uploads" 204 formattedBaseURL := baseURL + "/" 205 formattedUploadURL := uploadURL + "/" 206 207 c, err := NewEnterpriseClient(baseURL, uploadURL, nil) 208 if err != nil { 209 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err) 210 } 211 212 if got, want := c.BaseURL.String(), formattedBaseURL; got != want { 213 t.Errorf("NewClient BaseURL is %v, want %v", got, want) 214 } 215 if got, want := c.UploadURL.String(), formattedUploadURL; got != want { 216 t.Errorf("NewClient UploadURL is %v, want %v", got, want) 217 } 218 } 219 220 func TestNewEnterpriseClient_addsEnterpriseSuffixToURLs(t *testing.T) { 221 baseURL := "https://custom-url/" 222 uploadURL := "https://custom-upload-url/" 223 formattedBaseURL := baseURL + "api/v3/" 224 formattedUploadURL := uploadURL + "api/uploads/" 225 226 c, err := NewEnterpriseClient(baseURL, uploadURL, nil) 227 if err != nil { 228 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err) 229 } 230 231 if got, want := c.BaseURL.String(), formattedBaseURL; got != want { 232 t.Errorf("NewClient BaseURL is %v, want %v", got, want) 233 } 234 if got, want := c.UploadURL.String(), formattedUploadURL; got != want { 235 t.Errorf("NewClient UploadURL is %v, want %v", got, want) 236 } 237 } 238 239 func TestNewEnterpriseClient_addsEnterpriseSuffixAndTrailingSlashToURLs(t *testing.T) { 240 baseURL := "https://custom-url" 241 uploadURL := "https://custom-upload-url" 242 formattedBaseURL := baseURL + "/api/v3/" 243 formattedUploadURL := uploadURL + "/api/uploads/" 244 245 c, err := NewEnterpriseClient(baseURL, uploadURL, nil) 246 if err != nil { 247 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err) 248 } 249 250 if got, want := c.BaseURL.String(), formattedBaseURL; got != want { 251 t.Errorf("NewClient BaseURL is %v, want %v", got, want) 252 } 253 if got, want := c.UploadURL.String(), formattedUploadURL; got != want { 254 t.Errorf("NewClient UploadURL is %v, want %v", got, want) 255 } 256 } 257 258 func TestNewEnterpriseClient_URLHasExistingAPIPrefix_AddTrailingSlash(t *testing.T) { 259 baseURL := "https://api.custom-url" 260 uploadURL := "https://api.custom-upload-url" 261 formattedBaseURL := baseURL + "/" 262 formattedUploadURL := uploadURL + "/" 263 264 c, err := NewEnterpriseClient(baseURL, uploadURL, nil) 265 if err != nil { 266 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err) 267 } 268 269 if got, want := c.BaseURL.String(), formattedBaseURL; got != want { 270 t.Errorf("NewClient BaseURL is %v, want %v", got, want) 271 } 272 if got, want := c.UploadURL.String(), formattedUploadURL; got != want { 273 t.Errorf("NewClient UploadURL is %v, want %v", got, want) 274 } 275 } 276 277 func TestNewEnterpriseClient_URLHasExistingAPIPrefixAndTrailingSlash(t *testing.T) { 278 baseURL := "https://api.custom-url/" 279 uploadURL := "https://api.custom-upload-url/" 280 281 c, err := NewEnterpriseClient(baseURL, uploadURL, nil) 282 if err != nil { 283 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err) 284 } 285 286 if got, want := c.BaseURL.String(), baseURL; got != want { 287 t.Errorf("NewClient BaseURL is %v, want %v", got, want) 288 } 289 if got, want := c.UploadURL.String(), uploadURL; got != want { 290 t.Errorf("NewClient UploadURL is %v, want %v", got, want) 291 } 292 } 293 294 func TestNewEnterpriseClient_URLHasAPISubdomain_AddTrailingSlash(t *testing.T) { 295 baseURL := "https://catalog.api.custom-url" 296 uploadURL := "https://catalog.api.custom-upload-url" 297 formattedBaseURL := baseURL + "/" 298 formattedUploadURL := uploadURL + "/" 299 300 c, err := NewEnterpriseClient(baseURL, uploadURL, nil) 301 if err != nil { 302 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err) 303 } 304 305 if got, want := c.BaseURL.String(), formattedBaseURL; got != want { 306 t.Errorf("NewClient BaseURL is %v, want %v", got, want) 307 } 308 if got, want := c.UploadURL.String(), formattedUploadURL; got != want { 309 t.Errorf("NewClient UploadURL is %v, want %v", got, want) 310 } 311 } 312 313 func TestNewEnterpriseClient_URLHasAPISubdomainAndTrailingSlash(t *testing.T) { 314 baseURL := "https://catalog.api.custom-url/" 315 uploadURL := "https://catalog.api.custom-upload-url/" 316 317 c, err := NewEnterpriseClient(baseURL, uploadURL, nil) 318 if err != nil { 319 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err) 320 } 321 322 if got, want := c.BaseURL.String(), baseURL; got != want { 323 t.Errorf("NewClient BaseURL is %v, want %v", got, want) 324 } 325 if got, want := c.UploadURL.String(), uploadURL; got != want { 326 t.Errorf("NewClient UploadURL is %v, want %v", got, want) 327 } 328 } 329 330 func TestNewEnterpriseClient_URLIsNotAProperAPISubdomain_addsEnterpriseSuffixAndSlash(t *testing.T) { 331 baseURL := "https://cloud-api.custom-url" 332 uploadURL := "https://cloud-api.custom-upload-url" 333 formattedBaseURL := baseURL + "/api/v3/" 334 formattedUploadURL := uploadURL + "/api/uploads/" 335 336 c, err := NewEnterpriseClient(baseURL, uploadURL, nil) 337 if err != nil { 338 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err) 339 } 340 341 if got, want := c.BaseURL.String(), formattedBaseURL; got != want { 342 t.Errorf("NewClient BaseURL is %v, want %v", got, want) 343 } 344 if got, want := c.UploadURL.String(), formattedUploadURL; got != want { 345 t.Errorf("NewClient UploadURL is %v, want %v", got, want) 346 } 347 } 348 349 func TestNewEnterpriseClient_URLIsNotAProperAPISubdomain_addsEnterpriseSuffix(t *testing.T) { 350 baseURL := "https://cloud-api.custom-url/" 351 uploadURL := "https://cloud-api.custom-upload-url/" 352 formattedBaseURL := baseURL + "api/v3/" 353 formattedUploadURL := uploadURL + "api/uploads/" 354 355 c, err := NewEnterpriseClient(baseURL, uploadURL, nil) 356 if err != nil { 357 t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err) 358 } 359 360 if got, want := c.BaseURL.String(), formattedBaseURL; got != want { 361 t.Errorf("NewClient BaseURL is %v, want %v", got, want) 362 } 363 if got, want := c.UploadURL.String(), formattedUploadURL; got != want { 364 t.Errorf("NewClient UploadURL is %v, want %v", got, want) 365 } 366 } 367 368 // Ensure that length of Client.rateLimits is the same as number of fields in RateLimits struct. 369 func TestClient_rateLimits(t *testing.T) { 370 if got, want := len(Client{}.rateLimits), reflect.TypeOf(RateLimits{}).NumField(); got != want { 371 t.Errorf("len(Client{}.rateLimits) is %v, want %v", got, want) 372 } 373 } 374 375 func TestRateLimits_String(t *testing.T) { 376 v := RateLimits{ 377 Core: &Rate{}, 378 Search: &Rate{}, 379 } 380 want := `github.RateLimits{Core:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, Search:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}}` 381 if got := v.String(); got != want { 382 t.Errorf("RateLimits.String = %v, want %v", got, want) 383 } 384 } 385 386 func TestNewRequest(t *testing.T) { 387 c := NewClient(nil) 388 389 inURL, outURL := "/foo", defaultBaseURL+"foo" 390 inBody, outBody := &User{Login: String("l")}, `{"login":"l"}`+"\n" 391 req, _ := c.NewRequest("GET", inURL, inBody) 392 393 // test that relative URL was expanded 394 if got, want := req.URL.String(), outURL; got != want { 395 t.Errorf("NewRequest(%q) URL is %v, want %v", inURL, got, want) 396 } 397 398 // test that body was JSON encoded 399 body, _ := ioutil.ReadAll(req.Body) 400 if got, want := string(body), outBody; got != want { 401 t.Errorf("NewRequest(%q) Body is %v, want %v", inBody, got, want) 402 } 403 404 // test that default user-agent is attached to the request 405 if got, want := req.Header.Get("User-Agent"), c.UserAgent; got != want { 406 t.Errorf("NewRequest() User-Agent is %v, want %v", got, want) 407 } 408 } 409 410 func TestNewRequest_invalidJSON(t *testing.T) { 411 c := NewClient(nil) 412 413 type T struct { 414 A map[interface{}]interface{} 415 } 416 _, err := c.NewRequest("GET", ".", &T{}) 417 418 if err == nil { 419 t.Error("Expected error to be returned.") 420 } 421 if err, ok := err.(*json.UnsupportedTypeError); !ok { 422 t.Errorf("Expected a JSON error; got %#v.", err) 423 } 424 } 425 426 func TestNewRequest_badURL(t *testing.T) { 427 c := NewClient(nil) 428 _, err := c.NewRequest("GET", ":", nil) 429 testURLParseError(t, err) 430 } 431 432 // ensure that no User-Agent header is set if the client's UserAgent is empty. 433 // This caused a problem with Google's internal http client. 434 func TestNewRequest_emptyUserAgent(t *testing.T) { 435 c := NewClient(nil) 436 c.UserAgent = "" 437 req, err := c.NewRequest("GET", ".", nil) 438 if err != nil { 439 t.Fatalf("NewRequest returned unexpected error: %v", err) 440 } 441 if _, ok := req.Header["User-Agent"]; ok { 442 t.Fatal("constructed request contains unexpected User-Agent header") 443 } 444 } 445 446 // If a nil body is passed to github.NewRequest, make sure that nil is also 447 // passed to http.NewRequest. In most cases, passing an io.Reader that returns 448 // no content is fine, since there is no difference between an HTTP request 449 // body that is an empty string versus one that is not set at all. However in 450 // certain cases, intermediate systems may treat these differently resulting in 451 // subtle errors. 452 func TestNewRequest_emptyBody(t *testing.T) { 453 c := NewClient(nil) 454 req, err := c.NewRequest("GET", ".", nil) 455 if err != nil { 456 t.Fatalf("NewRequest returned unexpected error: %v", err) 457 } 458 if req.Body != nil { 459 t.Fatalf("constructed request contains a non-nil Body") 460 } 461 } 462 463 func TestNewRequest_errorForNoTrailingSlash(t *testing.T) { 464 tests := []struct { 465 rawurl string 466 wantError bool 467 }{ 468 {rawurl: "https://example.com/api/v3", wantError: true}, 469 {rawurl: "https://example.com/api/v3/", wantError: false}, 470 } 471 c := NewClient(nil) 472 for _, test := range tests { 473 u, err := url.Parse(test.rawurl) 474 if err != nil { 475 t.Fatalf("url.Parse returned unexpected error: %v.", err) 476 } 477 c.BaseURL = u 478 if _, err := c.NewRequest(http.MethodGet, "test", nil); test.wantError && err == nil { 479 t.Fatalf("Expected error to be returned.") 480 } else if !test.wantError && err != nil { 481 t.Fatalf("NewRequest returned unexpected error: %v.", err) 482 } 483 } 484 } 485 486 func TestNewUploadRequest_errorForNoTrailingSlash(t *testing.T) { 487 tests := []struct { 488 rawurl string 489 wantError bool 490 }{ 491 {rawurl: "https://example.com/api/uploads", wantError: true}, 492 {rawurl: "https://example.com/api/uploads/", wantError: false}, 493 } 494 c := NewClient(nil) 495 for _, test := range tests { 496 u, err := url.Parse(test.rawurl) 497 if err != nil { 498 t.Fatalf("url.Parse returned unexpected error: %v.", err) 499 } 500 c.UploadURL = u 501 if _, err = c.NewUploadRequest("test", nil, 0, ""); test.wantError && err == nil { 502 t.Fatalf("Expected error to be returned.") 503 } else if !test.wantError && err != nil { 504 t.Fatalf("NewUploadRequest returned unexpected error: %v.", err) 505 } 506 } 507 } 508 509 func TestResponse_populatePageValues(t *testing.T) { 510 r := http.Response{ 511 Header: http.Header{ 512 "Link": {`<https://api.github.com/?page=1>; rel="first",` + 513 ` <https://api.github.com/?page=2>; rel="prev",` + 514 ` <https://api.github.com/?page=4>; rel="next",` + 515 ` <https://api.github.com/?page=5>; rel="last"`, 516 }, 517 }, 518 } 519 520 response := newResponse(&r) 521 if got, want := response.FirstPage, 1; got != want { 522 t.Errorf("response.FirstPage: %v, want %v", got, want) 523 } 524 if got, want := response.PrevPage, 2; want != got { 525 t.Errorf("response.PrevPage: %v, want %v", got, want) 526 } 527 if got, want := response.NextPage, 4; want != got { 528 t.Errorf("response.NextPage: %v, want %v", got, want) 529 } 530 if got, want := response.LastPage, 5; want != got { 531 t.Errorf("response.LastPage: %v, want %v", got, want) 532 } 533 if got, want := response.NextPageToken, ""; want != got { 534 t.Errorf("response.NextPageToken: %v, want %v", got, want) 535 } 536 } 537 538 func TestResponse_cursorPagination(t *testing.T) { 539 r := http.Response{ 540 Header: http.Header{ 541 "Status": {"200 OK"}, 542 "Link": {`<https://api.github.com/resource?per_page=2&page=url-encoded-next-page-token>; rel="next"`}, 543 }, 544 } 545 546 response := newResponse(&r) 547 if got, want := response.FirstPage, 0; got != want { 548 t.Errorf("response.FirstPage: %v, want %v", got, want) 549 } 550 if got, want := response.PrevPage, 0; want != got { 551 t.Errorf("response.PrevPage: %v, want %v", got, want) 552 } 553 if got, want := response.NextPage, 0; want != got { 554 t.Errorf("response.NextPage: %v, want %v", got, want) 555 } 556 if got, want := response.LastPage, 0; want != got { 557 t.Errorf("response.LastPage: %v, want %v", got, want) 558 } 559 if got, want := response.NextPageToken, "url-encoded-next-page-token"; want != got { 560 t.Errorf("response.NextPageToken: %v, want %v", got, want) 561 } 562 } 563 564 func TestResponse_populatePageValues_invalid(t *testing.T) { 565 r := http.Response{ 566 Header: http.Header{ 567 "Link": {`<https://api.github.com/?page=1>,` + 568 `<https://api.github.com/?page=abc>; rel="first",` + 569 `https://api.github.com/?page=2; rel="prev",` + 570 `<https://api.github.com/>; rel="next",` + 571 `<https://api.github.com/?page=>; rel="last"`, 572 }, 573 }, 574 } 575 576 response := newResponse(&r) 577 if got, want := response.FirstPage, 0; got != want { 578 t.Errorf("response.FirstPage: %v, want %v", got, want) 579 } 580 if got, want := response.PrevPage, 0; got != want { 581 t.Errorf("response.PrevPage: %v, want %v", got, want) 582 } 583 if got, want := response.NextPage, 0; got != want { 584 t.Errorf("response.NextPage: %v, want %v", got, want) 585 } 586 if got, want := response.LastPage, 0; got != want { 587 t.Errorf("response.LastPage: %v, want %v", got, want) 588 } 589 590 // more invalid URLs 591 r = http.Response{ 592 Header: http.Header{ 593 "Link": {`<https://api.github.com/%?page=2>; rel="first"`}, 594 }, 595 } 596 597 response = newResponse(&r) 598 if got, want := response.FirstPage, 0; got != want { 599 t.Errorf("response.FirstPage: %v, want %v", got, want) 600 } 601 } 602 603 func TestDo(t *testing.T) { 604 client, mux, _, teardown := setup() 605 defer teardown() 606 607 type foo struct { 608 A string 609 } 610 611 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 612 testMethod(t, r, "GET") 613 fmt.Fprint(w, `{"A":"a"}`) 614 }) 615 616 req, _ := client.NewRequest("GET", ".", nil) 617 body := new(foo) 618 client.Do(context.Background(), req, body) 619 620 want := &foo{"a"} 621 if !reflect.DeepEqual(body, want) { 622 t.Errorf("Response body = %v, want %v", body, want) 623 } 624 } 625 626 func TestDo_nilContext(t *testing.T) { 627 client, _, _, teardown := setup() 628 defer teardown() 629 630 req, _ := client.NewRequest("GET", ".", nil) 631 _, err := client.Do(nil, req, nil) 632 633 if !reflect.DeepEqual(err, errors.New("context must be non-nil")) { 634 t.Errorf("Expected context must be non-nil error") 635 } 636 } 637 638 func TestDo_httpError(t *testing.T) { 639 client, mux, _, teardown := setup() 640 defer teardown() 641 642 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 643 http.Error(w, "Bad Request", 400) 644 }) 645 646 req, _ := client.NewRequest("GET", ".", nil) 647 resp, err := client.Do(context.Background(), req, nil) 648 649 if err == nil { 650 t.Fatal("Expected HTTP 400 error, got no error.") 651 } 652 if resp.StatusCode != 400 { 653 t.Errorf("Expected HTTP 400 error, got %d status code.", resp.StatusCode) 654 } 655 } 656 657 // Test handling of an error caused by the internal http client's Do() 658 // function. A redirect loop is pretty unlikely to occur within the GitHub 659 // API, but does allow us to exercise the right code path. 660 func TestDo_redirectLoop(t *testing.T) { 661 client, mux, _, teardown := setup() 662 defer teardown() 663 664 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 665 http.Redirect(w, r, baseURLPath, http.StatusFound) 666 }) 667 668 req, _ := client.NewRequest("GET", ".", nil) 669 _, err := client.Do(context.Background(), req, nil) 670 671 if err == nil { 672 t.Error("Expected error to be returned.") 673 } 674 if err, ok := err.(*url.Error); !ok { 675 t.Errorf("Expected a URL error; got %#v.", err) 676 } 677 } 678 679 // Test that an error caused by the internal http client's Do() function 680 // does not leak the client secret. 681 func TestDo_sanitizeURL(t *testing.T) { 682 tp := &UnauthenticatedRateLimitedTransport{ 683 ClientID: "id", 684 ClientSecret: "secret", 685 } 686 unauthedClient := NewClient(tp.Client()) 687 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". 688 req, err := unauthedClient.NewRequest("GET", ".", nil) 689 if err != nil { 690 t.Fatalf("NewRequest returned unexpected error: %v", err) 691 } 692 _, err = unauthedClient.Do(context.Background(), req, nil) 693 if err == nil { 694 t.Fatal("Expected error to be returned.") 695 } 696 if strings.Contains(err.Error(), "client_secret=secret") { 697 t.Errorf("Do error contains secret, should be redacted:\n%q", err) 698 } 699 } 700 701 func TestDo_rateLimit(t *testing.T) { 702 client, mux, _, teardown := setup() 703 defer teardown() 704 705 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 706 w.Header().Set(headerRateLimit, "60") 707 w.Header().Set(headerRateRemaining, "59") 708 w.Header().Set(headerRateReset, "1372700873") 709 }) 710 711 req, _ := client.NewRequest("GET", ".", nil) 712 resp, err := client.Do(context.Background(), req, nil) 713 if err != nil { 714 t.Errorf("Do returned unexpected error: %v", err) 715 } 716 if got, want := resp.Rate.Limit, 60; got != want { 717 t.Errorf("Client rate limit = %v, want %v", got, want) 718 } 719 if got, want := resp.Rate.Remaining, 59; got != want { 720 t.Errorf("Client rate remaining = %v, want %v", got, want) 721 } 722 reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC) 723 if resp.Rate.Reset.UTC() != reset { 724 t.Errorf("Client rate reset = %v, want %v", resp.Rate.Reset, reset) 725 } 726 } 727 728 // ensure rate limit is still parsed, even for error responses 729 func TestDo_rateLimit_errorResponse(t *testing.T) { 730 client, mux, _, teardown := setup() 731 defer teardown() 732 733 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 734 w.Header().Set(headerRateLimit, "60") 735 w.Header().Set(headerRateRemaining, "59") 736 w.Header().Set(headerRateReset, "1372700873") 737 http.Error(w, "Bad Request", 400) 738 }) 739 740 req, _ := client.NewRequest("GET", ".", nil) 741 resp, err := client.Do(context.Background(), req, nil) 742 if err == nil { 743 t.Error("Expected error to be returned.") 744 } 745 if _, ok := err.(*RateLimitError); ok { 746 t.Errorf("Did not expect a *RateLimitError error; got %#v.", err) 747 } 748 if got, want := resp.Rate.Limit, 60; got != want { 749 t.Errorf("Client rate limit = %v, want %v", got, want) 750 } 751 if got, want := resp.Rate.Remaining, 59; got != want { 752 t.Errorf("Client rate remaining = %v, want %v", got, want) 753 } 754 reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC) 755 if resp.Rate.Reset.UTC() != reset { 756 t.Errorf("Client rate reset = %v, want %v", resp.Rate.Reset, reset) 757 } 758 } 759 760 // Ensure *RateLimitError is returned when API rate limit is exceeded. 761 func TestDo_rateLimit_rateLimitError(t *testing.T) { 762 client, mux, _, teardown := setup() 763 defer teardown() 764 765 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 766 w.Header().Set(headerRateLimit, "60") 767 w.Header().Set(headerRateRemaining, "0") 768 w.Header().Set(headerRateReset, "1372700873") 769 w.Header().Set("Content-Type", "application/json; charset=utf-8") 770 w.WriteHeader(http.StatusForbidden) 771 fmt.Fprintln(w, `{ 772 "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.)", 773 "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits" 774 }`) 775 }) 776 777 req, _ := client.NewRequest("GET", ".", nil) 778 _, err := client.Do(context.Background(), req, nil) 779 780 if err == nil { 781 t.Error("Expected error to be returned.") 782 } 783 rateLimitErr, ok := err.(*RateLimitError) 784 if !ok { 785 t.Fatalf("Expected a *RateLimitError error; got %#v.", err) 786 } 787 if got, want := rateLimitErr.Rate.Limit, 60; got != want { 788 t.Errorf("rateLimitErr rate limit = %v, want %v", got, want) 789 } 790 if got, want := rateLimitErr.Rate.Remaining, 0; got != want { 791 t.Errorf("rateLimitErr rate remaining = %v, want %v", got, want) 792 } 793 reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC) 794 if rateLimitErr.Rate.Reset.UTC() != reset { 795 t.Errorf("rateLimitErr rate reset = %v, want %v", rateLimitErr.Rate.Reset.UTC(), reset) 796 } 797 } 798 799 // Ensure a network call is not made when it's known that API rate limit is still exceeded. 800 func TestDo_rateLimit_noNetworkCall(t *testing.T) { 801 client, mux, _, teardown := setup() 802 defer teardown() 803 804 reset := time.Now().UTC().Add(time.Minute).Round(time.Second) // Rate reset is a minute from now, with 1 second precision. 805 806 mux.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) { 807 w.Header().Set(headerRateLimit, "60") 808 w.Header().Set(headerRateRemaining, "0") 809 w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix())) 810 w.Header().Set("Content-Type", "application/json; charset=utf-8") 811 w.WriteHeader(http.StatusForbidden) 812 fmt.Fprintln(w, `{ 813 "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.)", 814 "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits" 815 }`) 816 }) 817 818 madeNetworkCall := false 819 mux.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) { 820 madeNetworkCall = true 821 }) 822 823 // First request is made, and it makes the client aware of rate reset time being in the future. 824 req, _ := client.NewRequest("GET", "first", nil) 825 client.Do(context.Background(), req, nil) 826 827 // Second request should not cause a network call to be made, since client can predict a rate limit error. 828 req, _ = client.NewRequest("GET", "second", nil) 829 _, err := client.Do(context.Background(), req, nil) 830 831 if madeNetworkCall { 832 t.Fatal("Network call was made, even though rate limit is known to still be exceeded.") 833 } 834 835 if err == nil { 836 t.Error("Expected error to be returned.") 837 } 838 rateLimitErr, ok := err.(*RateLimitError) 839 if !ok { 840 t.Fatalf("Expected a *RateLimitError error; got %#v.", err) 841 } 842 if got, want := rateLimitErr.Rate.Limit, 60; got != want { 843 t.Errorf("rateLimitErr rate limit = %v, want %v", got, want) 844 } 845 if got, want := rateLimitErr.Rate.Remaining, 0; got != want { 846 t.Errorf("rateLimitErr rate remaining = %v, want %v", got, want) 847 } 848 if rateLimitErr.Rate.Reset.UTC() != reset { 849 t.Errorf("rateLimitErr rate reset = %v, want %v", rateLimitErr.Rate.Reset.UTC(), reset) 850 } 851 } 852 853 // Ensure *AbuseRateLimitError is returned when the response indicates that 854 // the client has triggered an abuse detection mechanism. 855 func TestDo_rateLimit_abuseRateLimitError(t *testing.T) { 856 client, mux, _, teardown := setup() 857 defer teardown() 858 859 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 860 w.Header().Set("Content-Type", "application/json; charset=utf-8") 861 w.WriteHeader(http.StatusForbidden) 862 // When the abuse rate limit error is of the "temporarily blocked from content creation" type, 863 // there is no "Retry-After" header. 864 fmt.Fprintln(w, `{ 865 "message": "You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.", 866 "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits" 867 }`) 868 }) 869 870 req, _ := client.NewRequest("GET", ".", nil) 871 _, err := client.Do(context.Background(), req, nil) 872 873 if err == nil { 874 t.Error("Expected error to be returned.") 875 } 876 abuseRateLimitErr, ok := err.(*AbuseRateLimitError) 877 if !ok { 878 t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err) 879 } 880 if got, want := abuseRateLimitErr.RetryAfter, (*time.Duration)(nil); got != want { 881 t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want) 882 } 883 } 884 885 // Ensure *AbuseRateLimitError is returned when the response indicates that 886 // the client has triggered an abuse detection mechanism on GitHub Enterprise. 887 func TestDo_rateLimit_abuseRateLimitErrorEnterprise(t *testing.T) { 888 client, mux, _, teardown := setup() 889 defer teardown() 890 891 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 892 w.Header().Set("Content-Type", "application/json; charset=utf-8") 893 w.WriteHeader(http.StatusForbidden) 894 // When the abuse rate limit error is of the "temporarily blocked from content creation" type, 895 // there is no "Retry-After" header. 896 // This response returns a documentation url like the one returned for GitHub Enterprise, this 897 // url changes between versions but follows roughly the same format. 898 fmt.Fprintln(w, `{ 899 "message": "You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.", 900 "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits" 901 }`) 902 }) 903 904 req, _ := client.NewRequest("GET", ".", nil) 905 _, err := client.Do(context.Background(), req, nil) 906 907 if err == nil { 908 t.Error("Expected error to be returned.") 909 } 910 abuseRateLimitErr, ok := err.(*AbuseRateLimitError) 911 if !ok { 912 t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err) 913 } 914 if got, want := abuseRateLimitErr.RetryAfter, (*time.Duration)(nil); got != want { 915 t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want) 916 } 917 } 918 919 // Ensure *AbuseRateLimitError.RetryAfter is parsed correctly. 920 func TestDo_rateLimit_abuseRateLimitError_retryAfter(t *testing.T) { 921 client, mux, _, teardown := setup() 922 defer teardown() 923 924 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 925 w.Header().Set("Content-Type", "application/json; charset=utf-8") 926 w.Header().Set("Retry-After", "123") // Retry after value of 123 seconds. 927 w.WriteHeader(http.StatusForbidden) 928 fmt.Fprintln(w, `{ 929 "message": "You have triggered an abuse detection mechanism ...", 930 "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits" 931 }`) 932 }) 933 934 req, _ := client.NewRequest("GET", ".", nil) 935 _, err := client.Do(context.Background(), req, nil) 936 937 if err == nil { 938 t.Error("Expected error to be returned.") 939 } 940 abuseRateLimitErr, ok := err.(*AbuseRateLimitError) 941 if !ok { 942 t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err) 943 } 944 if abuseRateLimitErr.RetryAfter == nil { 945 t.Fatalf("abuseRateLimitErr RetryAfter is nil, expected not-nil") 946 } 947 if got, want := *abuseRateLimitErr.RetryAfter, 123*time.Second; got != want { 948 t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want) 949 } 950 } 951 952 func TestDo_noContent(t *testing.T) { 953 client, mux, _, teardown := setup() 954 defer teardown() 955 956 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 957 w.WriteHeader(http.StatusNoContent) 958 }) 959 960 var body json.RawMessage 961 962 req, _ := client.NewRequest("GET", ".", nil) 963 _, err := client.Do(context.Background(), req, &body) 964 if err != nil { 965 t.Fatalf("Do returned unexpected error: %v", err) 966 } 967 } 968 969 func TestSanitizeURL(t *testing.T) { 970 tests := []struct { 971 in, want string 972 }{ 973 {"/?a=b", "/?a=b"}, 974 {"/?a=b&client_secret=secret", "/?a=b&client_secret=REDACTED"}, 975 {"/?a=b&client_id=id&client_secret=secret", "/?a=b&client_id=id&client_secret=REDACTED"}, 976 } 977 978 for _, tt := range tests { 979 inURL, _ := url.Parse(tt.in) 980 want, _ := url.Parse(tt.want) 981 982 if got := sanitizeURL(inURL); !reflect.DeepEqual(got, want) { 983 t.Errorf("sanitizeURL(%v) returned %v, want %v", tt.in, got, want) 984 } 985 } 986 } 987 988 func TestCheckResponse(t *testing.T) { 989 res := &http.Response{ 990 Request: &http.Request{}, 991 StatusCode: http.StatusBadRequest, 992 Body: ioutil.NopCloser(strings.NewReader(`{"message":"m", 993 "errors": [{"resource": "r", "field": "f", "code": "c"}], 994 "block": {"reason": "dmca", "created_at": "2016-03-17T15:39:46Z"}}`)), 995 } 996 err := CheckResponse(res).(*ErrorResponse) 997 998 if err == nil { 999 t.Errorf("Expected error response.") 1000 } 1001 1002 want := &ErrorResponse{ 1003 Response: res, 1004 Message: "m", 1005 Errors: []Error{{Resource: "r", Field: "f", Code: "c"}}, 1006 Block: &struct { 1007 Reason string `json:"reason,omitempty"` 1008 CreatedAt *Timestamp `json:"created_at,omitempty"` 1009 }{ 1010 Reason: "dmca", 1011 CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)}, 1012 }, 1013 } 1014 if !reflect.DeepEqual(err, want) { 1015 t.Errorf("Error = %#v, want %#v", err, want) 1016 } 1017 } 1018 1019 func TestCheckResponse_RateLimit(t *testing.T) { 1020 res := &http.Response{ 1021 Request: &http.Request{}, 1022 StatusCode: http.StatusForbidden, 1023 Header: http.Header{}, 1024 Body: ioutil.NopCloser(strings.NewReader(`{"message":"m", 1025 "documentation_url": "url"}`)), 1026 } 1027 res.Header.Set(headerRateLimit, "60") 1028 res.Header.Set(headerRateRemaining, "0") 1029 res.Header.Set(headerRateReset, "243424") 1030 1031 err := CheckResponse(res).(*RateLimitError) 1032 1033 if err == nil { 1034 t.Errorf("Expected error response.") 1035 } 1036 1037 want := &RateLimitError{ 1038 Rate: parseRate(res), 1039 Response: res, 1040 Message: "m", 1041 } 1042 if !reflect.DeepEqual(err, want) { 1043 t.Errorf("Error = %#v, want %#v", err, want) 1044 } 1045 } 1046 1047 func TestCheckResponse_AbuseRateLimit(t *testing.T) { 1048 res := &http.Response{ 1049 Request: &http.Request{}, 1050 StatusCode: http.StatusForbidden, 1051 Body: ioutil.NopCloser(strings.NewReader(`{"message":"m", 1052 "documentation_url": "docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits"}`)), 1053 } 1054 err := CheckResponse(res).(*AbuseRateLimitError) 1055 1056 if err == nil { 1057 t.Errorf("Expected error response.") 1058 } 1059 1060 want := &AbuseRateLimitError{ 1061 Response: res, 1062 Message: "m", 1063 } 1064 if !reflect.DeepEqual(err, want) { 1065 t.Errorf("Error = %#v, want %#v", err, want) 1066 } 1067 } 1068 1069 // ensure that we properly handle API errors that do not contain a response body 1070 func TestCheckResponse_noBody(t *testing.T) { 1071 res := &http.Response{ 1072 Request: &http.Request{}, 1073 StatusCode: http.StatusBadRequest, 1074 Body: ioutil.NopCloser(strings.NewReader("")), 1075 } 1076 err := CheckResponse(res).(*ErrorResponse) 1077 1078 if err == nil { 1079 t.Errorf("Expected error response.") 1080 } 1081 1082 want := &ErrorResponse{ 1083 Response: res, 1084 } 1085 if !reflect.DeepEqual(err, want) { 1086 t.Errorf("Error = %#v, want %#v", err, want) 1087 } 1088 } 1089 1090 func TestCheckResponse_unexpectedErrorStructure(t *testing.T) { 1091 httpBody := `{"message":"m", "errors": ["error 1"]}` 1092 res := &http.Response{ 1093 Request: &http.Request{}, 1094 StatusCode: http.StatusBadRequest, 1095 Body: ioutil.NopCloser(strings.NewReader(httpBody)), 1096 } 1097 err := CheckResponse(res).(*ErrorResponse) 1098 1099 if err == nil { 1100 t.Errorf("Expected error response.") 1101 } 1102 1103 want := &ErrorResponse{ 1104 Response: res, 1105 Message: "m", 1106 Errors: []Error{{Message: "error 1"}}, 1107 } 1108 if !reflect.DeepEqual(err, want) { 1109 t.Errorf("Error = %#v, want %#v", err, want) 1110 } 1111 data, err2 := ioutil.ReadAll(err.Response.Body) 1112 if err2 != nil { 1113 t.Fatalf("failed to read response body: %v", err) 1114 } 1115 if got := string(data); got != httpBody { 1116 t.Errorf("ErrorResponse.Response.Body = %q, want %q", got, httpBody) 1117 } 1118 } 1119 1120 func TestParseBooleanResponse_true(t *testing.T) { 1121 result, err := parseBoolResponse(nil) 1122 if err != nil { 1123 t.Errorf("parseBoolResponse returned error: %+v", err) 1124 } 1125 1126 if want := true; result != want { 1127 t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want) 1128 } 1129 } 1130 1131 func TestParseBooleanResponse_false(t *testing.T) { 1132 v := &ErrorResponse{Response: &http.Response{StatusCode: http.StatusNotFound}} 1133 result, err := parseBoolResponse(v) 1134 if err != nil { 1135 t.Errorf("parseBoolResponse returned error: %+v", err) 1136 } 1137 1138 if want := false; result != want { 1139 t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want) 1140 } 1141 } 1142 1143 func TestParseBooleanResponse_error(t *testing.T) { 1144 v := &ErrorResponse{Response: &http.Response{StatusCode: http.StatusBadRequest}} 1145 result, err := parseBoolResponse(v) 1146 1147 if err == nil { 1148 t.Errorf("Expected error to be returned.") 1149 } 1150 1151 if want := false; result != want { 1152 t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want) 1153 } 1154 } 1155 1156 func TestErrorResponse_Error(t *testing.T) { 1157 res := &http.Response{Request: &http.Request{}} 1158 err := ErrorResponse{Message: "m", Response: res} 1159 if err.Error() == "" { 1160 t.Errorf("Expected non-empty ErrorResponse.Error()") 1161 } 1162 } 1163 1164 func TestError_Error(t *testing.T) { 1165 err := Error{} 1166 if err.Error() == "" { 1167 t.Errorf("Expected non-empty Error.Error()") 1168 } 1169 } 1170 1171 func TestRateLimits(t *testing.T) { 1172 client, mux, _, teardown := setup() 1173 defer teardown() 1174 1175 mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) { 1176 testMethod(t, r, "GET") 1177 fmt.Fprint(w, `{"resources":{ 1178 "core": {"limit":2,"remaining":1,"reset":1372700873}, 1179 "search": {"limit":3,"remaining":2,"reset":1372700874} 1180 }}`) 1181 }) 1182 1183 rate, _, err := client.RateLimits(context.Background()) 1184 if err != nil { 1185 t.Errorf("RateLimits returned error: %v", err) 1186 } 1187 1188 want := &RateLimits{ 1189 Core: &Rate{ 1190 Limit: 2, 1191 Remaining: 1, 1192 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC).Local()}, 1193 }, 1194 Search: &Rate{ 1195 Limit: 3, 1196 Remaining: 2, 1197 Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()}, 1198 }, 1199 } 1200 if !reflect.DeepEqual(rate, want) { 1201 t.Errorf("RateLimits returned %+v, want %+v", rate, want) 1202 } 1203 1204 if got, want := client.rateLimits[coreCategory], *want.Core; got != want { 1205 t.Errorf("client.rateLimits[coreCategory] is %+v, want %+v", got, want) 1206 } 1207 if got, want := client.rateLimits[searchCategory], *want.Search; got != want { 1208 t.Errorf("client.rateLimits[searchCategory] is %+v, want %+v", got, want) 1209 } 1210 } 1211 1212 func TestSetCredentialsAsHeaders(t *testing.T) { 1213 req := new(http.Request) 1214 id, secret := "id", "secret" 1215 modifiedRequest := setCredentialsAsHeaders(req, id, secret) 1216 1217 actualID, actualSecret, ok := modifiedRequest.BasicAuth() 1218 if !ok { 1219 t.Errorf("request does not contain basic credentials") 1220 } 1221 1222 if actualID != id { 1223 t.Errorf("id is %s, want %s", actualID, id) 1224 } 1225 1226 if actualSecret != secret { 1227 t.Errorf("secret is %s, want %s", actualSecret, secret) 1228 } 1229 } 1230 1231 func TestUnauthenticatedRateLimitedTransport(t *testing.T) { 1232 client, mux, _, teardown := setup() 1233 defer teardown() 1234 1235 clientID, clientSecret := "id", "secret" 1236 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 1237 id, secret, ok := r.BasicAuth() 1238 if !ok { 1239 t.Errorf("request does not contain basic auth credentials") 1240 } 1241 if id != clientID { 1242 t.Errorf("request contained basic auth username %q, want %q", id, clientID) 1243 } 1244 if secret != clientSecret { 1245 t.Errorf("request contained basic auth password %q, want %q", secret, clientSecret) 1246 } 1247 }) 1248 1249 tp := &UnauthenticatedRateLimitedTransport{ 1250 ClientID: clientID, 1251 ClientSecret: clientSecret, 1252 } 1253 unauthedClient := NewClient(tp.Client()) 1254 unauthedClient.BaseURL = client.BaseURL 1255 req, _ := unauthedClient.NewRequest("GET", ".", nil) 1256 unauthedClient.Do(context.Background(), req, nil) 1257 } 1258 1259 func TestUnauthenticatedRateLimitedTransport_missingFields(t *testing.T) { 1260 // missing ClientID 1261 tp := &UnauthenticatedRateLimitedTransport{ 1262 ClientSecret: "secret", 1263 } 1264 _, err := tp.RoundTrip(nil) 1265 if err == nil { 1266 t.Errorf("Expected error to be returned") 1267 } 1268 1269 // missing ClientSecret 1270 tp = &UnauthenticatedRateLimitedTransport{ 1271 ClientID: "id", 1272 } 1273 _, err = tp.RoundTrip(nil) 1274 if err == nil { 1275 t.Errorf("Expected error to be returned") 1276 } 1277 } 1278 1279 func TestUnauthenticatedRateLimitedTransport_transport(t *testing.T) { 1280 // default transport 1281 tp := &UnauthenticatedRateLimitedTransport{ 1282 ClientID: "id", 1283 ClientSecret: "secret", 1284 } 1285 if tp.transport() != http.DefaultTransport { 1286 t.Errorf("Expected http.DefaultTransport to be used.") 1287 } 1288 1289 // custom transport 1290 tp = &UnauthenticatedRateLimitedTransport{ 1291 ClientID: "id", 1292 ClientSecret: "secret", 1293 Transport: &http.Transport{}, 1294 } 1295 if tp.transport() == http.DefaultTransport { 1296 t.Errorf("Expected custom transport to be used.") 1297 } 1298 } 1299 1300 func TestBasicAuthTransport(t *testing.T) { 1301 client, mux, _, teardown := setup() 1302 defer teardown() 1303 1304 username, password, otp := "u", "p", "123456" 1305 1306 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 1307 u, p, ok := r.BasicAuth() 1308 if !ok { 1309 t.Errorf("request does not contain basic auth credentials") 1310 } 1311 if u != username { 1312 t.Errorf("request contained basic auth username %q, want %q", u, username) 1313 } 1314 if p != password { 1315 t.Errorf("request contained basic auth password %q, want %q", p, password) 1316 } 1317 if got, want := r.Header.Get(headerOTP), otp; got != want { 1318 t.Errorf("request contained OTP %q, want %q", got, want) 1319 } 1320 }) 1321 1322 tp := &BasicAuthTransport{ 1323 Username: username, 1324 Password: password, 1325 OTP: otp, 1326 } 1327 basicAuthClient := NewClient(tp.Client()) 1328 basicAuthClient.BaseURL = client.BaseURL 1329 req, _ := basicAuthClient.NewRequest("GET", ".", nil) 1330 basicAuthClient.Do(context.Background(), req, nil) 1331 } 1332 1333 func TestBasicAuthTransport_transport(t *testing.T) { 1334 // default transport 1335 tp := &BasicAuthTransport{} 1336 if tp.transport() != http.DefaultTransport { 1337 t.Errorf("Expected http.DefaultTransport to be used.") 1338 } 1339 1340 // custom transport 1341 tp = &BasicAuthTransport{ 1342 Transport: &http.Transport{}, 1343 } 1344 if tp.transport() == http.DefaultTransport { 1345 t.Errorf("Expected custom transport to be used.") 1346 } 1347 } 1348 1349 func TestFormatRateReset(t *testing.T) { 1350 d := 120*time.Minute + 12*time.Second 1351 got := formatRateReset(d) 1352 want := "[rate reset in 120m12s]" 1353 if got != want { 1354 t.Errorf("Format is wrong. got: %v, want: %v", got, want) 1355 } 1356 1357 d = 14*time.Minute + 2*time.Second 1358 got = formatRateReset(d) 1359 want = "[rate reset in 14m02s]" 1360 if got != want { 1361 t.Errorf("Format is wrong. got: %v, want: %v", got, want) 1362 } 1363 1364 d = 2*time.Minute + 2*time.Second 1365 got = formatRateReset(d) 1366 want = "[rate reset in 2m02s]" 1367 if got != want { 1368 t.Errorf("Format is wrong. got: %v, want: %v", got, want) 1369 } 1370 1371 d = 12 * time.Second 1372 got = formatRateReset(d) 1373 want = "[rate reset in 12s]" 1374 if got != want { 1375 t.Errorf("Format is wrong. got: %v, want: %v", got, want) 1376 } 1377 1378 d = -1 * (2*time.Hour + 2*time.Second) 1379 got = formatRateReset(d) 1380 want = "[rate limit was reset 120m02s ago]" 1381 if got != want { 1382 t.Errorf("Format is wrong. got: %v, want: %v", got, want) 1383 } 1384 } 1385 1386 func TestNestedStructAccessorNoPanic(t *testing.T) { 1387 issue := &Issue{User: nil} 1388 got := issue.GetUser().GetPlan().GetName() 1389 want := "" 1390 if got != want { 1391 t.Errorf("Issues.Get.GetUser().GetPlan().GetName() returned %+v, want %+v", got, want) 1392 } 1393 }