github.com/google/go-github/v42@v42.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  	"github.com/google/go-cmp/cmp"
    25  )
    26  
    27  const (
    28  	// baseURLPath is a non-empty Client.BaseURL path to use during tests,
    29  	// to ensure relative URLs are used for all endpoints. See issue #752.
    30  	baseURLPath = "/api-v3"
    31  )
    32  
    33  // setup sets up a test HTTP server along with a github.Client that is
    34  // configured to talk to that test server. Tests should register handlers on
    35  // mux which provide mock responses for the API method being tested.
    36  func setup() (client *Client, mux *http.ServeMux, serverURL string, teardown func()) {
    37  	// mux is the HTTP request multiplexer used with the test server.
    38  	mux = http.NewServeMux()
    39  
    40  	// We want to ensure that tests catch mistakes where the endpoint URL is
    41  	// specified as absolute rather than relative. It only makes a difference
    42  	// when there's a non-empty base URL path. So, use that. See issue #752.
    43  	apiHandler := http.NewServeMux()
    44  	apiHandler.Handle(baseURLPath+"/", http.StripPrefix(baseURLPath, mux))
    45  	apiHandler.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
    46  		fmt.Fprintln(os.Stderr, "FAIL: Client.BaseURL path prefix is not preserved in the request URL:")
    47  		fmt.Fprintln(os.Stderr)
    48  		fmt.Fprintln(os.Stderr, "\t"+req.URL.String())
    49  		fmt.Fprintln(os.Stderr)
    50  		fmt.Fprintln(os.Stderr, "\tDid you accidentally use an absolute endpoint URL rather than relative?")
    51  		fmt.Fprintln(os.Stderr, "\tSee https://github.com/google/go-github/issues/752 for information.")
    52  		http.Error(w, "Client.BaseURL path prefix is not preserved in the request URL.", http.StatusInternalServerError)
    53  	})
    54  
    55  	// server is a test HTTP server used to provide mock API responses.
    56  	server := httptest.NewServer(apiHandler)
    57  
    58  	// client is the GitHub client being tested and is
    59  	// configured to use test server.
    60  	client = NewClient(nil)
    61  	url, _ := url.Parse(server.URL + baseURLPath + "/")
    62  	client.BaseURL = url
    63  	client.UploadURL = url
    64  
    65  	return client, mux, server.URL, server.Close
    66  }
    67  
    68  // openTestFile creates a new file with the given name and content for testing.
    69  // In order to ensure the exact file name, this function will create a new temp
    70  // directory, and create the file in that directory. It is the caller's
    71  // responsibility to remove the directory and its contents when no longer needed.
    72  func openTestFile(name, content string) (file *os.File, dir string, err error) {
    73  	dir, err = ioutil.TempDir("", "go-github")
    74  	if err != nil {
    75  		return nil, dir, err
    76  	}
    77  
    78  	file, err = os.OpenFile(path.Join(dir, name), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
    79  	if err != nil {
    80  		return nil, dir, err
    81  	}
    82  
    83  	fmt.Fprint(file, content)
    84  
    85  	// close and re-open the file to keep file.Stat() happy
    86  	file.Close()
    87  	file, err = os.Open(file.Name())
    88  	if err != nil {
    89  		return nil, dir, err
    90  	}
    91  
    92  	return file, dir, err
    93  }
    94  
    95  func testMethod(t *testing.T, r *http.Request, want string) {
    96  	t.Helper()
    97  	if got := r.Method; got != want {
    98  		t.Errorf("Request method: %v, want %v", got, want)
    99  	}
   100  }
   101  
   102  type values map[string]string
   103  
   104  func testFormValues(t *testing.T, r *http.Request, values values) {
   105  	t.Helper()
   106  	want := url.Values{}
   107  	for k, v := range values {
   108  		want.Set(k, v)
   109  	}
   110  
   111  	r.ParseForm()
   112  	if got := r.Form; !cmp.Equal(got, want) {
   113  		t.Errorf("Request parameters: %v, want %v", got, want)
   114  	}
   115  }
   116  
   117  func testHeader(t *testing.T, r *http.Request, header string, want string) {
   118  	t.Helper()
   119  	if got := r.Header.Get(header); got != want {
   120  		t.Errorf("Header.Get(%q) returned %q, want %q", header, got, want)
   121  	}
   122  }
   123  
   124  func testURLParseError(t *testing.T, err error) {
   125  	t.Helper()
   126  	if err == nil {
   127  		t.Errorf("Expected error to be returned")
   128  	}
   129  	if err, ok := err.(*url.Error); !ok || err.Op != "parse" {
   130  		t.Errorf("Expected URL parse error, got %+v", err)
   131  	}
   132  }
   133  
   134  func testBody(t *testing.T, r *http.Request, want string) {
   135  	t.Helper()
   136  	b, err := ioutil.ReadAll(r.Body)
   137  	if err != nil {
   138  		t.Errorf("Error reading request body: %v", err)
   139  	}
   140  	if got := string(b); got != want {
   141  		t.Errorf("request Body is %s, want %s", got, want)
   142  	}
   143  }
   144  
   145  // Test whether the marshaling of v produces JSON that corresponds
   146  // to the want string.
   147  func testJSONMarshal(t *testing.T, v interface{}, want string) {
   148  	t.Helper()
   149  	// Unmarshal the wanted JSON, to verify its correctness, and marshal it back
   150  	// to sort the keys.
   151  	u := reflect.New(reflect.TypeOf(v)).Interface()
   152  	if err := json.Unmarshal([]byte(want), &u); err != nil {
   153  		t.Errorf("Unable to unmarshal JSON for %v: %v", want, err)
   154  	}
   155  	w, err := json.Marshal(u)
   156  	if err != nil {
   157  		t.Errorf("Unable to marshal JSON for %#v", u)
   158  	}
   159  
   160  	// Marshal the target value.
   161  	j, err := json.Marshal(v)
   162  	if err != nil {
   163  		t.Errorf("Unable to marshal JSON for %#v", v)
   164  	}
   165  
   166  	if string(w) != string(j) {
   167  		t.Errorf("json.Marshal(%q) returned %s, want %s", v, j, w)
   168  	}
   169  }
   170  
   171  // Test how bad options are handled. Method f under test should
   172  // return an error.
   173  func testBadOptions(t *testing.T, methodName string, f func() error) {
   174  	t.Helper()
   175  	if methodName == "" {
   176  		t.Error("testBadOptions: must supply method methodName")
   177  	}
   178  	if err := f(); err == nil {
   179  		t.Errorf("bad options %v err = nil, want error", methodName)
   180  	}
   181  }
   182  
   183  // Test function under NewRequest failure and then s.client.Do failure.
   184  // Method f should be a regular call that would normally succeed, but
   185  // should return an error when NewRequest or s.client.Do fails.
   186  func testNewRequestAndDoFailure(t *testing.T, methodName string, client *Client, f func() (*Response, error)) {
   187  	t.Helper()
   188  	if methodName == "" {
   189  		t.Error("testNewRequestAndDoFailure: must supply method methodName")
   190  	}
   191  
   192  	client.BaseURL.Path = ""
   193  	resp, err := f()
   194  	if resp != nil {
   195  		t.Errorf("client.BaseURL.Path='' %v resp = %#v, want nil", methodName, resp)
   196  	}
   197  	if err == nil {
   198  		t.Errorf("client.BaseURL.Path='' %v err = nil, want error", methodName)
   199  	}
   200  
   201  	client.BaseURL.Path = "/api-v3/"
   202  	client.rateLimits[0].Reset.Time = time.Now().Add(10 * time.Minute)
   203  	resp, err = f()
   204  	if bypass := resp.Request.Context().Value(bypassRateLimitCheck); bypass != nil {
   205  		return
   206  	}
   207  	if want := http.StatusForbidden; resp == nil || resp.Response.StatusCode != want {
   208  		if resp != nil {
   209  			t.Errorf("rate.Reset.Time > now %v resp = %#v, want StatusCode=%v", methodName, resp.Response, want)
   210  		} else {
   211  			t.Errorf("rate.Reset.Time > now %v resp = nil, want StatusCode=%v", methodName, want)
   212  		}
   213  	}
   214  	if err == nil {
   215  		t.Errorf("rate.Reset.Time > now %v err = nil, want error", methodName)
   216  	}
   217  }
   218  
   219  // Test that all error response types contain the status code.
   220  func testErrorResponseForStatusCode(t *testing.T, code int) {
   221  	t.Helper()
   222  	client, mux, _, teardown := setup()
   223  	defer teardown()
   224  
   225  	mux.HandleFunc("/repos/o/r/hooks", func(w http.ResponseWriter, r *http.Request) {
   226  		testMethod(t, r, "GET")
   227  		w.WriteHeader(code)
   228  	})
   229  
   230  	ctx := context.Background()
   231  	_, _, err := client.Repositories.ListHooks(ctx, "o", "r", nil)
   232  
   233  	switch e := err.(type) {
   234  	case *ErrorResponse:
   235  	case *RateLimitError:
   236  	case *AbuseRateLimitError:
   237  		if code != e.Response.StatusCode {
   238  			t.Error("Error response does not contain status code")
   239  		}
   240  	default:
   241  		t.Error("Unknown error response type")
   242  	}
   243  }
   244  
   245  func TestNewClient(t *testing.T) {
   246  	c := NewClient(nil)
   247  
   248  	if got, want := c.BaseURL.String(), defaultBaseURL; got != want {
   249  		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
   250  	}
   251  	if got, want := c.UserAgent, userAgent; got != want {
   252  		t.Errorf("NewClient UserAgent is %v, want %v", got, want)
   253  	}
   254  
   255  	c2 := NewClient(nil)
   256  	if c.client == c2.client {
   257  		t.Error("NewClient returned same http.Clients, but they should differ")
   258  	}
   259  }
   260  
   261  func TestClient(t *testing.T) {
   262  	c := NewClient(nil)
   263  	c2 := c.Client()
   264  	if c.client == c2 {
   265  		t.Error("Client returned same http.Client, but should be different")
   266  	}
   267  }
   268  
   269  func TestNewEnterpriseClient(t *testing.T) {
   270  	baseURL := "https://custom-url/api/v3/"
   271  	uploadURL := "https://custom-upload-url/api/uploads/"
   272  	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
   273  	if err != nil {
   274  		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
   275  	}
   276  
   277  	if got, want := c.BaseURL.String(), baseURL; got != want {
   278  		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
   279  	}
   280  	if got, want := c.UploadURL.String(), uploadURL; got != want {
   281  		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
   282  	}
   283  }
   284  
   285  func TestNewEnterpriseClient_addsTrailingSlashToURLs(t *testing.T) {
   286  	baseURL := "https://custom-url/api/v3"
   287  	uploadURL := "https://custom-upload-url/api/uploads"
   288  	formattedBaseURL := baseURL + "/"
   289  	formattedUploadURL := uploadURL + "/"
   290  
   291  	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
   292  	if err != nil {
   293  		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
   294  	}
   295  
   296  	if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
   297  		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
   298  	}
   299  	if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
   300  		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
   301  	}
   302  }
   303  
   304  func TestNewEnterpriseClient_addsEnterpriseSuffixToURLs(t *testing.T) {
   305  	baseURL := "https://custom-url/"
   306  	uploadURL := "https://custom-upload-url/"
   307  	formattedBaseURL := baseURL + "api/v3/"
   308  	formattedUploadURL := uploadURL + "api/uploads/"
   309  
   310  	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
   311  	if err != nil {
   312  		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
   313  	}
   314  
   315  	if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
   316  		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
   317  	}
   318  	if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
   319  		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
   320  	}
   321  }
   322  
   323  func TestNewEnterpriseClient_addsEnterpriseSuffixAndTrailingSlashToURLs(t *testing.T) {
   324  	baseURL := "https://custom-url"
   325  	uploadURL := "https://custom-upload-url"
   326  	formattedBaseURL := baseURL + "/api/v3/"
   327  	formattedUploadURL := uploadURL + "/api/uploads/"
   328  
   329  	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
   330  	if err != nil {
   331  		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
   332  	}
   333  
   334  	if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
   335  		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
   336  	}
   337  	if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
   338  		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
   339  	}
   340  }
   341  
   342  func TestNewEnterpriseClient_badBaseURL(t *testing.T) {
   343  	baseURL := "bogus\nbase\nURL"
   344  	uploadURL := "https://custom-upload-url/api/uploads/"
   345  	if _, err := NewEnterpriseClient(baseURL, uploadURL, nil); err == nil {
   346  		t.Fatal("NewEnterpriseClient returned nil, expected error")
   347  	}
   348  }
   349  
   350  func TestNewEnterpriseClient_badUploadURL(t *testing.T) {
   351  	baseURL := "https://custom-url/api/v3/"
   352  	uploadURL := "bogus\nupload\nURL"
   353  	if _, err := NewEnterpriseClient(baseURL, uploadURL, nil); err == nil {
   354  		t.Fatal("NewEnterpriseClient returned nil, expected error")
   355  	}
   356  }
   357  
   358  func TestNewEnterpriseClient_URLHasExistingAPIPrefix_AddTrailingSlash(t *testing.T) {
   359  	baseURL := "https://api.custom-url"
   360  	uploadURL := "https://api.custom-upload-url"
   361  	formattedBaseURL := baseURL + "/"
   362  	formattedUploadURL := uploadURL + "/"
   363  
   364  	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
   365  	if err != nil {
   366  		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
   367  	}
   368  
   369  	if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
   370  		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
   371  	}
   372  	if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
   373  		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
   374  	}
   375  }
   376  
   377  func TestNewEnterpriseClient_URLHasExistingAPIPrefixAndTrailingSlash(t *testing.T) {
   378  	baseURL := "https://api.custom-url/"
   379  	uploadURL := "https://api.custom-upload-url/"
   380  
   381  	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
   382  	if err != nil {
   383  		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
   384  	}
   385  
   386  	if got, want := c.BaseURL.String(), baseURL; got != want {
   387  		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
   388  	}
   389  	if got, want := c.UploadURL.String(), uploadURL; got != want {
   390  		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
   391  	}
   392  }
   393  
   394  func TestNewEnterpriseClient_URLHasAPISubdomain_AddTrailingSlash(t *testing.T) {
   395  	baseURL := "https://catalog.api.custom-url"
   396  	uploadURL := "https://catalog.api.custom-upload-url"
   397  	formattedBaseURL := baseURL + "/"
   398  	formattedUploadURL := uploadURL + "/"
   399  
   400  	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
   401  	if err != nil {
   402  		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
   403  	}
   404  
   405  	if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
   406  		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
   407  	}
   408  	if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
   409  		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
   410  	}
   411  }
   412  
   413  func TestNewEnterpriseClient_URLHasAPISubdomainAndTrailingSlash(t *testing.T) {
   414  	baseURL := "https://catalog.api.custom-url/"
   415  	uploadURL := "https://catalog.api.custom-upload-url/"
   416  
   417  	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
   418  	if err != nil {
   419  		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
   420  	}
   421  
   422  	if got, want := c.BaseURL.String(), baseURL; got != want {
   423  		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
   424  	}
   425  	if got, want := c.UploadURL.String(), uploadURL; got != want {
   426  		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
   427  	}
   428  }
   429  
   430  func TestNewEnterpriseClient_URLIsNotAProperAPISubdomain_addsEnterpriseSuffixAndSlash(t *testing.T) {
   431  	baseURL := "https://cloud-api.custom-url"
   432  	uploadURL := "https://cloud-api.custom-upload-url"
   433  	formattedBaseURL := baseURL + "/api/v3/"
   434  	formattedUploadURL := uploadURL + "/api/uploads/"
   435  
   436  	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
   437  	if err != nil {
   438  		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
   439  	}
   440  
   441  	if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
   442  		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
   443  	}
   444  	if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
   445  		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
   446  	}
   447  }
   448  
   449  func TestNewEnterpriseClient_URLIsNotAProperAPISubdomain_addsEnterpriseSuffix(t *testing.T) {
   450  	baseURL := "https://cloud-api.custom-url/"
   451  	uploadURL := "https://cloud-api.custom-upload-url/"
   452  	formattedBaseURL := baseURL + "api/v3/"
   453  	formattedUploadURL := uploadURL + "api/uploads/"
   454  
   455  	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
   456  	if err != nil {
   457  		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
   458  	}
   459  
   460  	if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
   461  		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
   462  	}
   463  	if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
   464  		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
   465  	}
   466  }
   467  
   468  // Ensure that length of Client.rateLimits is the same as number of fields in RateLimits struct.
   469  func TestClient_rateLimits(t *testing.T) {
   470  	if got, want := len(Client{}.rateLimits), reflect.TypeOf(RateLimits{}).NumField(); got != want {
   471  		t.Errorf("len(Client{}.rateLimits) is %v, want %v", got, want)
   472  	}
   473  }
   474  
   475  func TestRateLimits_String(t *testing.T) {
   476  	v := RateLimits{
   477  		Core:   &Rate{},
   478  		Search: &Rate{},
   479  	}
   480  	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}}}`
   481  	if got := v.String(); got != want {
   482  		t.Errorf("RateLimits.String = %v, want %v", got, want)
   483  	}
   484  }
   485  
   486  func TestNewRequest(t *testing.T) {
   487  	c := NewClient(nil)
   488  
   489  	inURL, outURL := "/foo", defaultBaseURL+"foo"
   490  	inBody, outBody := &User{Login: String("l")}, `{"login":"l"}`+"\n"
   491  	req, _ := c.NewRequest("GET", inURL, inBody)
   492  
   493  	// test that relative URL was expanded
   494  	if got, want := req.URL.String(), outURL; got != want {
   495  		t.Errorf("NewRequest(%q) URL is %v, want %v", inURL, got, want)
   496  	}
   497  
   498  	// test that body was JSON encoded
   499  	body, _ := ioutil.ReadAll(req.Body)
   500  	if got, want := string(body), outBody; got != want {
   501  		t.Errorf("NewRequest(%q) Body is %v, want %v", inBody, got, want)
   502  	}
   503  
   504  	// test that default user-agent is attached to the request
   505  	if got, want := req.Header.Get("User-Agent"), c.UserAgent; got != want {
   506  		t.Errorf("NewRequest() User-Agent is %v, want %v", got, want)
   507  	}
   508  }
   509  
   510  func TestNewRequest_invalidJSON(t *testing.T) {
   511  	c := NewClient(nil)
   512  
   513  	type T struct {
   514  		A map[interface{}]interface{}
   515  	}
   516  	_, err := c.NewRequest("GET", ".", &T{})
   517  
   518  	if err == nil {
   519  		t.Error("Expected error to be returned.")
   520  	}
   521  	if err, ok := err.(*json.UnsupportedTypeError); !ok {
   522  		t.Errorf("Expected a JSON error; got %#v.", err)
   523  	}
   524  }
   525  
   526  func TestNewRequest_badURL(t *testing.T) {
   527  	c := NewClient(nil)
   528  	_, err := c.NewRequest("GET", ":", nil)
   529  	testURLParseError(t, err)
   530  }
   531  
   532  func TestNewRequest_badMethod(t *testing.T) {
   533  	c := NewClient(nil)
   534  	if _, err := c.NewRequest("BOGUS\nMETHOD", ".", nil); err == nil {
   535  		t.Fatal("NewRequest returned nil; expected error")
   536  	}
   537  }
   538  
   539  // ensure that no User-Agent header is set if the client's UserAgent is empty.
   540  // This caused a problem with Google's internal http client.
   541  func TestNewRequest_emptyUserAgent(t *testing.T) {
   542  	c := NewClient(nil)
   543  	c.UserAgent = ""
   544  	req, err := c.NewRequest("GET", ".", nil)
   545  	if err != nil {
   546  		t.Fatalf("NewRequest returned unexpected error: %v", err)
   547  	}
   548  	if _, ok := req.Header["User-Agent"]; ok {
   549  		t.Fatal("constructed request contains unexpected User-Agent header")
   550  	}
   551  }
   552  
   553  // If a nil body is passed to github.NewRequest, make sure that nil is also
   554  // passed to http.NewRequest. In most cases, passing an io.Reader that returns
   555  // no content is fine, since there is no difference between an HTTP request
   556  // body that is an empty string versus one that is not set at all. However in
   557  // certain cases, intermediate systems may treat these differently resulting in
   558  // subtle errors.
   559  func TestNewRequest_emptyBody(t *testing.T) {
   560  	c := NewClient(nil)
   561  	req, err := c.NewRequest("GET", ".", nil)
   562  	if err != nil {
   563  		t.Fatalf("NewRequest returned unexpected error: %v", err)
   564  	}
   565  	if req.Body != nil {
   566  		t.Fatalf("constructed request contains a non-nil Body")
   567  	}
   568  }
   569  
   570  func TestNewRequest_errorForNoTrailingSlash(t *testing.T) {
   571  	tests := []struct {
   572  		rawurl    string
   573  		wantError bool
   574  	}{
   575  		{rawurl: "https://example.com/api/v3", wantError: true},
   576  		{rawurl: "https://example.com/api/v3/", wantError: false},
   577  	}
   578  	c := NewClient(nil)
   579  	for _, test := range tests {
   580  		u, err := url.Parse(test.rawurl)
   581  		if err != nil {
   582  			t.Fatalf("url.Parse returned unexpected error: %v.", err)
   583  		}
   584  		c.BaseURL = u
   585  		if _, err := c.NewRequest(http.MethodGet, "test", nil); test.wantError && err == nil {
   586  			t.Fatalf("Expected error to be returned.")
   587  		} else if !test.wantError && err != nil {
   588  			t.Fatalf("NewRequest returned unexpected error: %v.", err)
   589  		}
   590  	}
   591  }
   592  
   593  func TestNewUploadRequest_badURL(t *testing.T) {
   594  	c := NewClient(nil)
   595  	_, err := c.NewUploadRequest(":", nil, 0, "")
   596  	testURLParseError(t, err)
   597  }
   598  
   599  func TestNewUploadRequest_errorForNoTrailingSlash(t *testing.T) {
   600  	tests := []struct {
   601  		rawurl    string
   602  		wantError bool
   603  	}{
   604  		{rawurl: "https://example.com/api/uploads", wantError: true},
   605  		{rawurl: "https://example.com/api/uploads/", wantError: false},
   606  	}
   607  	c := NewClient(nil)
   608  	for _, test := range tests {
   609  		u, err := url.Parse(test.rawurl)
   610  		if err != nil {
   611  			t.Fatalf("url.Parse returned unexpected error: %v.", err)
   612  		}
   613  		c.UploadURL = u
   614  		if _, err = c.NewUploadRequest("test", nil, 0, ""); test.wantError && err == nil {
   615  			t.Fatalf("Expected error to be returned.")
   616  		} else if !test.wantError && err != nil {
   617  			t.Fatalf("NewUploadRequest returned unexpected error: %v.", err)
   618  		}
   619  	}
   620  }
   621  
   622  func TestResponse_populatePageValues(t *testing.T) {
   623  	r := http.Response{
   624  		Header: http.Header{
   625  			"Link": {`<https://api.github.com/?page=1>; rel="first",` +
   626  				` <https://api.github.com/?page=2>; rel="prev",` +
   627  				` <https://api.github.com/?page=4>; rel="next",` +
   628  				` <https://api.github.com/?page=5>; rel="last"`,
   629  			},
   630  		},
   631  	}
   632  
   633  	response := newResponse(&r)
   634  	if got, want := response.FirstPage, 1; got != want {
   635  		t.Errorf("response.FirstPage: %v, want %v", got, want)
   636  	}
   637  	if got, want := response.PrevPage, 2; want != got {
   638  		t.Errorf("response.PrevPage: %v, want %v", got, want)
   639  	}
   640  	if got, want := response.NextPage, 4; want != got {
   641  		t.Errorf("response.NextPage: %v, want %v", got, want)
   642  	}
   643  	if got, want := response.LastPage, 5; want != got {
   644  		t.Errorf("response.LastPage: %v, want %v", got, want)
   645  	}
   646  	if got, want := response.NextPageToken, ""; want != got {
   647  		t.Errorf("response.NextPageToken: %v, want %v", got, want)
   648  	}
   649  }
   650  
   651  func TestResponse_populateSinceValues(t *testing.T) {
   652  	r := http.Response{
   653  		Header: http.Header{
   654  			"Link": {`<https://api.github.com/?since=1>; rel="first",` +
   655  				` <https://api.github.com/?since=2>; rel="prev",` +
   656  				` <https://api.github.com/?since=4>; rel="next",` +
   657  				` <https://api.github.com/?since=5>; rel="last"`,
   658  			},
   659  		},
   660  	}
   661  
   662  	response := newResponse(&r)
   663  	if got, want := response.FirstPage, 1; got != want {
   664  		t.Errorf("response.FirstPage: %v, want %v", got, want)
   665  	}
   666  	if got, want := response.PrevPage, 2; want != got {
   667  		t.Errorf("response.PrevPage: %v, want %v", got, want)
   668  	}
   669  	if got, want := response.NextPage, 4; want != got {
   670  		t.Errorf("response.NextPage: %v, want %v", got, want)
   671  	}
   672  	if got, want := response.LastPage, 5; want != got {
   673  		t.Errorf("response.LastPage: %v, want %v", got, want)
   674  	}
   675  	if got, want := response.NextPageToken, ""; want != got {
   676  		t.Errorf("response.NextPageToken: %v, want %v", got, want)
   677  	}
   678  }
   679  
   680  func TestResponse_SinceWithPage(t *testing.T) {
   681  	r := http.Response{
   682  		Header: http.Header{
   683  			"Link": {`<https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=1>; rel="first",` +
   684  				` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=2>; rel="prev",` +
   685  				` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=4>; rel="next",` +
   686  				` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=5>; rel="last"`,
   687  			},
   688  		},
   689  	}
   690  
   691  	response := newResponse(&r)
   692  	if got, want := response.FirstPage, 1; got != want {
   693  		t.Errorf("response.FirstPage: %v, want %v", got, want)
   694  	}
   695  	if got, want := response.PrevPage, 2; want != got {
   696  		t.Errorf("response.PrevPage: %v, want %v", got, want)
   697  	}
   698  	if got, want := response.NextPage, 4; want != got {
   699  		t.Errorf("response.NextPage: %v, want %v", got, want)
   700  	}
   701  	if got, want := response.LastPage, 5; want != got {
   702  		t.Errorf("response.LastPage: %v, want %v", got, want)
   703  	}
   704  	if got, want := response.NextPageToken, ""; want != got {
   705  		t.Errorf("response.NextPageToken: %v, want %v", got, want)
   706  	}
   707  }
   708  
   709  func TestResponse_cursorPagination(t *testing.T) {
   710  	r := http.Response{
   711  		Header: http.Header{
   712  			"Status": {"200 OK"},
   713  			"Link":   {`<https://api.github.com/resource?per_page=2&page=url-encoded-next-page-token>; rel="next"`},
   714  		},
   715  	}
   716  
   717  	response := newResponse(&r)
   718  	if got, want := response.FirstPage, 0; got != want {
   719  		t.Errorf("response.FirstPage: %v, want %v", got, want)
   720  	}
   721  	if got, want := response.PrevPage, 0; want != got {
   722  		t.Errorf("response.PrevPage: %v, want %v", got, want)
   723  	}
   724  	if got, want := response.NextPage, 0; want != got {
   725  		t.Errorf("response.NextPage: %v, want %v", got, want)
   726  	}
   727  	if got, want := response.LastPage, 0; want != got {
   728  		t.Errorf("response.LastPage: %v, want %v", got, want)
   729  	}
   730  	if got, want := response.NextPageToken, "url-encoded-next-page-token"; want != got {
   731  		t.Errorf("response.NextPageToken: %v, want %v", got, want)
   732  	}
   733  
   734  	// cursor-based pagination with "cursor" param
   735  	r = http.Response{
   736  		Header: http.Header{
   737  			"Link": {
   738  				`<https://api.github.com/?cursor=v1_12345678>; rel="next"`,
   739  			},
   740  		},
   741  	}
   742  
   743  	response = newResponse(&r)
   744  	if got, want := response.Cursor, "v1_12345678"; got != want {
   745  		t.Errorf("response.Cursor: %v, want %v", got, want)
   746  	}
   747  }
   748  
   749  func TestResponse_beforeAfterPagination(t *testing.T) {
   750  	r := http.Response{
   751  		Header: http.Header{
   752  			"Link": {`<https://api.github.com/?after=a1b2c3&before=>; rel="next",` +
   753  				` <https://api.github.com/?after=&before=>; rel="first",` +
   754  				` <https://api.github.com/?after=&before=d4e5f6>; rel="prev",`,
   755  			},
   756  		},
   757  	}
   758  
   759  	response := newResponse(&r)
   760  	if got, want := response.Before, "d4e5f6"; got != want {
   761  		t.Errorf("response.Before: %v, want %v", got, want)
   762  	}
   763  	if got, want := response.After, "a1b2c3"; got != want {
   764  		t.Errorf("response.After: %v, want %v", got, want)
   765  	}
   766  	if got, want := response.FirstPage, 0; got != want {
   767  		t.Errorf("response.FirstPage: %v, want %v", got, want)
   768  	}
   769  	if got, want := response.PrevPage, 0; want != got {
   770  		t.Errorf("response.PrevPage: %v, want %v", got, want)
   771  	}
   772  	if got, want := response.NextPage, 0; want != got {
   773  		t.Errorf("response.NextPage: %v, want %v", got, want)
   774  	}
   775  	if got, want := response.LastPage, 0; want != got {
   776  		t.Errorf("response.LastPage: %v, want %v", got, want)
   777  	}
   778  	if got, want := response.NextPageToken, ""; want != got {
   779  		t.Errorf("response.NextPageToken: %v, want %v", got, want)
   780  	}
   781  }
   782  
   783  func TestResponse_populatePageValues_invalid(t *testing.T) {
   784  	r := http.Response{
   785  		Header: http.Header{
   786  			"Link": {`<https://api.github.com/?page=1>,` +
   787  				`<https://api.github.com/?page=abc>; rel="first",` +
   788  				`https://api.github.com/?page=2; rel="prev",` +
   789  				`<https://api.github.com/>; rel="next",` +
   790  				`<https://api.github.com/?page=>; rel="last"`,
   791  			},
   792  		},
   793  	}
   794  
   795  	response := newResponse(&r)
   796  	if got, want := response.FirstPage, 0; got != want {
   797  		t.Errorf("response.FirstPage: %v, want %v", got, want)
   798  	}
   799  	if got, want := response.PrevPage, 0; got != want {
   800  		t.Errorf("response.PrevPage: %v, want %v", got, want)
   801  	}
   802  	if got, want := response.NextPage, 0; got != want {
   803  		t.Errorf("response.NextPage: %v, want %v", got, want)
   804  	}
   805  	if got, want := response.LastPage, 0; got != want {
   806  		t.Errorf("response.LastPage: %v, want %v", got, want)
   807  	}
   808  
   809  	// more invalid URLs
   810  	r = http.Response{
   811  		Header: http.Header{
   812  			"Link": {`<https://api.github.com/%?page=2>; rel="first"`},
   813  		},
   814  	}
   815  
   816  	response = newResponse(&r)
   817  	if got, want := response.FirstPage, 0; got != want {
   818  		t.Errorf("response.FirstPage: %v, want %v", got, want)
   819  	}
   820  }
   821  
   822  func TestResponse_populateSinceValues_invalid(t *testing.T) {
   823  	r := http.Response{
   824  		Header: http.Header{
   825  			"Link": {`<https://api.github.com/?since=1>,` +
   826  				`<https://api.github.com/?since=abc>; rel="first",` +
   827  				`https://api.github.com/?since=2; rel="prev",` +
   828  				`<https://api.github.com/>; rel="next",` +
   829  				`<https://api.github.com/?since=>; rel="last"`,
   830  			},
   831  		},
   832  	}
   833  
   834  	response := newResponse(&r)
   835  	if got, want := response.FirstPage, 0; got != want {
   836  		t.Errorf("response.FirstPage: %v, want %v", got, want)
   837  	}
   838  	if got, want := response.PrevPage, 0; got != want {
   839  		t.Errorf("response.PrevPage: %v, want %v", got, want)
   840  	}
   841  	if got, want := response.NextPage, 0; got != want {
   842  		t.Errorf("response.NextPage: %v, want %v", got, want)
   843  	}
   844  	if got, want := response.LastPage, 0; got != want {
   845  		t.Errorf("response.LastPage: %v, want %v", got, want)
   846  	}
   847  
   848  	// more invalid URLs
   849  	r = http.Response{
   850  		Header: http.Header{
   851  			"Link": {`<https://api.github.com/%?since=2>; rel="first"`},
   852  		},
   853  	}
   854  
   855  	response = newResponse(&r)
   856  	if got, want := response.FirstPage, 0; got != want {
   857  		t.Errorf("response.FirstPage: %v, want %v", got, want)
   858  	}
   859  }
   860  
   861  func TestDo(t *testing.T) {
   862  	client, mux, _, teardown := setup()
   863  	defer teardown()
   864  
   865  	type foo struct {
   866  		A string
   867  	}
   868  
   869  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   870  		testMethod(t, r, "GET")
   871  		fmt.Fprint(w, `{"A":"a"}`)
   872  	})
   873  
   874  	req, _ := client.NewRequest("GET", ".", nil)
   875  	body := new(foo)
   876  	ctx := context.Background()
   877  	client.Do(ctx, req, body)
   878  
   879  	want := &foo{"a"}
   880  	if !cmp.Equal(body, want) {
   881  		t.Errorf("Response body = %v, want %v", body, want)
   882  	}
   883  }
   884  
   885  func TestDo_nilContext(t *testing.T) {
   886  	client, _, _, teardown := setup()
   887  	defer teardown()
   888  
   889  	req, _ := client.NewRequest("GET", ".", nil)
   890  	_, err := client.Do(nil, req, nil)
   891  
   892  	if !errors.Is(err, errNonNilContext) {
   893  		t.Errorf("Expected context must be non-nil error")
   894  	}
   895  }
   896  
   897  func TestDo_httpError(t *testing.T) {
   898  	client, mux, _, teardown := setup()
   899  	defer teardown()
   900  
   901  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   902  		http.Error(w, "Bad Request", 400)
   903  	})
   904  
   905  	req, _ := client.NewRequest("GET", ".", nil)
   906  	ctx := context.Background()
   907  	resp, err := client.Do(ctx, req, nil)
   908  
   909  	if err == nil {
   910  		t.Fatal("Expected HTTP 400 error, got no error.")
   911  	}
   912  	if resp.StatusCode != 400 {
   913  		t.Errorf("Expected HTTP 400 error, got %d status code.", resp.StatusCode)
   914  	}
   915  }
   916  
   917  // Test handling of an error caused by the internal http client's Do()
   918  // function. A redirect loop is pretty unlikely to occur within the GitHub
   919  // API, but does allow us to exercise the right code path.
   920  func TestDo_redirectLoop(t *testing.T) {
   921  	client, mux, _, teardown := setup()
   922  	defer teardown()
   923  
   924  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   925  		http.Redirect(w, r, baseURLPath, http.StatusFound)
   926  	})
   927  
   928  	req, _ := client.NewRequest("GET", ".", nil)
   929  	ctx := context.Background()
   930  	_, err := client.Do(ctx, req, nil)
   931  
   932  	if err == nil {
   933  		t.Error("Expected error to be returned.")
   934  	}
   935  	if err, ok := err.(*url.Error); !ok {
   936  		t.Errorf("Expected a URL error; got %#v.", err)
   937  	}
   938  }
   939  
   940  // Test that an error caused by the internal http client's Do() function
   941  // does not leak the client secret.
   942  func TestDo_sanitizeURL(t *testing.T) {
   943  	tp := &UnauthenticatedRateLimitedTransport{
   944  		ClientID:     "id",
   945  		ClientSecret: "secret",
   946  	}
   947  	unauthedClient := NewClient(tp.Client())
   948  	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".
   949  	req, err := unauthedClient.NewRequest("GET", ".", nil)
   950  	if err != nil {
   951  		t.Fatalf("NewRequest returned unexpected error: %v", err)
   952  	}
   953  	ctx := context.Background()
   954  	_, err = unauthedClient.Do(ctx, req, nil)
   955  	if err == nil {
   956  		t.Fatal("Expected error to be returned.")
   957  	}
   958  	if strings.Contains(err.Error(), "client_secret=secret") {
   959  		t.Errorf("Do error contains secret, should be redacted:\n%q", err)
   960  	}
   961  }
   962  
   963  func TestDo_rateLimit(t *testing.T) {
   964  	client, mux, _, teardown := setup()
   965  	defer teardown()
   966  
   967  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   968  		w.Header().Set(headerRateLimit, "60")
   969  		w.Header().Set(headerRateRemaining, "59")
   970  		w.Header().Set(headerRateReset, "1372700873")
   971  	})
   972  
   973  	req, _ := client.NewRequest("GET", ".", nil)
   974  	ctx := context.Background()
   975  	resp, err := client.Do(ctx, req, nil)
   976  	if err != nil {
   977  		t.Errorf("Do returned unexpected error: %v", err)
   978  	}
   979  	if got, want := resp.Rate.Limit, 60; got != want {
   980  		t.Errorf("Client rate limit = %v, want %v", got, want)
   981  	}
   982  	if got, want := resp.Rate.Remaining, 59; got != want {
   983  		t.Errorf("Client rate remaining = %v, want %v", got, want)
   984  	}
   985  	reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
   986  	if resp.Rate.Reset.UTC() != reset {
   987  		t.Errorf("Client rate reset = %v, want %v", resp.Rate.Reset, reset)
   988  	}
   989  }
   990  
   991  // ensure rate limit is still parsed, even for error responses
   992  func TestDo_rateLimit_errorResponse(t *testing.T) {
   993  	client, mux, _, teardown := setup()
   994  	defer teardown()
   995  
   996  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   997  		w.Header().Set(headerRateLimit, "60")
   998  		w.Header().Set(headerRateRemaining, "59")
   999  		w.Header().Set(headerRateReset, "1372700873")
  1000  		http.Error(w, "Bad Request", 400)
  1001  	})
  1002  
  1003  	req, _ := client.NewRequest("GET", ".", nil)
  1004  	ctx := context.Background()
  1005  	resp, err := client.Do(ctx, req, nil)
  1006  	if err == nil {
  1007  		t.Error("Expected error to be returned.")
  1008  	}
  1009  	if _, ok := err.(*RateLimitError); ok {
  1010  		t.Errorf("Did not expect a *RateLimitError error; got %#v.", err)
  1011  	}
  1012  	if got, want := resp.Rate.Limit, 60; got != want {
  1013  		t.Errorf("Client rate limit = %v, want %v", got, want)
  1014  	}
  1015  	if got, want := resp.Rate.Remaining, 59; got != want {
  1016  		t.Errorf("Client rate remaining = %v, want %v", got, want)
  1017  	}
  1018  	reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
  1019  	if resp.Rate.Reset.UTC() != reset {
  1020  		t.Errorf("Client rate reset = %v, want %v", resp.Rate.Reset, reset)
  1021  	}
  1022  }
  1023  
  1024  // Ensure *RateLimitError is returned when API rate limit is exceeded.
  1025  func TestDo_rateLimit_rateLimitError(t *testing.T) {
  1026  	client, mux, _, teardown := setup()
  1027  	defer teardown()
  1028  
  1029  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  1030  		w.Header().Set(headerRateLimit, "60")
  1031  		w.Header().Set(headerRateRemaining, "0")
  1032  		w.Header().Set(headerRateReset, "1372700873")
  1033  		w.Header().Set("Content-Type", "application/json; charset=utf-8")
  1034  		w.WriteHeader(http.StatusForbidden)
  1035  		fmt.Fprintln(w, `{
  1036     "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.)",
  1037     "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
  1038  }`)
  1039  	})
  1040  
  1041  	req, _ := client.NewRequest("GET", ".", nil)
  1042  	ctx := context.Background()
  1043  	_, err := client.Do(ctx, req, nil)
  1044  
  1045  	if err == nil {
  1046  		t.Error("Expected error to be returned.")
  1047  	}
  1048  	rateLimitErr, ok := err.(*RateLimitError)
  1049  	if !ok {
  1050  		t.Fatalf("Expected a *RateLimitError error; got %#v.", err)
  1051  	}
  1052  	if got, want := rateLimitErr.Rate.Limit, 60; got != want {
  1053  		t.Errorf("rateLimitErr rate limit = %v, want %v", got, want)
  1054  	}
  1055  	if got, want := rateLimitErr.Rate.Remaining, 0; got != want {
  1056  		t.Errorf("rateLimitErr rate remaining = %v, want %v", got, want)
  1057  	}
  1058  	reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
  1059  	if rateLimitErr.Rate.Reset.UTC() != reset {
  1060  		t.Errorf("rateLimitErr rate reset = %v, want %v", rateLimitErr.Rate.Reset.UTC(), reset)
  1061  	}
  1062  }
  1063  
  1064  // Ensure a network call is not made when it's known that API rate limit is still exceeded.
  1065  func TestDo_rateLimit_noNetworkCall(t *testing.T) {
  1066  	client, mux, _, teardown := setup()
  1067  	defer teardown()
  1068  
  1069  	reset := time.Now().UTC().Add(time.Minute).Round(time.Second) // Rate reset is a minute from now, with 1 second precision.
  1070  
  1071  	mux.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) {
  1072  		w.Header().Set(headerRateLimit, "60")
  1073  		w.Header().Set(headerRateRemaining, "0")
  1074  		w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix()))
  1075  		w.Header().Set("Content-Type", "application/json; charset=utf-8")
  1076  		w.WriteHeader(http.StatusForbidden)
  1077  		fmt.Fprintln(w, `{
  1078     "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.)",
  1079     "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
  1080  }`)
  1081  	})
  1082  
  1083  	madeNetworkCall := false
  1084  	mux.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) {
  1085  		madeNetworkCall = true
  1086  	})
  1087  
  1088  	// First request is made, and it makes the client aware of rate reset time being in the future.
  1089  	req, _ := client.NewRequest("GET", "first", nil)
  1090  	ctx := context.Background()
  1091  	client.Do(ctx, req, nil)
  1092  
  1093  	// Second request should not cause a network call to be made, since client can predict a rate limit error.
  1094  	req, _ = client.NewRequest("GET", "second", nil)
  1095  	_, err := client.Do(ctx, req, nil)
  1096  
  1097  	if madeNetworkCall {
  1098  		t.Fatal("Network call was made, even though rate limit is known to still be exceeded.")
  1099  	}
  1100  
  1101  	if err == nil {
  1102  		t.Error("Expected error to be returned.")
  1103  	}
  1104  	rateLimitErr, ok := err.(*RateLimitError)
  1105  	if !ok {
  1106  		t.Fatalf("Expected a *RateLimitError error; got %#v.", err)
  1107  	}
  1108  	if got, want := rateLimitErr.Rate.Limit, 60; got != want {
  1109  		t.Errorf("rateLimitErr rate limit = %v, want %v", got, want)
  1110  	}
  1111  	if got, want := rateLimitErr.Rate.Remaining, 0; got != want {
  1112  		t.Errorf("rateLimitErr rate remaining = %v, want %v", got, want)
  1113  	}
  1114  	if rateLimitErr.Rate.Reset.UTC() != reset {
  1115  		t.Errorf("rateLimitErr rate reset = %v, want %v", rateLimitErr.Rate.Reset.UTC(), reset)
  1116  	}
  1117  }
  1118  
  1119  // Ensure *AbuseRateLimitError is returned when the response indicates that
  1120  // the client has triggered an abuse detection mechanism.
  1121  func TestDo_rateLimit_abuseRateLimitError(t *testing.T) {
  1122  	client, mux, _, teardown := setup()
  1123  	defer teardown()
  1124  
  1125  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  1126  		w.Header().Set("Content-Type", "application/json; charset=utf-8")
  1127  		w.WriteHeader(http.StatusForbidden)
  1128  		// When the abuse rate limit error is of the "temporarily blocked from content creation" type,
  1129  		// there is no "Retry-After" header.
  1130  		fmt.Fprintln(w, `{
  1131     "message": "You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.",
  1132     "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
  1133  }`)
  1134  	})
  1135  
  1136  	req, _ := client.NewRequest("GET", ".", nil)
  1137  	ctx := context.Background()
  1138  	_, err := client.Do(ctx, req, nil)
  1139  
  1140  	if err == nil {
  1141  		t.Error("Expected error to be returned.")
  1142  	}
  1143  	abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
  1144  	if !ok {
  1145  		t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
  1146  	}
  1147  	if got, want := abuseRateLimitErr.RetryAfter, (*time.Duration)(nil); got != want {
  1148  		t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
  1149  	}
  1150  }
  1151  
  1152  // Ensure *AbuseRateLimitError is returned when the response indicates that
  1153  // the client has triggered an abuse detection mechanism on GitHub Enterprise.
  1154  func TestDo_rateLimit_abuseRateLimitErrorEnterprise(t *testing.T) {
  1155  	client, mux, _, teardown := setup()
  1156  	defer teardown()
  1157  
  1158  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  1159  		w.Header().Set("Content-Type", "application/json; charset=utf-8")
  1160  		w.WriteHeader(http.StatusForbidden)
  1161  		// When the abuse rate limit error is of the "temporarily blocked from content creation" type,
  1162  		// there is no "Retry-After" header.
  1163  		// This response returns a documentation url like the one returned for GitHub Enterprise, this
  1164  		// url changes between versions but follows roughly the same format.
  1165  		fmt.Fprintln(w, `{
  1166     "message": "You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.",
  1167     "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
  1168  }`)
  1169  	})
  1170  
  1171  	req, _ := client.NewRequest("GET", ".", nil)
  1172  	ctx := context.Background()
  1173  	_, err := client.Do(ctx, req, nil)
  1174  
  1175  	if err == nil {
  1176  		t.Error("Expected error to be returned.")
  1177  	}
  1178  	abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
  1179  	if !ok {
  1180  		t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
  1181  	}
  1182  	if got, want := abuseRateLimitErr.RetryAfter, (*time.Duration)(nil); got != want {
  1183  		t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
  1184  	}
  1185  }
  1186  
  1187  // Ensure *AbuseRateLimitError.RetryAfter is parsed correctly.
  1188  func TestDo_rateLimit_abuseRateLimitError_retryAfter(t *testing.T) {
  1189  	client, mux, _, teardown := setup()
  1190  	defer teardown()
  1191  
  1192  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  1193  		w.Header().Set("Content-Type", "application/json; charset=utf-8")
  1194  		w.Header().Set("Retry-After", "123") // Retry after value of 123 seconds.
  1195  		w.WriteHeader(http.StatusForbidden)
  1196  		fmt.Fprintln(w, `{
  1197     "message": "You have triggered an abuse detection mechanism ...",
  1198     "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
  1199  }`)
  1200  	})
  1201  
  1202  	req, _ := client.NewRequest("GET", ".", nil)
  1203  	ctx := context.Background()
  1204  	_, err := client.Do(ctx, req, nil)
  1205  
  1206  	if err == nil {
  1207  		t.Error("Expected error to be returned.")
  1208  	}
  1209  	abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
  1210  	if !ok {
  1211  		t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
  1212  	}
  1213  	if abuseRateLimitErr.RetryAfter == nil {
  1214  		t.Fatalf("abuseRateLimitErr RetryAfter is nil, expected not-nil")
  1215  	}
  1216  	if got, want := *abuseRateLimitErr.RetryAfter, 123*time.Second; got != want {
  1217  		t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
  1218  	}
  1219  }
  1220  
  1221  func TestDo_noContent(t *testing.T) {
  1222  	client, mux, _, teardown := setup()
  1223  	defer teardown()
  1224  
  1225  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  1226  		w.WriteHeader(http.StatusNoContent)
  1227  	})
  1228  
  1229  	var body json.RawMessage
  1230  
  1231  	req, _ := client.NewRequest("GET", ".", nil)
  1232  	ctx := context.Background()
  1233  	_, err := client.Do(ctx, req, &body)
  1234  	if err != nil {
  1235  		t.Fatalf("Do returned unexpected error: %v", err)
  1236  	}
  1237  }
  1238  
  1239  func TestSanitizeURL(t *testing.T) {
  1240  	tests := []struct {
  1241  		in, want string
  1242  	}{
  1243  		{"/?a=b", "/?a=b"},
  1244  		{"/?a=b&client_secret=secret", "/?a=b&client_secret=REDACTED"},
  1245  		{"/?a=b&client_id=id&client_secret=secret", "/?a=b&client_id=id&client_secret=REDACTED"},
  1246  	}
  1247  
  1248  	for _, tt := range tests {
  1249  		inURL, _ := url.Parse(tt.in)
  1250  		want, _ := url.Parse(tt.want)
  1251  
  1252  		if got := sanitizeURL(inURL); !cmp.Equal(got, want) {
  1253  			t.Errorf("sanitizeURL(%v) returned %v, want %v", tt.in, got, want)
  1254  		}
  1255  	}
  1256  }
  1257  
  1258  func TestCheckResponse(t *testing.T) {
  1259  	res := &http.Response{
  1260  		Request:    &http.Request{},
  1261  		StatusCode: http.StatusBadRequest,
  1262  		Body: ioutil.NopCloser(strings.NewReader(`{"message":"m",
  1263  			"errors": [{"resource": "r", "field": "f", "code": "c"}],
  1264  			"block": {"reason": "dmca", "created_at": "2016-03-17T15:39:46Z"}}`)),
  1265  	}
  1266  	err := CheckResponse(res).(*ErrorResponse)
  1267  
  1268  	if err == nil {
  1269  		t.Errorf("Expected error response.")
  1270  	}
  1271  
  1272  	want := &ErrorResponse{
  1273  		Response: res,
  1274  		Message:  "m",
  1275  		Errors:   []Error{{Resource: "r", Field: "f", Code: "c"}},
  1276  		Block: &ErrorBlock{
  1277  			Reason:    "dmca",
  1278  			CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
  1279  		},
  1280  	}
  1281  	if !errors.Is(err, want) {
  1282  		t.Errorf("Error = %#v, want %#v", err, want)
  1283  	}
  1284  }
  1285  
  1286  func TestCheckResponse_RateLimit(t *testing.T) {
  1287  	res := &http.Response{
  1288  		Request:    &http.Request{},
  1289  		StatusCode: http.StatusForbidden,
  1290  		Header:     http.Header{},
  1291  		Body: ioutil.NopCloser(strings.NewReader(`{"message":"m",
  1292  			"documentation_url": "url"}`)),
  1293  	}
  1294  	res.Header.Set(headerRateLimit, "60")
  1295  	res.Header.Set(headerRateRemaining, "0")
  1296  	res.Header.Set(headerRateReset, "243424")
  1297  
  1298  	err := CheckResponse(res).(*RateLimitError)
  1299  
  1300  	if err == nil {
  1301  		t.Errorf("Expected error response.")
  1302  	}
  1303  
  1304  	want := &RateLimitError{
  1305  		Rate:     parseRate(res),
  1306  		Response: res,
  1307  		Message:  "m",
  1308  	}
  1309  	if !errors.Is(err, want) {
  1310  		t.Errorf("Error = %#v, want %#v", err, want)
  1311  	}
  1312  }
  1313  
  1314  func TestCheckResponse_AbuseRateLimit(t *testing.T) {
  1315  	res := &http.Response{
  1316  		Request:    &http.Request{},
  1317  		StatusCode: http.StatusForbidden,
  1318  		Body: ioutil.NopCloser(strings.NewReader(`{"message":"m",
  1319  			"documentation_url": "docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits"}`)),
  1320  	}
  1321  	err := CheckResponse(res).(*AbuseRateLimitError)
  1322  
  1323  	if err == nil {
  1324  		t.Errorf("Expected error response.")
  1325  	}
  1326  
  1327  	want := &AbuseRateLimitError{
  1328  		Response: res,
  1329  		Message:  "m",
  1330  	}
  1331  	if !errors.Is(err, want) {
  1332  		t.Errorf("Error = %#v, want %#v", err, want)
  1333  	}
  1334  }
  1335  
  1336  func TestCompareHttpResponse(t *testing.T) {
  1337  	testcases := map[string]struct {
  1338  		h1       *http.Response
  1339  		h2       *http.Response
  1340  		expected bool
  1341  	}{
  1342  		"both are nil": {
  1343  			expected: true,
  1344  		},
  1345  		"both are non nil - same StatusCode": {
  1346  			expected: true,
  1347  			h1:       &http.Response{StatusCode: 200},
  1348  			h2:       &http.Response{StatusCode: 200},
  1349  		},
  1350  		"both are non nil - different StatusCode": {
  1351  			expected: false,
  1352  			h1:       &http.Response{StatusCode: 200},
  1353  			h2:       &http.Response{StatusCode: 404},
  1354  		},
  1355  		"one is nil, other is not": {
  1356  			expected: false,
  1357  			h2:       &http.Response{},
  1358  		},
  1359  	}
  1360  
  1361  	for name, tc := range testcases {
  1362  		t.Run(name, func(t *testing.T) {
  1363  			v := compareHTTPResponse(tc.h1, tc.h2)
  1364  			if tc.expected != v {
  1365  				t.Errorf("Expected %t, got %t for (%#v, %#v)", tc.expected, v, tc.h1, tc.h2)
  1366  			}
  1367  		})
  1368  	}
  1369  }
  1370  
  1371  func TestErrorResponse_Is(t *testing.T) {
  1372  	err := &ErrorResponse{
  1373  		Response: &http.Response{},
  1374  		Message:  "m",
  1375  		Errors:   []Error{{Resource: "r", Field: "f", Code: "c"}},
  1376  		Block: &ErrorBlock{
  1377  			Reason:    "r",
  1378  			CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
  1379  		},
  1380  		DocumentationURL: "https://github.com",
  1381  	}
  1382  	testcases := map[string]struct {
  1383  		wantSame   bool
  1384  		otherError error
  1385  	}{
  1386  		"errors are same": {
  1387  			wantSame: true,
  1388  			otherError: &ErrorResponse{
  1389  				Response: &http.Response{},
  1390  				Errors:   []Error{{Resource: "r", Field: "f", Code: "c"}},
  1391  				Message:  "m",
  1392  				Block: &ErrorBlock{
  1393  					Reason:    "r",
  1394  					CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
  1395  				},
  1396  				DocumentationURL: "https://github.com",
  1397  			},
  1398  		},
  1399  		"errors have different values - Message": {
  1400  			wantSame: false,
  1401  			otherError: &ErrorResponse{
  1402  				Response: &http.Response{},
  1403  				Errors:   []Error{{Resource: "r", Field: "f", Code: "c"}},
  1404  				Message:  "m1",
  1405  				Block: &ErrorBlock{
  1406  					Reason:    "r",
  1407  					CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
  1408  				},
  1409  				DocumentationURL: "https://github.com",
  1410  			},
  1411  		},
  1412  		"errors have different values - DocumentationURL": {
  1413  			wantSame: false,
  1414  			otherError: &ErrorResponse{
  1415  				Response: &http.Response{},
  1416  				Errors:   []Error{{Resource: "r", Field: "f", Code: "c"}},
  1417  				Message:  "m",
  1418  				Block: &ErrorBlock{
  1419  					Reason:    "r",
  1420  					CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
  1421  				},
  1422  				DocumentationURL: "https://google.com",
  1423  			},
  1424  		},
  1425  		"errors have different values - Response is nil": {
  1426  			wantSame: false,
  1427  			otherError: &ErrorResponse{
  1428  				Errors:  []Error{{Resource: "r", Field: "f", Code: "c"}},
  1429  				Message: "m",
  1430  				Block: &ErrorBlock{
  1431  					Reason:    "r",
  1432  					CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
  1433  				},
  1434  				DocumentationURL: "https://github.com",
  1435  			},
  1436  		},
  1437  		"errors have different values - Errors": {
  1438  			wantSame: false,
  1439  			otherError: &ErrorResponse{
  1440  				Response: &http.Response{},
  1441  				Errors:   []Error{{Resource: "r1", Field: "f1", Code: "c1"}},
  1442  				Message:  "m",
  1443  				Block: &ErrorBlock{
  1444  					Reason:    "r",
  1445  					CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
  1446  				},
  1447  				DocumentationURL: "https://github.com",
  1448  			},
  1449  		},
  1450  		"errors have different values - Errors have different length": {
  1451  			wantSame: false,
  1452  			otherError: &ErrorResponse{
  1453  				Response: &http.Response{},
  1454  				Errors:   []Error{},
  1455  				Message:  "m",
  1456  				Block: &ErrorBlock{
  1457  					Reason:    "r",
  1458  					CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
  1459  				},
  1460  				DocumentationURL: "https://github.com",
  1461  			},
  1462  		},
  1463  		"errors have different values - Block - one is nil, other is not": {
  1464  			wantSame: false,
  1465  			otherError: &ErrorResponse{
  1466  				Response:         &http.Response{},
  1467  				Errors:           []Error{{Resource: "r", Field: "f", Code: "c"}},
  1468  				Message:          "m",
  1469  				DocumentationURL: "https://github.com",
  1470  			},
  1471  		},
  1472  		"errors have different values - Block - different Reason": {
  1473  			wantSame: false,
  1474  			otherError: &ErrorResponse{
  1475  				Response: &http.Response{},
  1476  				Errors:   []Error{{Resource: "r", Field: "f", Code: "c"}},
  1477  				Message:  "m",
  1478  				Block: &ErrorBlock{
  1479  					Reason:    "r1",
  1480  					CreatedAt: &Timestamp{time.Date(2016, time.March, 17, 15, 39, 46, 0, time.UTC)},
  1481  				},
  1482  				DocumentationURL: "https://github.com",
  1483  			},
  1484  		},
  1485  		"errors have different values - Block - different CreatedAt #1": {
  1486  			wantSame: false,
  1487  			otherError: &ErrorResponse{
  1488  				Response: &http.Response{},
  1489  				Errors:   []Error{{Resource: "r", Field: "f", Code: "c"}},
  1490  				Message:  "m",
  1491  				Block: &ErrorBlock{
  1492  					Reason:    "r",
  1493  					CreatedAt: nil,
  1494  				},
  1495  				DocumentationURL: "https://github.com",
  1496  			},
  1497  		},
  1498  		"errors have different values - Block - different CreatedAt #2": {
  1499  			wantSame: false,
  1500  			otherError: &ErrorResponse{
  1501  				Response: &http.Response{},
  1502  				Errors:   []Error{{Resource: "r", Field: "f", Code: "c"}},
  1503  				Message:  "m",
  1504  				Block: &ErrorBlock{
  1505  					Reason:    "r",
  1506  					CreatedAt: &Timestamp{time.Date(2017, time.March, 17, 15, 39, 46, 0, time.UTC)},
  1507  				},
  1508  				DocumentationURL: "https://github.com",
  1509  			},
  1510  		},
  1511  		"errors have different types": {
  1512  			wantSame:   false,
  1513  			otherError: errors.New("Github"),
  1514  		},
  1515  	}
  1516  
  1517  	for name, tc := range testcases {
  1518  		t.Run(name, func(t *testing.T) {
  1519  			if tc.wantSame != err.Is(tc.otherError) {
  1520  				t.Errorf("Error = %#v, want %#v", err, tc.otherError)
  1521  			}
  1522  		})
  1523  	}
  1524  }
  1525  
  1526  func TestRateLimitError_Is(t *testing.T) {
  1527  	err := &RateLimitError{
  1528  		Response: &http.Response{},
  1529  		Message:  "Github",
  1530  	}
  1531  	testcases := map[string]struct {
  1532  		wantSame   bool
  1533  		err        *RateLimitError
  1534  		otherError error
  1535  	}{
  1536  		"errors are same": {
  1537  			wantSame: true,
  1538  			err:      err,
  1539  			otherError: &RateLimitError{
  1540  				Response: &http.Response{},
  1541  				Message:  "Github",
  1542  			},
  1543  		},
  1544  		"errors are same - Response is nil": {
  1545  			wantSame: true,
  1546  			err: &RateLimitError{
  1547  				Message: "Github",
  1548  			},
  1549  			otherError: &RateLimitError{
  1550  				Message: "Github",
  1551  			},
  1552  		},
  1553  		"errors have different values - Rate": {
  1554  			wantSame: false,
  1555  			err:      err,
  1556  			otherError: &RateLimitError{
  1557  				Rate:     Rate{Limit: 10},
  1558  				Response: &http.Response{},
  1559  				Message:  "Gitlab",
  1560  			},
  1561  		},
  1562  		"errors have different values - Response is nil": {
  1563  			wantSame: false,
  1564  			err:      err,
  1565  			otherError: &RateLimitError{
  1566  				Message: "Github",
  1567  			},
  1568  		},
  1569  		"errors have different values - StatusCode": {
  1570  			wantSame: false,
  1571  			err:      err,
  1572  			otherError: &RateLimitError{
  1573  				Response: &http.Response{StatusCode: 200},
  1574  				Message:  "Github",
  1575  			},
  1576  		},
  1577  		"errors have different types": {
  1578  			wantSame:   false,
  1579  			err:        err,
  1580  			otherError: errors.New("Github"),
  1581  		},
  1582  	}
  1583  
  1584  	for name, tc := range testcases {
  1585  		t.Run(name, func(t *testing.T) {
  1586  			if tc.wantSame != tc.err.Is(tc.otherError) {
  1587  				t.Errorf("Error = %#v, want %#v", tc.err, tc.otherError)
  1588  			}
  1589  		})
  1590  	}
  1591  }
  1592  
  1593  func TestAbuseRateLimitError_Is(t *testing.T) {
  1594  	t1 := 1 * time.Second
  1595  	t2 := 2 * time.Second
  1596  	err := &AbuseRateLimitError{
  1597  		Response:   &http.Response{},
  1598  		Message:    "Github",
  1599  		RetryAfter: &t1,
  1600  	}
  1601  	testcases := map[string]struct {
  1602  		wantSame   bool
  1603  		err        *AbuseRateLimitError
  1604  		otherError error
  1605  	}{
  1606  		"errors are same": {
  1607  			wantSame: true,
  1608  			err:      err,
  1609  			otherError: &AbuseRateLimitError{
  1610  				Response:   &http.Response{},
  1611  				Message:    "Github",
  1612  				RetryAfter: &t1,
  1613  			},
  1614  		},
  1615  		"errors are same - Response is nil": {
  1616  			wantSame: true,
  1617  			err: &AbuseRateLimitError{
  1618  				Message:    "Github",
  1619  				RetryAfter: &t1,
  1620  			},
  1621  			otherError: &AbuseRateLimitError{
  1622  				Message:    "Github",
  1623  				RetryAfter: &t1,
  1624  			},
  1625  		},
  1626  		"errors have different values - Message": {
  1627  			wantSame: false,
  1628  			err:      err,
  1629  			otherError: &AbuseRateLimitError{
  1630  				Response:   &http.Response{},
  1631  				Message:    "Gitlab",
  1632  				RetryAfter: nil,
  1633  			},
  1634  		},
  1635  		"errors have different values - RetryAfter": {
  1636  			wantSame: false,
  1637  			err:      err,
  1638  			otherError: &AbuseRateLimitError{
  1639  				Response:   &http.Response{},
  1640  				Message:    "Github",
  1641  				RetryAfter: &t2,
  1642  			},
  1643  		},
  1644  		"errors have different values - Response is nil": {
  1645  			wantSame: false,
  1646  			err:      err,
  1647  			otherError: &AbuseRateLimitError{
  1648  				Message:    "Github",
  1649  				RetryAfter: &t1,
  1650  			},
  1651  		},
  1652  		"errors have different values - StatusCode": {
  1653  			wantSame: false,
  1654  			err:      err,
  1655  			otherError: &AbuseRateLimitError{
  1656  				Response:   &http.Response{StatusCode: 200},
  1657  				Message:    "Github",
  1658  				RetryAfter: &t1,
  1659  			},
  1660  		},
  1661  		"errors have different types": {
  1662  			wantSame:   false,
  1663  			err:        err,
  1664  			otherError: errors.New("Github"),
  1665  		},
  1666  	}
  1667  
  1668  	for name, tc := range testcases {
  1669  		t.Run(name, func(t *testing.T) {
  1670  			if tc.wantSame != tc.err.Is(tc.otherError) {
  1671  				t.Errorf("Error = %#v, want %#v", tc.err, tc.otherError)
  1672  			}
  1673  		})
  1674  	}
  1675  }
  1676  
  1677  func TestAcceptedError_Is(t *testing.T) {
  1678  	err := &AcceptedError{Raw: []byte("Github")}
  1679  	testcases := map[string]struct {
  1680  		wantSame   bool
  1681  		otherError error
  1682  	}{
  1683  		"errors are same": {
  1684  			wantSame:   true,
  1685  			otherError: &AcceptedError{Raw: []byte("Github")},
  1686  		},
  1687  		"errors have different values": {
  1688  			wantSame:   false,
  1689  			otherError: &AcceptedError{Raw: []byte("Gitlab")},
  1690  		},
  1691  		"errors have different types": {
  1692  			wantSame:   false,
  1693  			otherError: errors.New("Github"),
  1694  		},
  1695  	}
  1696  
  1697  	for name, tc := range testcases {
  1698  		t.Run(name, func(t *testing.T) {
  1699  			if tc.wantSame != err.Is(tc.otherError) {
  1700  				t.Errorf("Error = %#v, want %#v", err, tc.otherError)
  1701  			}
  1702  		})
  1703  	}
  1704  }
  1705  
  1706  // ensure that we properly handle API errors that do not contain a response body
  1707  func TestCheckResponse_noBody(t *testing.T) {
  1708  	res := &http.Response{
  1709  		Request:    &http.Request{},
  1710  		StatusCode: http.StatusBadRequest,
  1711  		Body:       ioutil.NopCloser(strings.NewReader("")),
  1712  	}
  1713  	err := CheckResponse(res).(*ErrorResponse)
  1714  
  1715  	if err == nil {
  1716  		t.Errorf("Expected error response.")
  1717  	}
  1718  
  1719  	want := &ErrorResponse{
  1720  		Response: res,
  1721  	}
  1722  	if !errors.Is(err, want) {
  1723  		t.Errorf("Error = %#v, want %#v", err, want)
  1724  	}
  1725  }
  1726  
  1727  func TestCheckResponse_unexpectedErrorStructure(t *testing.T) {
  1728  	httpBody := `{"message":"m", "errors": ["error 1"]}`
  1729  	res := &http.Response{
  1730  		Request:    &http.Request{},
  1731  		StatusCode: http.StatusBadRequest,
  1732  		Body:       ioutil.NopCloser(strings.NewReader(httpBody)),
  1733  	}
  1734  	err := CheckResponse(res).(*ErrorResponse)
  1735  
  1736  	if err == nil {
  1737  		t.Errorf("Expected error response.")
  1738  	}
  1739  
  1740  	want := &ErrorResponse{
  1741  		Response: res,
  1742  		Message:  "m",
  1743  		Errors:   []Error{{Message: "error 1"}},
  1744  	}
  1745  	if !errors.Is(err, want) {
  1746  		t.Errorf("Error = %#v, want %#v", err, want)
  1747  	}
  1748  	data, err2 := ioutil.ReadAll(err.Response.Body)
  1749  	if err2 != nil {
  1750  		t.Fatalf("failed to read response body: %v", err)
  1751  	}
  1752  	if got := string(data); got != httpBody {
  1753  		t.Errorf("ErrorResponse.Response.Body = %q, want %q", got, httpBody)
  1754  	}
  1755  }
  1756  
  1757  func TestParseBooleanResponse_true(t *testing.T) {
  1758  	result, err := parseBoolResponse(nil)
  1759  	if err != nil {
  1760  		t.Errorf("parseBoolResponse returned error: %+v", err)
  1761  	}
  1762  
  1763  	if want := true; result != want {
  1764  		t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want)
  1765  	}
  1766  }
  1767  
  1768  func TestParseBooleanResponse_false(t *testing.T) {
  1769  	v := &ErrorResponse{Response: &http.Response{StatusCode: http.StatusNotFound}}
  1770  	result, err := parseBoolResponse(v)
  1771  	if err != nil {
  1772  		t.Errorf("parseBoolResponse returned error: %+v", err)
  1773  	}
  1774  
  1775  	if want := false; result != want {
  1776  		t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want)
  1777  	}
  1778  }
  1779  
  1780  func TestParseBooleanResponse_error(t *testing.T) {
  1781  	v := &ErrorResponse{Response: &http.Response{StatusCode: http.StatusBadRequest}}
  1782  	result, err := parseBoolResponse(v)
  1783  
  1784  	if err == nil {
  1785  		t.Errorf("Expected error to be returned.")
  1786  	}
  1787  
  1788  	if want := false; result != want {
  1789  		t.Errorf("parseBoolResponse returned %+v, want: %+v", result, want)
  1790  	}
  1791  }
  1792  
  1793  func TestErrorResponse_Error(t *testing.T) {
  1794  	res := &http.Response{Request: &http.Request{}}
  1795  	err := ErrorResponse{Message: "m", Response: res}
  1796  	if err.Error() == "" {
  1797  		t.Errorf("Expected non-empty ErrorResponse.Error()")
  1798  	}
  1799  }
  1800  
  1801  func TestError_Error(t *testing.T) {
  1802  	err := Error{}
  1803  	if err.Error() == "" {
  1804  		t.Errorf("Expected non-empty Error.Error()")
  1805  	}
  1806  }
  1807  
  1808  func TestRateLimits(t *testing.T) {
  1809  	client, mux, _, teardown := setup()
  1810  	defer teardown()
  1811  
  1812  	mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) {
  1813  		testMethod(t, r, "GET")
  1814  		fmt.Fprint(w, `{"resources":{
  1815  			"core": {"limit":2,"remaining":1,"reset":1372700873},
  1816  			"search": {"limit":3,"remaining":2,"reset":1372700874}
  1817  		}}`)
  1818  	})
  1819  
  1820  	ctx := context.Background()
  1821  	rate, _, err := client.RateLimits(ctx)
  1822  	if err != nil {
  1823  		t.Errorf("RateLimits returned error: %v", err)
  1824  	}
  1825  
  1826  	want := &RateLimits{
  1827  		Core: &Rate{
  1828  			Limit:     2,
  1829  			Remaining: 1,
  1830  			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC).Local()},
  1831  		},
  1832  		Search: &Rate{
  1833  			Limit:     3,
  1834  			Remaining: 2,
  1835  			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()},
  1836  		},
  1837  	}
  1838  	if !cmp.Equal(rate, want) {
  1839  		t.Errorf("RateLimits returned %+v, want %+v", rate, want)
  1840  	}
  1841  
  1842  	if got, want := client.rateLimits[coreCategory], *want.Core; got != want {
  1843  		t.Errorf("client.rateLimits[coreCategory] is %+v, want %+v", got, want)
  1844  	}
  1845  	if got, want := client.rateLimits[searchCategory], *want.Search; got != want {
  1846  		t.Errorf("client.rateLimits[searchCategory] is %+v, want %+v", got, want)
  1847  	}
  1848  }
  1849  
  1850  func TestRateLimits_coverage(t *testing.T) {
  1851  	client, _, _, teardown := setup()
  1852  	defer teardown()
  1853  
  1854  	ctx := context.Background()
  1855  
  1856  	const methodName = "RateLimits"
  1857  	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
  1858  		_, resp, err := client.RateLimits(ctx)
  1859  		return resp, err
  1860  	})
  1861  }
  1862  
  1863  func TestRateLimits_overQuota(t *testing.T) {
  1864  	client, mux, _, teardown := setup()
  1865  	defer teardown()
  1866  
  1867  	client.rateLimits[coreCategory] = Rate{
  1868  		Limit:     1,
  1869  		Remaining: 0,
  1870  		Reset:     Timestamp{time.Now().Add(time.Hour).Local()},
  1871  	}
  1872  	mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) {
  1873  		fmt.Fprint(w, `{"resources":{
  1874  			"core": {"limit":2,"remaining":1,"reset":1372700873},
  1875  			"search": {"limit":3,"remaining":2,"reset":1372700874}
  1876  		}}`)
  1877  	})
  1878  
  1879  	ctx := context.Background()
  1880  	rate, _, err := client.RateLimits(ctx)
  1881  	if err != nil {
  1882  		t.Errorf("RateLimits returned error: %v", err)
  1883  	}
  1884  
  1885  	want := &RateLimits{
  1886  		Core: &Rate{
  1887  			Limit:     2,
  1888  			Remaining: 1,
  1889  			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC).Local()},
  1890  		},
  1891  		Search: &Rate{
  1892  			Limit:     3,
  1893  			Remaining: 2,
  1894  			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()},
  1895  		},
  1896  	}
  1897  	if !cmp.Equal(rate, want) {
  1898  		t.Errorf("RateLimits returned %+v, want %+v", rate, want)
  1899  	}
  1900  
  1901  	if got, want := client.rateLimits[coreCategory], *want.Core; got != want {
  1902  		t.Errorf("client.rateLimits[coreCategory] is %+v, want %+v", got, want)
  1903  	}
  1904  	if got, want := client.rateLimits[searchCategory], *want.Search; got != want {
  1905  		t.Errorf("client.rateLimits[searchCategory] is %+v, want %+v", got, want)
  1906  	}
  1907  }
  1908  
  1909  func TestSetCredentialsAsHeaders(t *testing.T) {
  1910  	req := new(http.Request)
  1911  	id, secret := "id", "secret"
  1912  	modifiedRequest := setCredentialsAsHeaders(req, id, secret)
  1913  
  1914  	actualID, actualSecret, ok := modifiedRequest.BasicAuth()
  1915  	if !ok {
  1916  		t.Errorf("request does not contain basic credentials")
  1917  	}
  1918  
  1919  	if actualID != id {
  1920  		t.Errorf("id is %s, want %s", actualID, id)
  1921  	}
  1922  
  1923  	if actualSecret != secret {
  1924  		t.Errorf("secret is %s, want %s", actualSecret, secret)
  1925  	}
  1926  }
  1927  
  1928  func TestUnauthenticatedRateLimitedTransport(t *testing.T) {
  1929  	client, mux, _, teardown := setup()
  1930  	defer teardown()
  1931  
  1932  	clientID, clientSecret := "id", "secret"
  1933  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  1934  		id, secret, ok := r.BasicAuth()
  1935  		if !ok {
  1936  			t.Errorf("request does not contain basic auth credentials")
  1937  		}
  1938  		if id != clientID {
  1939  			t.Errorf("request contained basic auth username %q, want %q", id, clientID)
  1940  		}
  1941  		if secret != clientSecret {
  1942  			t.Errorf("request contained basic auth password %q, want %q", secret, clientSecret)
  1943  		}
  1944  	})
  1945  
  1946  	tp := &UnauthenticatedRateLimitedTransport{
  1947  		ClientID:     clientID,
  1948  		ClientSecret: clientSecret,
  1949  	}
  1950  	unauthedClient := NewClient(tp.Client())
  1951  	unauthedClient.BaseURL = client.BaseURL
  1952  	req, _ := unauthedClient.NewRequest("GET", ".", nil)
  1953  	ctx := context.Background()
  1954  	unauthedClient.Do(ctx, req, nil)
  1955  }
  1956  
  1957  func TestUnauthenticatedRateLimitedTransport_missingFields(t *testing.T) {
  1958  	// missing ClientID
  1959  	tp := &UnauthenticatedRateLimitedTransport{
  1960  		ClientSecret: "secret",
  1961  	}
  1962  	_, err := tp.RoundTrip(nil)
  1963  	if err == nil {
  1964  		t.Errorf("Expected error to be returned")
  1965  	}
  1966  
  1967  	// missing ClientSecret
  1968  	tp = &UnauthenticatedRateLimitedTransport{
  1969  		ClientID: "id",
  1970  	}
  1971  	_, err = tp.RoundTrip(nil)
  1972  	if err == nil {
  1973  		t.Errorf("Expected error to be returned")
  1974  	}
  1975  }
  1976  
  1977  func TestUnauthenticatedRateLimitedTransport_transport(t *testing.T) {
  1978  	// default transport
  1979  	tp := &UnauthenticatedRateLimitedTransport{
  1980  		ClientID:     "id",
  1981  		ClientSecret: "secret",
  1982  	}
  1983  	if tp.transport() != http.DefaultTransport {
  1984  		t.Errorf("Expected http.DefaultTransport to be used.")
  1985  	}
  1986  
  1987  	// custom transport
  1988  	tp = &UnauthenticatedRateLimitedTransport{
  1989  		ClientID:     "id",
  1990  		ClientSecret: "secret",
  1991  		Transport:    &http.Transport{},
  1992  	}
  1993  	if tp.transport() == http.DefaultTransport {
  1994  		t.Errorf("Expected custom transport to be used.")
  1995  	}
  1996  }
  1997  
  1998  func TestBasicAuthTransport(t *testing.T) {
  1999  	client, mux, _, teardown := setup()
  2000  	defer teardown()
  2001  
  2002  	username, password, otp := "u", "p", "123456"
  2003  
  2004  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  2005  		u, p, ok := r.BasicAuth()
  2006  		if !ok {
  2007  			t.Errorf("request does not contain basic auth credentials")
  2008  		}
  2009  		if u != username {
  2010  			t.Errorf("request contained basic auth username %q, want %q", u, username)
  2011  		}
  2012  		if p != password {
  2013  			t.Errorf("request contained basic auth password %q, want %q", p, password)
  2014  		}
  2015  		if got, want := r.Header.Get(headerOTP), otp; got != want {
  2016  			t.Errorf("request contained OTP %q, want %q", got, want)
  2017  		}
  2018  	})
  2019  
  2020  	tp := &BasicAuthTransport{
  2021  		Username: username,
  2022  		Password: password,
  2023  		OTP:      otp,
  2024  	}
  2025  	basicAuthClient := NewClient(tp.Client())
  2026  	basicAuthClient.BaseURL = client.BaseURL
  2027  	req, _ := basicAuthClient.NewRequest("GET", ".", nil)
  2028  	ctx := context.Background()
  2029  	basicAuthClient.Do(ctx, req, nil)
  2030  }
  2031  
  2032  func TestBasicAuthTransport_transport(t *testing.T) {
  2033  	// default transport
  2034  	tp := &BasicAuthTransport{}
  2035  	if tp.transport() != http.DefaultTransport {
  2036  		t.Errorf("Expected http.DefaultTransport to be used.")
  2037  	}
  2038  
  2039  	// custom transport
  2040  	tp = &BasicAuthTransport{
  2041  		Transport: &http.Transport{},
  2042  	}
  2043  	if tp.transport() == http.DefaultTransport {
  2044  		t.Errorf("Expected custom transport to be used.")
  2045  	}
  2046  }
  2047  
  2048  func TestFormatRateReset(t *testing.T) {
  2049  	d := 120*time.Minute + 12*time.Second
  2050  	got := formatRateReset(d)
  2051  	want := "[rate reset in 120m12s]"
  2052  	if got != want {
  2053  		t.Errorf("Format is wrong. got: %v, want: %v", got, want)
  2054  	}
  2055  
  2056  	d = 14*time.Minute + 2*time.Second
  2057  	got = formatRateReset(d)
  2058  	want = "[rate reset in 14m02s]"
  2059  	if got != want {
  2060  		t.Errorf("Format is wrong. got: %v, want: %v", got, want)
  2061  	}
  2062  
  2063  	d = 2*time.Minute + 2*time.Second
  2064  	got = formatRateReset(d)
  2065  	want = "[rate reset in 2m02s]"
  2066  	if got != want {
  2067  		t.Errorf("Format is wrong. got: %v, want: %v", got, want)
  2068  	}
  2069  
  2070  	d = 12 * time.Second
  2071  	got = formatRateReset(d)
  2072  	want = "[rate reset in 12s]"
  2073  	if got != want {
  2074  		t.Errorf("Format is wrong. got: %v, want: %v", got, want)
  2075  	}
  2076  
  2077  	d = -1 * (2*time.Hour + 2*time.Second)
  2078  	got = formatRateReset(d)
  2079  	want = "[rate limit was reset 120m02s ago]"
  2080  	if got != want {
  2081  		t.Errorf("Format is wrong. got: %v, want: %v", got, want)
  2082  	}
  2083  }
  2084  
  2085  func TestNestedStructAccessorNoPanic(t *testing.T) {
  2086  	issue := &Issue{User: nil}
  2087  	got := issue.GetUser().GetPlan().GetName()
  2088  	want := ""
  2089  	if got != want {
  2090  		t.Errorf("Issues.Get.GetUser().GetPlan().GetName() returned %+v, want %+v", got, want)
  2091  	}
  2092  }
  2093  
  2094  func TestTwoFactorAuthError(t *testing.T) {
  2095  	u, err := url.Parse("https://example.com")
  2096  	if err != nil {
  2097  		t.Fatal(err)
  2098  	}
  2099  
  2100  	e := &TwoFactorAuthError{
  2101  		Response: &http.Response{
  2102  			Request:    &http.Request{Method: "PUT", URL: u},
  2103  			StatusCode: http.StatusTooManyRequests,
  2104  		},
  2105  		Message: "<msg>",
  2106  	}
  2107  	if got, want := e.Error(), "PUT https://example.com: 429 <msg> []"; got != want {
  2108  		t.Errorf("TwoFactorAuthError = %q, want %q", got, want)
  2109  	}
  2110  }
  2111  
  2112  func TestRateLimitError(t *testing.T) {
  2113  	u, err := url.Parse("https://example.com")
  2114  	if err != nil {
  2115  		t.Fatal(err)
  2116  	}
  2117  
  2118  	r := &RateLimitError{
  2119  		Response: &http.Response{
  2120  			Request:    &http.Request{Method: "PUT", URL: u},
  2121  			StatusCode: http.StatusTooManyRequests,
  2122  		},
  2123  		Message: "<msg>",
  2124  	}
  2125  	if got, want := r.Error(), "PUT https://example.com: 429 <msg> [rate limit was reset"; !strings.Contains(got, want) {
  2126  		t.Errorf("RateLimitError = %q, want %q", got, want)
  2127  	}
  2128  }
  2129  
  2130  func TestAcceptedError(t *testing.T) {
  2131  	a := &AcceptedError{}
  2132  	if got, want := a.Error(), "try again later"; !strings.Contains(got, want) {
  2133  		t.Errorf("AcceptedError = %q, want %q", got, want)
  2134  	}
  2135  }
  2136  
  2137  func TestAbuseRateLimitError(t *testing.T) {
  2138  	u, err := url.Parse("https://example.com")
  2139  	if err != nil {
  2140  		t.Fatal(err)
  2141  	}
  2142  
  2143  	r := &AbuseRateLimitError{
  2144  		Response: &http.Response{
  2145  			Request:    &http.Request{Method: "PUT", URL: u},
  2146  			StatusCode: http.StatusTooManyRequests,
  2147  		},
  2148  		Message: "<msg>",
  2149  	}
  2150  	if got, want := r.Error(), "PUT https://example.com: 429 <msg>"; got != want {
  2151  		t.Errorf("AbuseRateLimitError = %q, want %q", got, want)
  2152  	}
  2153  }
  2154  
  2155  func TestAddOptions_QueryValues(t *testing.T) {
  2156  	if _, err := addOptions("yo", ""); err == nil {
  2157  		t.Error("addOptions err = nil, want error")
  2158  	}
  2159  }
  2160  
  2161  func TestBareDo_returnsOpenBody(t *testing.T) {
  2162  	client, mux, _, teardown := setup()
  2163  	defer teardown()
  2164  
  2165  	expectedBody := "Hello from the other side !"
  2166  
  2167  	mux.HandleFunc("/test-url", func(w http.ResponseWriter, r *http.Request) {
  2168  		testMethod(t, r, "GET")
  2169  		fmt.Fprint(w, expectedBody)
  2170  	})
  2171  
  2172  	ctx := context.Background()
  2173  	req, err := client.NewRequest("GET", "test-url", nil)
  2174  	if err != nil {
  2175  		t.Fatalf("client.NewRequest returned error: %v", err)
  2176  	}
  2177  
  2178  	resp, err := client.BareDo(ctx, req)
  2179  	if err != nil {
  2180  		t.Fatalf("client.BareDo returned error: %v", err)
  2181  	}
  2182  
  2183  	got, err := ioutil.ReadAll(resp.Body)
  2184  	if err != nil {
  2185  		t.Fatalf("ioutil.ReadAll returned error: %v", err)
  2186  	}
  2187  	if string(got) != expectedBody {
  2188  		t.Fatalf("Expected %q, got %q", expectedBody, string(got))
  2189  	}
  2190  	if err := resp.Body.Close(); err != nil {
  2191  		t.Fatalf("resp.Body.Close() returned error: %v", err)
  2192  	}
  2193  }
  2194  
  2195  // roundTripperFunc creates a mock RoundTripper (transport)
  2196  type roundTripperFunc func(*http.Request) (*http.Response, error)
  2197  
  2198  func (fn roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
  2199  	return fn(r)
  2200  }
  2201  
  2202  func TestErrorResponse_Marshal(t *testing.T) {
  2203  	testJSONMarshal(t, &ErrorResponse{}, "{}")
  2204  
  2205  	u := &ErrorResponse{
  2206  		Message: "msg",
  2207  		Errors: []Error{
  2208  			{
  2209  				Resource: "res",
  2210  				Field:    "f",
  2211  				Code:     "c",
  2212  				Message:  "msg",
  2213  			},
  2214  		},
  2215  		Block: &ErrorBlock{
  2216  			Reason:    "reason",
  2217  			CreatedAt: &Timestamp{referenceTime},
  2218  		},
  2219  		DocumentationURL: "doc",
  2220  	}
  2221  
  2222  	want := `{
  2223  		"message": "msg",
  2224  		"errors": [
  2225  			{
  2226  				"resource": "res",
  2227  				"field": "f",
  2228  				"code": "c",
  2229  				"message": "msg"
  2230  			}
  2231  		],
  2232  		"block": {
  2233  			"reason": "reason",
  2234  			"created_at": ` + referenceTimeStr + `
  2235  		},
  2236  		"documentation_url": "doc"
  2237  	}`
  2238  
  2239  	testJSONMarshal(t, u, want)
  2240  }
  2241  
  2242  func TestErrorBlock_Marshal(t *testing.T) {
  2243  	testJSONMarshal(t, &ErrorBlock{}, "{}")
  2244  
  2245  	u := &ErrorBlock{
  2246  		Reason:    "reason",
  2247  		CreatedAt: &Timestamp{referenceTime},
  2248  	}
  2249  
  2250  	want := `{
  2251  		"reason": "reason",
  2252  		"created_at": ` + referenceTimeStr + `
  2253  	}`
  2254  
  2255  	testJSONMarshal(t, u, want)
  2256  }
  2257  
  2258  func TestRateLimitError_Marshal(t *testing.T) {
  2259  	testJSONMarshal(t, &RateLimitError{}, "{}")
  2260  
  2261  	u := &RateLimitError{
  2262  		Rate: Rate{
  2263  			Limit:     1,
  2264  			Remaining: 1,
  2265  			Reset:     Timestamp{referenceTime},
  2266  		},
  2267  		Message: "msg",
  2268  	}
  2269  
  2270  	want := `{
  2271  		"Rate": {
  2272  			"limit": 1,
  2273  			"remaining": 1,
  2274  			"reset": ` + referenceTimeStr + `
  2275  		},
  2276  		"message": "msg"
  2277  	}`
  2278  
  2279  	testJSONMarshal(t, u, want)
  2280  }
  2281  
  2282  func TestAbuseRateLimitError_Marshal(t *testing.T) {
  2283  	testJSONMarshal(t, &AbuseRateLimitError{}, "{}")
  2284  
  2285  	u := &AbuseRateLimitError{
  2286  		Message: "msg",
  2287  	}
  2288  
  2289  	want := `{
  2290  		"message": "msg"
  2291  	}`
  2292  
  2293  	testJSONMarshal(t, u, want)
  2294  }
  2295  
  2296  func TestError_Marshal(t *testing.T) {
  2297  	testJSONMarshal(t, &Error{}, "{}")
  2298  
  2299  	u := &Error{
  2300  		Resource: "res",
  2301  		Field:    "field",
  2302  		Code:     "code",
  2303  		Message:  "msg",
  2304  	}
  2305  
  2306  	want := `{
  2307  		"resource": "res",
  2308  		"field": "field",
  2309  		"code": "code",
  2310  		"message": "msg"
  2311  	}`
  2312  
  2313  	testJSONMarshal(t, u, want)
  2314  }
  2315  
  2316  func TestRate_Marshal(t *testing.T) {
  2317  	testJSONMarshal(t, &Rate{}, "{}")
  2318  
  2319  	u := &Rate{
  2320  		Limit:     1,
  2321  		Remaining: 1,
  2322  		Reset:     Timestamp{referenceTime},
  2323  	}
  2324  
  2325  	want := `{
  2326  		"limit": 1,
  2327  		"remaining": 1,
  2328  		"reset": ` + referenceTimeStr + `
  2329  	}`
  2330  
  2331  	testJSONMarshal(t, u, want)
  2332  }
  2333  
  2334  func TestRateLimits_Marshal(t *testing.T) {
  2335  	testJSONMarshal(t, &RateLimits{}, "{}")
  2336  
  2337  	u := &RateLimits{
  2338  		Core: &Rate{
  2339  			Limit:     1,
  2340  			Remaining: 1,
  2341  			Reset:     Timestamp{referenceTime},
  2342  		},
  2343  		Search: &Rate{
  2344  			Limit:     1,
  2345  			Remaining: 1,
  2346  			Reset:     Timestamp{referenceTime},
  2347  		},
  2348  	}
  2349  
  2350  	want := `{
  2351  		"core": {
  2352  			"limit": 1,
  2353  			"remaining": 1,
  2354  			"reset": ` + referenceTimeStr + `
  2355  		},
  2356  		"search": {
  2357  			"limit": 1,
  2358  			"remaining": 1,
  2359  			"reset": ` + referenceTimeStr + `
  2360  		}
  2361  	}`
  2362  
  2363  	testJSONMarshal(t, u, want)
  2364  }
  2365  
  2366  func TestParseTokenExpiration(t *testing.T) {
  2367  	tests := []struct {
  2368  		header string
  2369  		want   Timestamp
  2370  	}{
  2371  		{
  2372  			header: "",
  2373  			want:   Timestamp{},
  2374  		},
  2375  		{
  2376  			header: "this is a garbage",
  2377  			want:   Timestamp{},
  2378  		},
  2379  		{
  2380  			header: "2021-09-03 02:34:04 UTC",
  2381  			want:   Timestamp{time.Date(2021, time.September, 3, 2, 34, 4, 0, time.UTC)},
  2382  		},
  2383  	}
  2384  
  2385  	for _, tt := range tests {
  2386  		res := &http.Response{
  2387  			Request: &http.Request{},
  2388  			Header:  http.Header{},
  2389  		}
  2390  
  2391  		res.Header.Set(headerTokenExpiration, tt.header)
  2392  		exp := parseTokenExpiration(res)
  2393  		if !exp.Equal(tt.want) {
  2394  			t.Errorf("parseTokenExpiration returned %#v, want %#v", exp, tt.want)
  2395  		}
  2396  	}
  2397  }