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  }