github.com/google/go-github/v53@v53.2.0/github/github_test.go (about)

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