sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/github/client_test.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package github
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/rsa"
    23  	"crypto/tls"
    24  	"encoding/base64"
    25  	"encoding/json"
    26  	"errors"
    27  	"fmt"
    28  	"io"
    29  	"net/http"
    30  	"net/http/httptest"
    31  	"net/url"
    32  	"reflect"
    33  	"strconv"
    34  	"strings"
    35  	"testing"
    36  	"time"
    37  
    38  	"github.com/google/go-cmp/cmp"
    39  	"github.com/shurcooL/githubv4"
    40  	"github.com/sirupsen/logrus"
    41  	"k8s.io/apimachinery/pkg/util/sets"
    42  	"k8s.io/utils/diff"
    43  
    44  	"sigs.k8s.io/prow/pkg/throttle"
    45  	"sigs.k8s.io/prow/pkg/version"
    46  )
    47  
    48  type testTime struct {
    49  	now   time.Time
    50  	slept time.Duration
    51  }
    52  
    53  func (tt *testTime) Sleep(d time.Duration) {
    54  	tt.slept = d
    55  }
    56  func (tt *testTime) Until(t time.Time) time.Duration {
    57  	return t.Sub(tt.now)
    58  }
    59  
    60  func getClient(url string) *client {
    61  	getToken := func() []byte {
    62  		return []byte("")
    63  	}
    64  
    65  	logger := logrus.New()
    66  	logger.SetLevel(logrus.DebugLevel)
    67  	c := &client{
    68  		logger: logrus.NewEntry(logger),
    69  		delegate: &delegate{
    70  			time:     &testTime{},
    71  			throttle: ghThrottler{Throttler: &throttle.Throttler{}},
    72  			getToken: getToken,
    73  			censor: func(content []byte) []byte {
    74  				return content
    75  			},
    76  			client: &http.Client{
    77  				Transport: &http.Transport{
    78  					TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    79  				},
    80  			},
    81  			bases:         []string{url},
    82  			maxRetries:    DefaultMaxRetries,
    83  			max404Retries: DefaultMax404Retries,
    84  			initialDelay:  DefaultInitialDelay,
    85  			maxSleepTime:  DefaultMaxSleepTime,
    86  		},
    87  	}
    88  	c.wrapThrottler()
    89  	return c
    90  }
    91  
    92  func TestRequestRateLimit(t *testing.T) {
    93  	tc := &testTime{now: time.Now()}
    94  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    95  		if tc.slept == 0 {
    96  			w.Header().Set("X-RateLimit-Remaining", "0")
    97  			w.Header().Set("X-RateLimit-Reset", strconv.Itoa(int(tc.now.Add(time.Second).Unix())))
    98  			http.Error(w, "403 Forbidden", http.StatusForbidden)
    99  		}
   100  	}))
   101  	defer ts.Close()
   102  	c := getClient(ts.URL)
   103  	c.time = tc
   104  	resp, err := c.requestRetry(http.MethodGet, "/", "", "", nil)
   105  	if err != nil {
   106  		t.Errorf("Error from request: %v", err)
   107  	} else if resp.StatusCode != 200 {
   108  		t.Errorf("Expected status code 200, got %d", resp.StatusCode)
   109  	} else if tc.slept < time.Second {
   110  		t.Errorf("Expected to sleep for at least a second, got %v", tc.slept)
   111  	}
   112  }
   113  
   114  func TestAbuseRateLimit(t *testing.T) {
   115  	tc := &testTime{now: time.Now()}
   116  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   117  		if tc.slept == 0 {
   118  			w.Header().Set("Retry-After", "1")
   119  			http.Error(w, "403 Forbidden", http.StatusForbidden)
   120  		}
   121  	}))
   122  	defer ts.Close()
   123  	c := getClient(ts.URL)
   124  	c.time = tc
   125  	resp, err := c.requestRetry(http.MethodGet, "/", "", "", nil)
   126  	if err != nil {
   127  		t.Errorf("Error from request: %v", err)
   128  	} else if resp.StatusCode != 200 {
   129  		t.Errorf("Expected status code 200, got %d", resp.StatusCode)
   130  	} else if tc.slept < time.Second {
   131  		t.Errorf("Expected to sleep for at least a second, got %v", tc.slept)
   132  	}
   133  }
   134  
   135  func TestRetry404(t *testing.T) {
   136  	tc := &testTime{now: time.Now()}
   137  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   138  		if tc.slept == 0 {
   139  			http.Error(w, "404 Not Found", http.StatusNotFound)
   140  		}
   141  	}))
   142  	defer ts.Close()
   143  	c := getClient(ts.URL)
   144  	c.time = tc
   145  	resp, err := c.requestRetry(http.MethodGet, "/", "", "", nil)
   146  	if err != nil {
   147  		t.Errorf("Error from request: %v", err)
   148  	} else if resp.StatusCode != 200 {
   149  		t.Errorf("Expected status code 200, got %d", resp.StatusCode)
   150  	}
   151  }
   152  
   153  func TestIncorrectOAuthScopes(t *testing.T) {
   154  	testCases := []struct {
   155  		name                string
   156  		acceptedOAuthScopes string
   157  		oauthScopes         string
   158  		expectedErr         string
   159  	}{
   160  		{
   161  			name:                "no overlapping OAuth scopes",
   162  			acceptedOAuthScopes: "admin:org,repo",
   163  			oauthScopes:         "admin:repo_hook,workflow",
   164  			expectedErr:         "the account is using admin:repo_hook,workflow oauth scopes, please make sure you are using at least one of the following oauth scopes: admin:org,repo",
   165  		},
   166  		{
   167  			name:                "empty OAuth scopes",
   168  			acceptedOAuthScopes: "admin:org,repo",
   169  			oauthScopes:         "",
   170  			expectedErr:         "the account is using no oauth scopes, please make sure you are using at least one of the following oauth scopes: admin:org,repo",
   171  		},
   172  		{
   173  			name:                "empty accepted OAuth scopes",
   174  			acceptedOAuthScopes: "",
   175  			oauthScopes:         "",
   176  			expectedErr:         "the GitHub API request returns a 403 error: 403 Forbidden\n",
   177  		},
   178  	}
   179  	for _, tc := range testCases {
   180  		tt := &testTime{now: time.Now()}
   181  		ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   182  			w.Header().Set("X-Accepted-OAuth-Scopes", tc.acceptedOAuthScopes)
   183  			w.Header().Set("X-OAuth-Scopes", tc.oauthScopes)
   184  			http.Error(w, "403 Forbidden", http.StatusForbidden)
   185  		}))
   186  		defer ts.Close()
   187  		c := getClient(ts.URL)
   188  		c.time = tt
   189  		_, err := c.requestRetry(http.MethodGet, "/", "", "", nil)
   190  		if err == nil {
   191  			t.Error("Expected an error from a request with incorrect OAuth scopes, but succeeded!?")
   192  		} else if diff := cmp.Diff(err.Error(), tc.expectedErr); diff != "" {
   193  			t.Errorf("Unexpected error message: %s", diff)
   194  		}
   195  	}
   196  }
   197  
   198  func TestUnparsable403Error(t *testing.T) {
   199  	tt := &testTime{now: time.Now()}
   200  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   201  		w.Header().Set("X-Accepted-OAuth-Scopes", "admin:org,repo")
   202  		w.Header().Set("X-OAuth-Scopes", "repo")
   203  		http.Error(w, "403 Forbidden", http.StatusForbidden)
   204  	}))
   205  	defer ts.Close()
   206  	c := getClient(ts.URL)
   207  	c.time = tt
   208  	_, err := c.requestRetry(http.MethodGet, "/", "", "", nil)
   209  	if err == nil {
   210  		t.Error("Expected an error from a request that can cause a 403 error, but succeeded!?")
   211  	}
   212  }
   213  
   214  func TestRetryBase(t *testing.T) {
   215  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
   216  	defer ts.Close()
   217  	c := getClient(ts.URL)
   218  	c.initialDelay = time.Microsecond
   219  	// One good endpoint:
   220  	c.bases = []string{c.bases[0]}
   221  	resp, err := c.requestRetry(http.MethodGet, "/", "", "", nil)
   222  	if err != nil {
   223  		t.Errorf("Error from request: %v", err)
   224  	} else if resp.StatusCode != 200 {
   225  		t.Errorf("Expected status code 200, got %d", resp.StatusCode)
   226  	}
   227  	// Bad endpoint followed by good endpoint:
   228  	c.bases = []string{"not-a-valid-base", c.bases[0]}
   229  	resp, err = c.requestRetry(http.MethodGet, "/", "", "", nil)
   230  	if err != nil {
   231  		t.Errorf("Error from request: %v", err)
   232  	} else if resp.StatusCode != 200 {
   233  		t.Errorf("Expected status code 200, got %d", resp.StatusCode)
   234  	}
   235  	// One bad endpoint:
   236  	c.bases = []string{"not-a-valid-base"}
   237  	_, err = c.requestRetry(http.MethodGet, "/", "", "", nil)
   238  	if err == nil {
   239  		t.Error("Expected an error from a request to an invalid base, but succeeded!?")
   240  	}
   241  }
   242  
   243  func TestIsMember(t *testing.T) {
   244  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   245  		if r.Method != http.MethodGet {
   246  			t.Errorf("Bad method: %s", r.Method)
   247  		}
   248  		if r.URL.Path != "/orgs/k8s/members/person" {
   249  			t.Errorf("Bad request path: %s", r.URL.Path)
   250  		}
   251  		http.Error(w, "204 No Content", http.StatusNoContent)
   252  	}))
   253  	defer ts.Close()
   254  	c := getClient(ts.URL)
   255  	mem, err := c.IsMember("k8s", "person")
   256  	if err != nil {
   257  		t.Errorf("Didn't expect error: %v", err)
   258  	} else if !mem {
   259  		t.Errorf("Should be member.")
   260  	}
   261  }
   262  
   263  func TestCreateComment(t *testing.T) {
   264  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   265  		if r.Method != http.MethodPost {
   266  			t.Errorf("Bad method: %s", r.Method)
   267  		}
   268  		if r.URL.Path != "/repos/k8s/kuber/issues/5/comments" {
   269  			t.Errorf("Bad request path: %s", r.URL.Path)
   270  		}
   271  		b, err := io.ReadAll(r.Body)
   272  		if err != nil {
   273  			t.Fatalf("Could not read request body: %v", err)
   274  		}
   275  		var ic IssueComment
   276  		if err := json.Unmarshal(b, &ic); err != nil {
   277  			t.Errorf("Could not unmarshal request: %v", err)
   278  		} else if ic.Body != "hello" {
   279  			t.Errorf("Wrong body: %s", ic.Body)
   280  		}
   281  		http.Error(w, "201 Created", http.StatusCreated)
   282  	}))
   283  	defer ts.Close()
   284  	c := getClient(ts.URL)
   285  	if err := c.CreateComment("k8s", "kuber", 5, "hello"); err != nil {
   286  		t.Errorf("Didn't expect error: %v", err)
   287  	}
   288  }
   289  
   290  func TestCreateCommentCensored(t *testing.T) {
   291  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   292  		if r.Method != http.MethodPost {
   293  			t.Errorf("Bad method: %s", r.Method)
   294  		}
   295  		if r.URL.Path != "/repos/k8s/kuber/issues/5/comments" {
   296  			t.Errorf("Bad request path: %s", r.URL.Path)
   297  		}
   298  		b, err := io.ReadAll(r.Body)
   299  		if err != nil {
   300  			t.Fatalf("Could not read request body: %v", err)
   301  		}
   302  		var ic IssueComment
   303  		if err := json.Unmarshal(b, &ic); err != nil {
   304  			t.Errorf("Could not unmarshal request: %v", err)
   305  		} else if ic.Body != "CENSORED" {
   306  			t.Errorf("Wrong body: %s", ic.Body)
   307  		}
   308  		http.Error(w, "201 Created", http.StatusCreated)
   309  	}))
   310  	defer ts.Close()
   311  	c := getClient(ts.URL)
   312  	c.delegate.censor = func(content []byte) []byte {
   313  		return bytes.ReplaceAll(content, []byte("hello"), []byte("CENSORED"))
   314  	}
   315  	if err := c.CreateComment("k8s", "kuber", 5, "hello"); err != nil {
   316  		t.Errorf("Didn't expect error: %v", err)
   317  	}
   318  }
   319  
   320  func TestCreateCommentReaction(t *testing.T) {
   321  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   322  		if r.Method != http.MethodPost {
   323  			t.Errorf("Bad method: %s", r.Method)
   324  		}
   325  		if r.URL.Path != "/repos/k8s/kuber/issues/comments/5/reactions" {
   326  			t.Errorf("Bad request path: %s", r.URL.Path)
   327  		}
   328  		if r.Header.Get("Accept") != "application/vnd.github.squirrel-girl-preview" {
   329  			t.Errorf("Bad Accept header: %s", r.Header.Get("Accept"))
   330  		}
   331  		http.Error(w, "201 Created", http.StatusCreated)
   332  	}))
   333  	defer ts.Close()
   334  	c := getClient(ts.URL)
   335  	if err := c.CreateCommentReaction("k8s", "kuber", 5, "+1"); err != nil {
   336  		t.Errorf("Didn't expect error: %v", err)
   337  	}
   338  }
   339  
   340  func TestDeleteComment(t *testing.T) {
   341  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   342  		if r.Method != http.MethodDelete {
   343  			t.Errorf("Bad method: %s", r.Method)
   344  		}
   345  		if r.URL.Path != "/repos/k8s/kuber/issues/comments/123" {
   346  			t.Errorf("Bad request path: %s", r.URL.Path)
   347  		}
   348  		http.Error(w, "204 No Content", http.StatusNoContent)
   349  	}))
   350  	defer ts.Close()
   351  	c := getClient(ts.URL)
   352  	if err := c.DeleteComment("k8s", "kuber", 123); err != nil {
   353  		t.Errorf("Didn't expect error: %v", err)
   354  	}
   355  }
   356  
   357  func TestGetPullRequest(t *testing.T) {
   358  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   359  		if r.Method != http.MethodGet {
   360  			t.Errorf("Bad method: %s", r.Method)
   361  		}
   362  		if r.URL.Path != "/repos/k8s/kuber/pulls/12" {
   363  			t.Errorf("Bad request path: %s", r.URL.Path)
   364  		}
   365  		pr := PullRequest{
   366  			User: User{Login: "bla"},
   367  		}
   368  		b, err := json.Marshal(&pr)
   369  		if err != nil {
   370  			t.Fatalf("Didn't expect error: %v", err)
   371  		}
   372  		fmt.Fprint(w, string(b))
   373  	}))
   374  	defer ts.Close()
   375  	c := getClient(ts.URL)
   376  	pr, err := c.GetPullRequest("k8s", "kuber", 12)
   377  	if err != nil {
   378  		t.Errorf("Didn't expect error: %v", err)
   379  	} else if pr.User.Login != "bla" {
   380  		t.Errorf("Wrong user: %s", pr.User.Login)
   381  	}
   382  }
   383  
   384  func TestGetPullRequestChanges(t *testing.T) {
   385  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   386  		if r.Method != http.MethodGet {
   387  			t.Errorf("Bad method: %s", r.Method)
   388  		}
   389  		if r.URL.Path != "/repos/k8s/kuber/pulls/12/files" {
   390  			t.Errorf("Bad request path: %s", r.URL.Path)
   391  		}
   392  		changes := []PullRequestChange{
   393  			{Filename: "foo.txt"},
   394  		}
   395  		b, err := json.Marshal(&changes)
   396  		if err != nil {
   397  			t.Fatalf("Didn't expect error: %v", err)
   398  		}
   399  		fmt.Fprint(w, string(b))
   400  	}))
   401  	defer ts.Close()
   402  	c := getClient(ts.URL)
   403  	cs, err := c.GetPullRequestChanges("k8s", "kuber", 12)
   404  	if err != nil {
   405  		t.Errorf("Didn't expect error: %v", err)
   406  	}
   407  	if len(cs) != 1 || cs[0].Filename != "foo.txt" {
   408  		t.Errorf("Wrong result: %#v", cs)
   409  	}
   410  }
   411  
   412  func TestGetRef(t *testing.T) {
   413  	testCases := []struct {
   414  		name              string
   415  		githubResponse    []byte
   416  		expectedSHA       string
   417  		expectedError     string
   418  		expectedErrorType error
   419  	}{
   420  		{
   421  			name:           "single ref",
   422  			githubResponse: []byte(`{"object": {"sha":"abcde"}}`),
   423  			expectedSHA:    "abcde",
   424  		},
   425  		{
   426  			name:           "unexpected response to trigger an error",
   427  			githubResponse: []byte(`malformed json`),
   428  			expectedError:  "invalid character 'm' looking for beginning of value",
   429  		},
   430  		{
   431  			name: "multiple refs, no match",
   432  			githubResponse: []byte(`
   433  [
   434    {
   435      "ref": "refs/heads/feature-a",
   436      "node_id": "MDM6UmVmcmVmcy9oZWFkcy9mZWF0dXJlLWE=",
   437      "url": "https://api.github.com/repos/octocat/Hello-World/git/refs/heads/feature-a",
   438      "object": {
   439        "type": "commit",
   440        "sha": "aa218f56b14c9653891f9e74264a383fa43fefbd",
   441        "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/aa218f56b14c9653891f9e74264a383fa43fefbd"
   442      }
   443    },
   444    {
   445      "ref": "refs/heads/feature-b",
   446      "node_id": "MDM6UmVmcmVmcy9oZWFkcy9mZWF0dXJlLWI=",
   447      "url": "https://api.github.com/repos/octocat/Hello-World/git/refs/heads/feature-b",
   448      "object": {
   449        "type": "commit",
   450        "sha": "612077ae6dffb4d2fbd8ce0cccaa58893b07b5ac",
   451        "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/612077ae6dffb4d2fbd8ce0cccaa58893b07b5ac"
   452      }
   453    }
   454  ]`),
   455  			expectedError:     "query for org/repo ref \"heads/branch\" didn't match one but multiple refs: [refs/heads/feature-a refs/heads/feature-b]",
   456  			expectedErrorType: GetRefTooManyResultsError{},
   457  		},
   458  		{
   459  			name: "multiple refs with match",
   460  			githubResponse: []byte(`
   461  [
   462    {
   463      "ref": "refs/heads/branch",
   464      "node_id": "MDM6UmVmcmVmcy9oZWFkcy9mZWF0dXJlLWE=",
   465      "url": "https://api.github.com/repos/octocat/Hello-World/git/refs/heads/feature-a",
   466      "object": {
   467        "type": "commit",
   468        "sha": "aa218f56b14c9653891f9e74264a383fa43fefbd",
   469        "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/aa218f56b14c9653891f9e74264a383fa43fefbd"
   470      }
   471    },
   472    {
   473      "ref": "refs/heads/feature-b",
   474      "node_id": "MDM6UmVmcmVmcy9oZWFkcy9mZWF0dXJlLWI=",
   475      "url": "https://api.github.com/repos/octocat/Hello-World/git/refs/heads/feature-b",
   476      "object": {
   477        "type": "commit",
   478        "sha": "612077ae6dffb4d2fbd8ce0cccaa58893b07b5ac",
   479        "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/612077ae6dffb4d2fbd8ce0cccaa58893b07b5ac"
   480      }
   481    }
   482  ]`),
   483  			expectedSHA: "aa218f56b14c9653891f9e74264a383fa43fefbd",
   484  		},
   485  	}
   486  
   487  	for _, tc := range testCases {
   488  		t.Run(tc.name, func(t *testing.T) {
   489  			ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   490  				w.WriteHeader(200)
   491  				if r.Method != http.MethodGet {
   492  					t.Errorf("Bad method: %s", r.Method)
   493  				}
   494  				expectedPath := "/repos/org/repo/git/refs/heads/branch"
   495  				if r.URL.Path != expectedPath {
   496  					t.Errorf("expected path %s, got path %s", expectedPath, r.URL.Path)
   497  				}
   498  				w.Write(tc.githubResponse)
   499  			}))
   500  			defer ts.Close()
   501  
   502  			c := getClient(ts.URL)
   503  			var errMsg string
   504  			sha, err := c.GetRef("org", "repo", "heads/branch")
   505  			if err != nil {
   506  				errMsg = err.Error()
   507  			}
   508  			if errMsg != tc.expectedError {
   509  				t.Fatalf("expected error %q, got error %q", tc.expectedError, err)
   510  			}
   511  
   512  			// skip checking the error type for the case
   513  			// because the actual type is json.SyntaxError that does not provide the Is method
   514  			// and it is hard to raise other type of errors for the test
   515  			if tc.name != "unexpected response to trigger an error" {
   516  				if !errors.Is(err, tc.expectedErrorType) {
   517  					t.Errorf("expected error of type %T, got %T", tc.expectedErrorType, err)
   518  				}
   519  			}
   520  			if sha != tc.expectedSHA {
   521  				t.Errorf("expected sha %q, got sha %q", tc.expectedSHA, sha)
   522  			}
   523  		})
   524  	}
   525  }
   526  
   527  func TestDeleteRef(t *testing.T) {
   528  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   529  		if r.Method != http.MethodDelete {
   530  			t.Errorf("Bad method: %s", r.Method)
   531  		}
   532  		if r.URL.Path != "/repos/k8s/kuber/git/refs/heads/my-feature" {
   533  			t.Errorf("Bad request path: %s", r.URL.Path)
   534  		}
   535  		http.Error(w, "204 No Content", http.StatusNoContent)
   536  	}))
   537  	defer ts.Close()
   538  	c := getClient(ts.URL)
   539  	if err := c.DeleteRef("k8s", "kuber", "heads/my-feature"); err != nil {
   540  		t.Errorf("Didn't expect error: %v", err)
   541  	}
   542  }
   543  
   544  func TestListFileCommits(t *testing.T) {
   545  	githubResponse := []byte(`
   546  [
   547    {
   548      "sha": "5833e02133690c6d608f66ef369e85865ede51de",
   549      "node_id": "MDY6Q29tbWl0Mjk2ODI0MjU5OjU4MzNlMDIxMzM2OTBjNmQ2MDhmNjZlZjM2OWU4NTg2NWVkZTUxZGU=",
   550      "commit": {
   551        "author": {
   552          "name": "Rustin Liu",
   553          "email": "rustin.liu@gmail.com",
   554          "date": "2021-01-17T15:29:04Z"
   555        },
   556        "committer": {
   557          "name": "GitHub",
   558          "email": "noreply@github.com",
   559          "date": "2021-01-17T15:29:04Z"
   560        },
   561        "message": "chore: update README.md (#281)\n\n* chore: update README.md\r\n\r\n* chore: update README.md",
   562        "tree": {
   563          "sha": "0cbce1df534461fb686a4d97f7e1549657f45594",
   564          "url": "https://api.github.com/repos/ti-community-infra/tichi/git/trees/0cbce1df534461fb686a4d97f7e1549657f45594"
   565        },
   566        "url": "https://api.github.com/repos/ti-community-infra/tichi/git/commits/5833e02133690c6d608f66ef369e85865ede51de",
   567        "comment_count": 0,
   568        "verification": {
   569          "verified": true,
   570          "reason": "valid",
   571          "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJgBFfACRBK7hj4Ov3rIwAAdHIIAAdRO4WoBZPAcLREqPuSPX+h\nM1CpnIyytSoF8QesyCffLkCWbFwswMhPLM4aXW55EeSZKeEZyghb0Ehz0ZN1b3Zx\nJzFaHeydih2S5rTFk6MCn8ZY1oSZuA3spauqEJ8RxAoaHSmZ+Zq5ykQ9qar4rLto\n3LgpMkr+z137cTfeJ5iUQZPih8AsTS3/YAmUtPLMOanNKLtMDfD1xVj4luOqXz6X\nV0UFwQs/F+4HDvVAnwmh3soMxrKZ+ZOcSAGZYP6EjR75gaUy4EmRNUkVQxxNbJ11\nY4LV0j7ShFsRPQrSfBByhKL0Ug7uAiHGLGYCxW1wkULg4hArklS0YFFfuvZwhws=\n=ujFx\n-----END PGP SIGNATURE-----\n",
   572          "payload": "tree 0cbce1df534461fb686a4d97f7e1549657f45594\nparent 9e00ae5d353eb520b58a7440757f9d715572009f\nauthor Rustin Liu <rustin.liu@gmail.com> 1610897344 +0800\ncommitter GitHub <noreply@github.com> 1610897344 +0800\n\nchore: update README.md (#281)\n\n* chore: update README.md\r\n\r\n* chore: update README.md"
   573        }
   574      },
   575      "url": "https://api.github.com/repos/ti-community-infra/tichi/commits/5833e02133690c6d608f66ef369e85865ede51de",
   576      "html_url": "https://github.com/ti-community-infra/tichi/commit/5833e02133690c6d608f66ef369e85865ede51de",
   577      "comments_url": "https://api.github.com/repos/ti-community-infra/tichi/commits/5833e02133690c6d608f66ef369e85865ede51de/comments",
   578      "author": {
   579        "login": "hi-rustin",
   580        "id": 29879298,
   581        "node_id": "MDQ6VXNlcjI5ODc5Mjk4",
   582        "avatar_url": "https://avatars.githubusercontent.com/u/29879298?v=4",
   583        "gravatar_id": "",
   584        "url": "https://api.github.com/users/hi-rustin",
   585        "html_url": "https://github.com/hi-rustin",
   586        "followers_url": "https://api.github.com/users/hi-rustin/followers",
   587        "following_url": "https://api.github.com/users/hi-rustin/following{/other_user}",
   588        "gists_url": "https://api.github.com/users/hi-rustin/gists{/gist_id}",
   589        "starred_url": "https://api.github.com/users/hi-rustin/starred{/owner}{/repo}",
   590        "subscriptions_url": "https://api.github.com/users/hi-rustin/subscriptions",
   591        "organizations_url": "https://api.github.com/users/hi-rustin/orgs",
   592        "repos_url": "https://api.github.com/users/hi-rustin/repos",
   593        "events_url": "https://api.github.com/users/hi-rustin/events{/privacy}",
   594        "received_events_url": "https://api.github.com/users/hi-rustin/received_events",
   595        "type": "User",
   596        "site_admin": false
   597      },
   598      "committer": {
   599        "login": "web-flow",
   600        "id": 19864447,
   601        "node_id": "MDQ6VXNlcjE5ODY0NDQ3",
   602        "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4",
   603        "gravatar_id": "",
   604        "url": "https://api.github.com/users/web-flow",
   605        "html_url": "https://github.com/web-flow",
   606        "followers_url": "https://api.github.com/users/web-flow/followers",
   607        "following_url": "https://api.github.com/users/web-flow/following{/other_user}",
   608        "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}",
   609        "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}",
   610        "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions",
   611        "organizations_url": "https://api.github.com/users/web-flow/orgs",
   612        "repos_url": "https://api.github.com/users/web-flow/repos",
   613        "events_url": "https://api.github.com/users/web-flow/events{/privacy}",
   614        "received_events_url": "https://api.github.com/users/web-flow/received_events",
   615        "type": "User",
   616        "site_admin": false
   617      },
   618      "parents": [
   619        {
   620          "sha": "9e00ae5d353eb520b58a7440757f9d715572009f",
   621          "url": "https://api.github.com/repos/ti-community-infra/tichi/commits/9e00ae5d353eb520b58a7440757f9d715572009f",
   622          "html_url": "https://github.com/ti-community-infra/tichi/commit/9e00ae5d353eb520b58a7440757f9d715572009f"
   623        }
   624      ]
   625    },
   626    {
   627      "sha": "68af84c32436c16564e1ac3c6ac36090d5d0baee",
   628      "node_id": "MDY6Q29tbWl0Mjk2ODI0MjU5OjY4YWY4NGMzMjQzNmMxNjU2NGUxYWMzYzZhYzM2MDkwZDVkMGJhZWU=",
   629      "commit": {
   630        "author": {
   631          "name": "Rustin Liu",
   632          "email": "rustin.liu@gmail.com",
   633          "date": "2021-01-14T08:34:14Z"
   634        },
   635        "committer": {
   636          "name": "GitHub",
   637          "email": "noreply@github.com",
   638          "date": "2021-01-14T08:34:14Z"
   639        },
   640        "message": "chore: rename project (#265)",
   641        "tree": {
   642          "sha": "853d8d79ab3fe498fcb415fb71ac8901de0272df",
   643          "url": "https://api.github.com/repos/ti-community-infra/tichi/git/trees/853d8d79ab3fe498fcb415fb71ac8901de0272df"
   644        },
   645        "url": "https://api.github.com/repos/ti-community-infra/tichi/git/commits/68af84c32436c16564e1ac3c6ac36090d5d0baee",
   646        "comment_count": 0,
   647        "verification": {
   648          "verified": true,
   649          "reason": "valid",
   650          "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJgAAIGCRBK7hj4Ov3rIwAAdHIIAFdNBdgiG48GtiSXpbwXCpiq\nLTvCiJEkoRsuggNKlhvXvt3xEeVki8T0WcrKY70mkdNA11ie9PXdHLSowGyFYFRS\n9FwEUBKLBTYIyTpgvuBcUb17/M3QnobmIF1X66T/vxnqy8xvny6kRUk8qsxhLi6K\n5v61mHt3J5F+DwFhVaUVniMnUnQTdW+o9Utd8zEkKbT2pJkvi6cSAiQK6RqIBD7l\nZTBWgKtvrk75u1xBfqcTRRe00qmJdW+OmgPIhRKP9PGRLOrHUeLBs8Ov1YaSBa08\njd92057tt8tigiQBBgo6cTMlK0tupIf+YS5es3eNNVYkEdfxeZ8fRgwghfOLNAQ=\n=5STI\n-----END PGP SIGNATURE-----\n",
   651          "payload": "tree 853d8d79ab3fe498fcb415fb71ac8901de0272df\nparent a17a9df826165b832476c13c5f93ed8e7b58f2ce\nauthor Rustin Liu <rustin.liu@gmail.com> 1610613254 +0800\ncommitter GitHub <noreply@github.com> 1610613254 +0800\n\nchore: rename project (#265)\n\n"
   652        }
   653      },
   654      "url": "https://api.github.com/repos/ti-community-infra/tichi/commits/68af84c32436c16564e1ac3c6ac36090d5d0baee",
   655      "html_url": "https://github.com/ti-community-infra/tichi/commit/68af84c32436c16564e1ac3c6ac36090d5d0baee",
   656      "comments_url": "https://api.github.com/repos/ti-community-infra/tichi/commits/68af84c32436c16564e1ac3c6ac36090d5d0baee/comments",
   657      "author": {
   658        "login": "hi-rustin",
   659        "id": 29879298,
   660        "node_id": "MDQ6VXNlcjI5ODc5Mjk4",
   661        "avatar_url": "https://avatars.githubusercontent.com/u/29879298?v=4",
   662        "gravatar_id": "",
   663        "url": "https://api.github.com/users/hi-rustin",
   664        "html_url": "https://github.com/hi-rustin",
   665        "followers_url": "https://api.github.com/users/hi-rustin/followers",
   666        "following_url": "https://api.github.com/users/hi-rustin/following{/other_user}",
   667        "gists_url": "https://api.github.com/users/hi-rustin/gists{/gist_id}",
   668        "starred_url": "https://api.github.com/users/hi-rustin/starred{/owner}{/repo}",
   669        "subscriptions_url": "https://api.github.com/users/hi-rustin/subscriptions",
   670        "organizations_url": "https://api.github.com/users/hi-rustin/orgs",
   671        "repos_url": "https://api.github.com/users/hi-rustin/repos",
   672        "events_url": "https://api.github.com/users/hi-rustin/events{/privacy}",
   673        "received_events_url": "https://api.github.com/users/hi-rustin/received_events",
   674        "type": "User",
   675        "site_admin": false
   676      },
   677      "committer": {
   678        "login": "web-flow",
   679        "id": 19864447,
   680        "node_id": "MDQ6VXNlcjE5ODY0NDQ3",
   681        "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4",
   682        "gravatar_id": "",
   683        "url": "https://api.github.com/users/web-flow",
   684        "html_url": "https://github.com/web-flow",
   685        "followers_url": "https://api.github.com/users/web-flow/followers",
   686        "following_url": "https://api.github.com/users/web-flow/following{/other_user}",
   687        "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}",
   688        "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}",
   689        "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions",
   690        "organizations_url": "https://api.github.com/users/web-flow/orgs",
   691        "repos_url": "https://api.github.com/users/web-flow/repos",
   692        "events_url": "https://api.github.com/users/web-flow/events{/privacy}",
   693        "received_events_url": "https://api.github.com/users/web-flow/received_events",
   694        "type": "User",
   695        "site_admin": false
   696      },
   697      "parents": [
   698        {
   699          "sha": "a17a9df826165b832476c13c5f93ed8e7b58f2ce",
   700          "url": "https://api.github.com/repos/ti-community-infra/tichi/commits/a17a9df826165b832476c13c5f93ed8e7b58f2ce",
   701          "html_url": "https://github.com/ti-community-infra/tichi/commit/a17a9df826165b832476c13c5f93ed8e7b58f2ce"
   702        }
   703      ]
   704    }
   705  ]`)
   706  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   707  		w.WriteHeader(200)
   708  		if r.Method != http.MethodGet {
   709  			t.Errorf("Bad method: %s", r.Method)
   710  		}
   711  		expectedPath := "/repos/org/repo/commits"
   712  		if r.URL.Path != expectedPath {
   713  			t.Errorf("expected path %s, got path %s", expectedPath, r.URL.Path)
   714  		}
   715  		expectRequestURI := "/repos/org/repo/commits?path=README.md&per_page=100"
   716  		if r.URL.RequestURI() != expectRequestURI {
   717  			t.Errorf("expected request URI %s, got request URI %s", expectRequestURI, r.URL.RequestURI())
   718  		}
   719  		w.Write(githubResponse)
   720  	}))
   721  	defer ts.Close()
   722  
   723  	c := getClient(ts.URL)
   724  	commits, err := c.ListFileCommits("org", "repo", "README.md")
   725  	if err != nil {
   726  		t.Errorf("Didn't expect error: %v", err)
   727  	} else if len(commits) != 2 {
   728  		t.Errorf("Expected two commits, found %d: %v", len(commits), commits)
   729  		return
   730  	}
   731  	if commits[0].Author.Login != "hi-rustin" {
   732  		t.Errorf("Wrong author login for index 0: %v", commits[0])
   733  	}
   734  	if commits[1].Author.Login != "hi-rustin" {
   735  		t.Errorf("Wrong author login for index 1: %v", commits[1])
   736  	}
   737  }
   738  
   739  func TestGetSingleCommit(t *testing.T) {
   740  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   741  		if r.Method != http.MethodGet {
   742  			t.Errorf("Bad method: %s", r.Method)
   743  		}
   744  		if r.URL.Path != "/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e" {
   745  			t.Errorf("Bad request path: %s", r.URL.Path)
   746  		}
   747  		fmt.Fprint(w, `{
   748  			"commit": {
   749  			  "tree": {
   750  				"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e"
   751  			  }
   752  		        }
   753  		  }`)
   754  	}))
   755  	defer ts.Close()
   756  	c := getClient(ts.URL)
   757  	commit, err := c.GetSingleCommit("octocat", "Hello-World", "6dcb09b5b57875f334f61aebed695e2e4193db5e")
   758  	if err != nil {
   759  		t.Errorf("Didn't expect error: %v", err)
   760  	} else if commit.Commit.Tree.SHA != "6dcb09b5b57875f334f61aebed695e2e4193db5e" {
   761  		t.Errorf("Wrong tree-hash: %s", commit.Commit.Tree.SHA)
   762  	}
   763  }
   764  
   765  func TestCreateStatus(t *testing.T) {
   766  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   767  		if r.Method != http.MethodPost {
   768  			t.Errorf("Bad method: %s", r.Method)
   769  		}
   770  		if r.URL.Path != "/repos/k8s/kuber/statuses/abcdef" {
   771  			t.Errorf("Bad request path: %s", r.URL.Path)
   772  		}
   773  		b, err := io.ReadAll(r.Body)
   774  		if err != nil {
   775  			t.Fatalf("Could not read request body: %v", err)
   776  		}
   777  		var s Status
   778  		if err := json.Unmarshal(b, &s); err != nil {
   779  			t.Errorf("Could not unmarshal request: %v", err)
   780  		} else if s.Context != "c" {
   781  			t.Errorf("Wrong context: %s", s.Context)
   782  		}
   783  		http.Error(w, "201 Created", http.StatusCreated)
   784  	}))
   785  	defer ts.Close()
   786  	c := getClient(ts.URL)
   787  	if err := c.CreateStatus("k8s", "kuber", "abcdef", Status{
   788  		Context: "c",
   789  	}); err != nil {
   790  		t.Errorf("Didn't expect error: %v", err)
   791  	}
   792  }
   793  
   794  func TestListIssues(t *testing.T) {
   795  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   796  		if r.Method != http.MethodGet {
   797  			t.Errorf("Bad method: %s", r.Method)
   798  		}
   799  		if r.URL.Path == "/repos/k8s/kuber/issues" {
   800  			ics := []Issue{{Number: 1}}
   801  			b, err := json.Marshal(ics)
   802  			if err != nil {
   803  				t.Fatalf("Didn't expect error: %v", err)
   804  			}
   805  			w.Header().Set("Link", fmt.Sprintf(`<blorp>; rel="first", <https://%s/someotherpath>; rel="next"`, r.Host))
   806  			fmt.Fprint(w, string(b))
   807  		} else if r.URL.Path == "/someotherpath" {
   808  			ics := []Issue{{Number: 2}}
   809  			b, err := json.Marshal(ics)
   810  			if err != nil {
   811  				t.Fatalf("Didn't expect error: %v", err)
   812  			}
   813  			fmt.Fprint(w, string(b))
   814  		} else {
   815  			t.Errorf("Bad request path: %s", r.URL.Path)
   816  		}
   817  	}))
   818  	defer ts.Close()
   819  	c := getClient(ts.URL)
   820  	ics, err := c.ListOpenIssues("k8s", "kuber")
   821  	if err != nil {
   822  		t.Errorf("Didn't expect error: %v", err)
   823  	} else if len(ics) != 2 {
   824  		t.Errorf("Expected two issues, found %d: %v", len(ics), ics)
   825  	} else if ics[0].Number != 1 || ics[1].Number != 2 {
   826  		t.Errorf("Wrong issue IDs: %v", ics)
   827  	}
   828  }
   829  
   830  func TestListIssueComments(t *testing.T) {
   831  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   832  		if r.Method != http.MethodGet {
   833  			t.Errorf("Bad method: %s", r.Method)
   834  		}
   835  		if r.URL.Path == "/repos/k8s/kuber/issues/15/comments" {
   836  			ics := []IssueComment{{ID: 1}}
   837  			b, err := json.Marshal(ics)
   838  			if err != nil {
   839  				t.Fatalf("Didn't expect error: %v", err)
   840  			}
   841  			w.Header().Set("Link", fmt.Sprintf(`<blorp>; rel="first", <https://%s/someotherpath>; rel="next"`, r.Host))
   842  			fmt.Fprint(w, string(b))
   843  		} else if r.URL.Path == "/someotherpath" {
   844  			ics := []IssueComment{{ID: 2}}
   845  			b, err := json.Marshal(ics)
   846  			if err != nil {
   847  				t.Fatalf("Didn't expect error: %v", err)
   848  			}
   849  			fmt.Fprint(w, string(b))
   850  		} else {
   851  			t.Errorf("Bad request path: %s", r.URL.Path)
   852  		}
   853  	}))
   854  	defer ts.Close()
   855  	c := getClient(ts.URL)
   856  	ics, err := c.ListIssueComments("k8s", "kuber", 15)
   857  	if err != nil {
   858  		t.Errorf("Didn't expect error: %v", err)
   859  	} else if len(ics) != 2 {
   860  		t.Errorf("Expected two issues, found %d: %v", len(ics), ics)
   861  	} else if ics[0].ID != 1 || ics[1].ID != 2 {
   862  		t.Errorf("Wrong issue IDs: %v", ics)
   863  	}
   864  }
   865  
   866  func addLabelHTTPServer(t *testing.T, org, repo string, number int, labels ...string) *httptest.Server {
   867  	return httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   868  		if r.Method != http.MethodPost {
   869  			t.Errorf("Bad method: %s", r.Method)
   870  		}
   871  		if r.URL.Path != fmt.Sprintf("/repos/%s/%s/issues/%d/labels", org, repo, number) {
   872  			t.Errorf("Bad request path: %s", r.URL.Path)
   873  		}
   874  		b, err := io.ReadAll(r.Body)
   875  		if err != nil {
   876  			t.Fatalf("Could not read request body: %v", err)
   877  		}
   878  		var ls []string
   879  		if err := json.Unmarshal(b, &ls); err != nil {
   880  			t.Errorf("Could not unmarshal request: %v", err)
   881  		} else if len(ls) != len(labels) {
   882  			t.Errorf("Wrong length labels: %v", ls)
   883  		}
   884  
   885  		for index, label := range labels {
   886  			if ls[index] != label {
   887  				t.Errorf("Wrong label: %s", ls[index])
   888  			}
   889  		}
   890  	}))
   891  }
   892  
   893  func TestAddLabel(t *testing.T) {
   894  	ts := addLabelHTTPServer(t, "k8s", "kuber", 5, "yay")
   895  	defer ts.Close()
   896  	c := getClient(ts.URL)
   897  	if err := c.AddLabel("k8s", "kuber", 5, "yay"); err != nil {
   898  		t.Errorf("Didn't expect error: %v", err)
   899  	}
   900  }
   901  
   902  func TestAddLabels(t *testing.T) {
   903  	testCases := []struct {
   904  		name   string
   905  		org    string
   906  		repo   string
   907  		number int
   908  		labels []string
   909  	}{
   910  		{
   911  			name:   "one label",
   912  			org:    "k8s",
   913  			repo:   "kuber",
   914  			number: 1,
   915  			labels: []string{"one"},
   916  		},
   917  		{
   918  			name:   "two label",
   919  			org:    "k8s",
   920  			repo:   "kuber",
   921  			number: 2,
   922  			labels: []string{"one", "two"},
   923  		},
   924  	}
   925  	for _, tc := range testCases {
   926  		t.Run(tc.name, func(t *testing.T) {
   927  			ts := addLabelHTTPServer(t, tc.org, tc.repo, tc.number, tc.labels...)
   928  			defer ts.Close()
   929  
   930  			c := getClient(ts.URL)
   931  			if err := c.AddLabels(tc.org, tc.repo, tc.number, tc.labels...); err != nil {
   932  				t.Errorf("Didn't expect error: %v", err)
   933  			}
   934  		})
   935  	}
   936  }
   937  
   938  func TestRemoveLabel(t *testing.T) {
   939  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   940  		if r.Method != http.MethodDelete {
   941  			t.Errorf("Bad method: %s", r.Method)
   942  		}
   943  		if r.URL.Path != "/repos/k8s/kuber/issues/5/labels/yay" {
   944  			t.Errorf("Bad request path: %s", r.URL.Path)
   945  		}
   946  		http.Error(w, "204 No Content", http.StatusNoContent)
   947  	}))
   948  	defer ts.Close()
   949  	c := getClient(ts.URL)
   950  	if err := c.RemoveLabel("k8s", "kuber", 5, "yay"); err != nil {
   951  		t.Errorf("Didn't expect error: %v", err)
   952  	}
   953  }
   954  
   955  func TestRemoveLabelFailsOnOtherThan404(t *testing.T) {
   956  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   957  		if r.Method != http.MethodDelete {
   958  			t.Errorf("Bad method: %s", r.Method)
   959  		}
   960  		if r.URL.Path != "/repos/k8s/kuber/issues/5/labels/yay" {
   961  			t.Errorf("Bad request path: %s", r.URL.Path)
   962  		}
   963  		http.Error(w, "403 Forbidden", http.StatusForbidden)
   964  	}))
   965  	defer ts.Close()
   966  	c := getClient(ts.URL)
   967  	err := c.RemoveLabel("k8s", "kuber", 5, "yay")
   968  	if err == nil {
   969  		t.Errorf("Expected error but got none")
   970  	}
   971  }
   972  
   973  func TestRemoveLabelNotFound(t *testing.T) {
   974  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   975  		http.Error(w, `{"message": "Label does not exist"}`, 404)
   976  	}))
   977  	defer ts.Close()
   978  	c := getClient(ts.URL)
   979  	err := c.RemoveLabel("any", "old", 3, "label")
   980  
   981  	if err != nil {
   982  		t.Fatalf("RemoveLabel expected no error, got one: %v", err)
   983  	}
   984  }
   985  
   986  func TestNewNotFoundIsNotFound(t *testing.T) {
   987  	if !IsNotFound(NewNotFound()) {
   988  		t.Error("NewNotFound didn't return an error that was considered a NotFound")
   989  	}
   990  }
   991  
   992  func TestIsNotFound(t *testing.T) {
   993  	testCases := []struct {
   994  		name       string
   995  		code       int
   996  		body       string
   997  		isNotFound bool
   998  	}{
   999  		{
  1000  			name:       "should be not found when status code is 404",
  1001  			code:       404,
  1002  			body:       `{"message":"not found","errors":[{"resource":"fake resource","field":"fake field","code":"404","message":"status code 404"}]}`,
  1003  			isNotFound: true,
  1004  		},
  1005  		{
  1006  			name:       "should not be not found when status code is 200",
  1007  			code:       200,
  1008  			body:       `{"message": "ok"}`,
  1009  			isNotFound: false,
  1010  		},
  1011  	}
  1012  
  1013  	for _, tc := range testCases {
  1014  		t.Run(tc.name, func(t *testing.T) {
  1015  			ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1016  				http.Error(w, tc.body, tc.code)
  1017  			}))
  1018  			defer ts.Close()
  1019  
  1020  			c := getClient(ts.URL)
  1021  
  1022  			code, _, err := c.requestRaw(&request{
  1023  				method:    http.MethodGet,
  1024  				path:      fmt.Sprintf("/repos/%s/%s/branches/%s/protection", "org", "repo", "branch"),
  1025  				exitCodes: []int{200},
  1026  			})
  1027  
  1028  			if code != tc.code {
  1029  				t.Fatalf("Expected code to be %d, but got %d", tc.code, code)
  1030  			}
  1031  
  1032  			isNotFound := IsNotFound(err)
  1033  
  1034  			if isNotFound != tc.isNotFound {
  1035  				t.Fatalf("Expected isNotFound to be %t, but got %t", tc.isNotFound, isNotFound)
  1036  			}
  1037  		})
  1038  	}
  1039  }
  1040  
  1041  func TestIsNotFound_nested(t *testing.T) {
  1042  	t.Parallel()
  1043  	testCases := []struct {
  1044  		name        string
  1045  		err         error
  1046  		expectMatch bool
  1047  	}{
  1048  		{
  1049  			name:        "direct match",
  1050  			err:         requestError{ClientError: ClientError{Errors: []clientErrorSubError{{Message: "status code 404"}}}},
  1051  			expectMatch: true,
  1052  		},
  1053  		{
  1054  			name:        "direct, no match",
  1055  			err:         requestError{ClientError: ClientError{Errors: []clientErrorSubError{{Message: "status code 403"}}}},
  1056  			expectMatch: false,
  1057  		},
  1058  		{
  1059  			name:        "nested match",
  1060  			err:         fmt.Errorf("wrapping: %w", requestError{ClientError: ClientError{Errors: []clientErrorSubError{{Message: "status code 404"}}}}),
  1061  			expectMatch: true,
  1062  		},
  1063  		{
  1064  			name:        "nested, no match",
  1065  			err:         fmt.Errorf("wrapping: %w", requestError{ClientError: ClientError{Errors: []clientErrorSubError{{Message: "status code 403"}}}}),
  1066  			expectMatch: false,
  1067  		},
  1068  	}
  1069  
  1070  	for _, tc := range testCases {
  1071  		t.Run(tc.name, func(t *testing.T) {
  1072  			if result := IsNotFound(tc.err); result != tc.expectMatch {
  1073  				t.Errorf("expected match: %t, got match: %t", tc.expectMatch, result)
  1074  			}
  1075  		})
  1076  	}
  1077  
  1078  }
  1079  
  1080  func TestAssignIssue(t *testing.T) {
  1081  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1082  		if r.Method != http.MethodPost {
  1083  			t.Errorf("Bad method: %s", r.Method)
  1084  		}
  1085  		if r.URL.Path != "/repos/k8s/kuber/issues/5/assignees" {
  1086  			t.Errorf("Bad request path: %s", r.URL.Path)
  1087  		}
  1088  		b, err := io.ReadAll(r.Body)
  1089  		if err != nil {
  1090  			t.Fatalf("Could not read request body: %v", err)
  1091  		}
  1092  		var ps map[string][]string
  1093  		if err := json.Unmarshal(b, &ps); err != nil {
  1094  			t.Errorf("Could not unmarshal request: %v", err)
  1095  		} else if len(ps) != 1 {
  1096  			t.Errorf("Wrong length patch: %v", ps)
  1097  		} else if len(ps["assignees"]) == 3 {
  1098  			if ps["assignees"][0] != "george" || ps["assignees"][1] != "jungle" || ps["assignees"][2] != "not-in-the-org" {
  1099  				t.Errorf("Wrong assignees: %v", ps)
  1100  			}
  1101  		} else if len(ps["assignees"]) == 2 {
  1102  			if ps["assignees"][0] != "george" || ps["assignees"][1] != "jungle" {
  1103  				t.Errorf("Wrong assignees: %v", ps)
  1104  			}
  1105  
  1106  		} else {
  1107  			t.Errorf("Wrong assignees length: %v", ps)
  1108  		}
  1109  		w.WriteHeader(http.StatusCreated)
  1110  		json.NewEncoder(w).Encode(Issue{
  1111  			Assignees: []User{{Login: "george"}, {Login: "jungle"}, {Login: "ignore-other"}},
  1112  		})
  1113  	}))
  1114  	defer ts.Close()
  1115  	c := getClient(ts.URL)
  1116  	if err := c.AssignIssue("k8s", "kuber", 5, []string{"george", "jungle"}); err != nil {
  1117  		t.Errorf("Unexpected error: %v", err)
  1118  	}
  1119  	if err := c.AssignIssue("k8s", "kuber", 5, []string{"george", "jungle", "not-in-the-org"}); err == nil {
  1120  		t.Errorf("Expected an error")
  1121  	} else if merr, ok := err.(MissingUsers); ok {
  1122  		if len(merr.Users) != 1 || merr.Users[0] != "not-in-the-org" {
  1123  			t.Errorf("Expected [not-in-the-org], not %v", merr.Users)
  1124  		}
  1125  	} else {
  1126  		t.Errorf("Expected MissingUsers error")
  1127  	}
  1128  }
  1129  
  1130  func TestUnassignIssue(t *testing.T) {
  1131  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1132  		if r.Method != http.MethodDelete {
  1133  			t.Errorf("Bad method: %s", r.Method)
  1134  		}
  1135  		if r.URL.Path != "/repos/k8s/kuber/issues/5/assignees" {
  1136  			t.Errorf("Bad request path: %s", r.URL.Path)
  1137  		}
  1138  		b, err := io.ReadAll(r.Body)
  1139  		if err != nil {
  1140  			t.Fatalf("Could not read request body: %v", err)
  1141  		}
  1142  		var ps map[string][]string
  1143  		if err := json.Unmarshal(b, &ps); err != nil {
  1144  			t.Errorf("Could not unmarshal request: %v", err)
  1145  		} else if len(ps) != 1 {
  1146  			t.Errorf("Wrong length patch: %v", ps)
  1147  		} else if len(ps["assignees"]) == 3 {
  1148  			if ps["assignees"][0] != "george" || ps["assignees"][1] != "jungle" || ps["assignees"][2] != "perma-assignee" {
  1149  				t.Errorf("Wrong assignees: %v", ps)
  1150  			}
  1151  		} else if len(ps["assignees"]) == 2 {
  1152  			if ps["assignees"][0] != "george" || ps["assignees"][1] != "jungle" {
  1153  				t.Errorf("Wrong assignees: %v", ps)
  1154  			}
  1155  
  1156  		} else {
  1157  			t.Errorf("Wrong assignees length: %v", ps)
  1158  		}
  1159  		json.NewEncoder(w).Encode(Issue{
  1160  			Assignees: []User{{Login: "perma-assignee"}, {Login: "ignore-other"}},
  1161  		})
  1162  	}))
  1163  	defer ts.Close()
  1164  	c := getClient(ts.URL)
  1165  	if err := c.UnassignIssue("k8s", "kuber", 5, []string{"george", "jungle"}); err != nil {
  1166  		t.Errorf("Unexpected error: %v", err)
  1167  	}
  1168  	if err := c.UnassignIssue("k8s", "kuber", 5, []string{"george", "jungle", "perma-assignee"}); err == nil {
  1169  		t.Errorf("Expected an error")
  1170  	} else if merr, ok := err.(ExtraUsers); ok {
  1171  		if len(merr.Users) != 1 || merr.Users[0] != "perma-assignee" {
  1172  			t.Errorf("Expected [perma-assignee], not %v", merr.Users)
  1173  		}
  1174  	} else {
  1175  		t.Errorf("Expected ExtraUsers error")
  1176  	}
  1177  }
  1178  
  1179  func TestReadPaginatedResults(t *testing.T) {
  1180  	type response struct {
  1181  		labels []Label
  1182  		next   string
  1183  	}
  1184  	cases := []struct {
  1185  		name           string
  1186  		baseSuffix     string
  1187  		initialPath    string
  1188  		responses      map[string]response
  1189  		expectedLabels []Label
  1190  	}{
  1191  		{
  1192  			name:        "regular pagination",
  1193  			initialPath: "/label/foo",
  1194  			responses: map[string]response{
  1195  				"/label/foo": {
  1196  					labels: []Label{{Name: "foo"}},
  1197  					next:   `<blorp>; rel="first", <https://%s/label/bar>; rel="next"`,
  1198  				},
  1199  				"/label/bar": {
  1200  					labels: []Label{{Name: "bar"}},
  1201  				},
  1202  			},
  1203  			expectedLabels: []Label{{Name: "foo"}, {Name: "bar"}},
  1204  		},
  1205  		{
  1206  			name:        "pagination with /api/v3 base suffix",
  1207  			initialPath: "/label/foo",
  1208  			baseSuffix:  "/api/v3",
  1209  			responses: map[string]response{
  1210  				"/api/v3/label/foo": {
  1211  					labels: []Label{{Name: "foo"}},
  1212  					next:   `<blorp>; rel="first", <https://%s/api/v3/label/bar>; rel="next"`,
  1213  				},
  1214  				"/api/v3/label/bar": {
  1215  					labels: []Label{{Name: "bar"}},
  1216  				},
  1217  			},
  1218  			expectedLabels: []Label{{Name: "foo"}, {Name: "bar"}},
  1219  		},
  1220  	}
  1221  	for _, tc := range cases {
  1222  		ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1223  			if r.Method != http.MethodGet {
  1224  				t.Errorf("Bad method: %s", r.Method)
  1225  			}
  1226  			if response, ok := tc.responses[r.URL.Path]; ok {
  1227  				b, err := json.Marshal(response.labels)
  1228  				if err != nil {
  1229  					t.Fatalf("Didn't expect error: %v", err)
  1230  				}
  1231  				if response.next != "" {
  1232  					w.Header().Set("Link", fmt.Sprintf(response.next, r.Host))
  1233  				}
  1234  				fmt.Fprint(w, string(b))
  1235  			} else {
  1236  				t.Errorf("Bad request path: %s", r.URL.Path)
  1237  			}
  1238  		}))
  1239  		defer ts.Close()
  1240  
  1241  		c := getClient(ts.URL)
  1242  		c.bases[0] = c.bases[0] + tc.baseSuffix
  1243  		var labels []Label
  1244  		err := c.readPaginatedResults(
  1245  			tc.initialPath,
  1246  			"",
  1247  			"",
  1248  			func() interface{} {
  1249  				return &[]Label{}
  1250  			},
  1251  			func(obj interface{}) {
  1252  				labels = append(labels, *(obj.(*[]Label))...)
  1253  			},
  1254  		)
  1255  		if err != nil {
  1256  			t.Errorf("%s: didn't expect error: %v", tc.name, err)
  1257  		} else {
  1258  			if !reflect.DeepEqual(labels, tc.expectedLabels) {
  1259  				t.Errorf("%s: expected %s, got %s", tc.name, tc.expectedLabels, labels)
  1260  			}
  1261  		}
  1262  	}
  1263  }
  1264  
  1265  func TestListPullRequestComments(t *testing.T) {
  1266  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1267  		if r.Method != http.MethodGet {
  1268  			t.Errorf("Bad method: %s", r.Method)
  1269  		}
  1270  		if r.URL.Path == "/repos/k8s/kuber/pulls/15/comments" {
  1271  			prcs := []ReviewComment{{ID: 1}}
  1272  			b, err := json.Marshal(prcs)
  1273  			if err != nil {
  1274  				t.Fatalf("Didn't expect error: %v", err)
  1275  			}
  1276  			w.Header().Set("Link", fmt.Sprintf(`<blorp>; rel="first", <https://%s/someotherpath>; rel="next"`, r.Host))
  1277  			fmt.Fprint(w, string(b))
  1278  		} else if r.URL.Path == "/someotherpath" {
  1279  			prcs := []ReviewComment{{ID: 2}}
  1280  			b, err := json.Marshal(prcs)
  1281  			if err != nil {
  1282  				t.Fatalf("Didn't expect error: %v", err)
  1283  			}
  1284  			fmt.Fprint(w, string(b))
  1285  		} else {
  1286  			t.Errorf("Bad request path: %s", r.URL.Path)
  1287  		}
  1288  	}))
  1289  	defer ts.Close()
  1290  	c := getClient(ts.URL)
  1291  	prcs, err := c.ListPullRequestComments("k8s", "kuber", 15)
  1292  	if err != nil {
  1293  		t.Errorf("Didn't expect error: %v", err)
  1294  	} else if len(prcs) != 2 {
  1295  		t.Errorf("Expected two comments, found %d: %v", len(prcs), prcs)
  1296  	} else if prcs[0].ID != 1 || prcs[1].ID != 2 {
  1297  		t.Errorf("Wrong issue IDs: %v", prcs)
  1298  	}
  1299  }
  1300  
  1301  func TestListReviews(t *testing.T) {
  1302  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1303  		if r.Method != http.MethodGet {
  1304  			t.Errorf("Bad method: %s", r.Method)
  1305  		}
  1306  		if r.URL.Path == "/repos/k8s/kuber/pulls/15/reviews" {
  1307  			reviews := []Review{{ID: 1}}
  1308  			b, err := json.Marshal(reviews)
  1309  			if err != nil {
  1310  				t.Fatalf("Didn't expect error: %v", err)
  1311  			}
  1312  			w.Header().Set("Link", fmt.Sprintf(`<blorp>; rel="first", <https://%s/someotherpath>; rel="next"`, r.Host))
  1313  			fmt.Fprint(w, string(b))
  1314  		} else if r.URL.Path == "/someotherpath" {
  1315  			reviews := []Review{{ID: 2}}
  1316  			b, err := json.Marshal(reviews)
  1317  			if err != nil {
  1318  				t.Fatalf("Didn't expect error: %v", err)
  1319  			}
  1320  			fmt.Fprint(w, string(b))
  1321  		} else {
  1322  			t.Errorf("Bad request path: %s", r.URL.Path)
  1323  		}
  1324  	}))
  1325  	defer ts.Close()
  1326  	c := getClient(ts.URL)
  1327  	reviews, err := c.ListReviews("k8s", "kuber", 15)
  1328  	if err != nil {
  1329  		t.Errorf("Didn't expect error: %v", err)
  1330  	} else if len(reviews) != 2 {
  1331  		t.Errorf("Expected two reviews, found %d: %v", len(reviews), reviews)
  1332  	} else if reviews[0].ID != 1 || reviews[1].ID != 2 {
  1333  		t.Errorf("Wrong review IDs: %v", reviews)
  1334  	}
  1335  }
  1336  
  1337  func TestPrepareReviewersBody(t *testing.T) {
  1338  	var tests = []struct {
  1339  		name         string
  1340  		logins       []string
  1341  		expectedBody map[string][]string
  1342  	}{
  1343  		{
  1344  			name:         "one reviewer",
  1345  			logins:       []string{"george"},
  1346  			expectedBody: map[string][]string{"reviewers": {"george"}},
  1347  		},
  1348  		{
  1349  			name:         "three reviewers",
  1350  			logins:       []string{"george", "jungle", "chimp"},
  1351  			expectedBody: map[string][]string{"reviewers": {"george", "jungle", "chimp"}},
  1352  		},
  1353  		{
  1354  			name:         "one team",
  1355  			logins:       []string{"kubernetes/sig-testing-misc"},
  1356  			expectedBody: map[string][]string{"team_reviewers": {"sig-testing-misc"}},
  1357  		},
  1358  		{
  1359  			name:         "two teams",
  1360  			logins:       []string{"kubernetes/sig-testing-misc", "kubernetes/sig-testing-bugs"},
  1361  			expectedBody: map[string][]string{"team_reviewers": {"sig-testing-misc", "sig-testing-bugs"}},
  1362  		},
  1363  		{
  1364  			name:         "one team not in org",
  1365  			logins:       []string{"kubernetes/sig-testing-misc", "other-org/sig-testing-bugs"},
  1366  			expectedBody: map[string][]string{"team_reviewers": {"sig-testing-misc"}},
  1367  		},
  1368  		{
  1369  			name:         "mixed single",
  1370  			logins:       []string{"george", "kubernetes/sig-testing-misc"},
  1371  			expectedBody: map[string][]string{"reviewers": {"george"}, "team_reviewers": {"sig-testing-misc"}},
  1372  		},
  1373  		{
  1374  			name:         "mixed multiple",
  1375  			logins:       []string{"george", "kubernetes/sig-testing-misc", "kubernetes/sig-testing-bugs", "jungle", "chimp"},
  1376  			expectedBody: map[string][]string{"reviewers": {"george", "jungle", "chimp"}, "team_reviewers": {"sig-testing-misc", "sig-testing-bugs"}},
  1377  		},
  1378  	}
  1379  	for _, test := range tests {
  1380  		body, _ := prepareReviewersBody(test.logins, "kubernetes")
  1381  		if !reflect.DeepEqual(body, test.expectedBody) {
  1382  			t.Errorf("%s: got %s instead of %s", test.name, body, test.expectedBody)
  1383  		}
  1384  	}
  1385  }
  1386  
  1387  func TestRequestReview(t *testing.T) {
  1388  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1389  		if r.Method != http.MethodPost {
  1390  			t.Errorf("Bad method: %s", r.Method)
  1391  		}
  1392  		if r.URL.Path != "/repos/k8s/kuber/pulls/5/requested_reviewers" {
  1393  			t.Errorf("Bad request path: %s", r.URL.Path)
  1394  		}
  1395  		b, err := io.ReadAll(r.Body)
  1396  		if err != nil {
  1397  			t.Fatalf("Could not read request body: %v", err)
  1398  		}
  1399  		var ps map[string][]string
  1400  		if err := json.Unmarshal(b, &ps); err != nil {
  1401  			t.Fatalf("Could not unmarshal request: %v", err)
  1402  		}
  1403  		if len(ps) < 1 || len(ps) > 2 {
  1404  			t.Fatalf("Wrong length patch: %v", ps)
  1405  		}
  1406  		if sets.New[string](ps["reviewers"]...).Has("not-a-collaborator") {
  1407  			w.WriteHeader(http.StatusUnprocessableEntity)
  1408  			return
  1409  		}
  1410  		requestedReviewers := []User{}
  1411  		for _, reviewers := range ps {
  1412  			for _, reviewer := range reviewers {
  1413  				requestedReviewers = append(requestedReviewers, User{Login: reviewer})
  1414  			}
  1415  		}
  1416  		w.WriteHeader(http.StatusCreated)
  1417  		json.NewEncoder(w).Encode(PullRequest{
  1418  			RequestedReviewers: requestedReviewers,
  1419  		})
  1420  	}))
  1421  	defer ts.Close()
  1422  	c := getClient(ts.URL)
  1423  	if err := c.RequestReview("k8s", "kuber", 5, []string{"george", "jungle"}); err != nil {
  1424  		t.Errorf("Unexpected error: %v", err)
  1425  	}
  1426  	if err := c.RequestReview("k8s", "kuber", 5, []string{"george", "jungle", "k8s/team1"}); err != nil {
  1427  		t.Errorf("Unexpected error: %v", err)
  1428  	}
  1429  	if err := c.RequestReview("k8s", "kuber", 5, []string{"george", "jungle", "not-a-collaborator"}); err == nil {
  1430  		t.Errorf("Expected an error")
  1431  	} else if merr, ok := err.(MissingUsers); ok {
  1432  		if len(merr.Users) != 1 || merr.Users[0] != "not-a-collaborator" {
  1433  			t.Errorf("Expected [not-a-collaborator], not %v", merr.Users)
  1434  		}
  1435  		expErr := "could not request a PR review from the following user(s): not-a-collaborator; status code 422 not one of [201], body: ."
  1436  		if merr.Error() != expErr {
  1437  			t.Errorf("Expected error string %q, not %q", expErr, merr.Error())
  1438  		}
  1439  	} else {
  1440  		t.Errorf("Expected MissingUsers error")
  1441  	}
  1442  	if err := c.RequestReview("k8s", "kuber", 5, []string{"george", "jungle", "notk8s/team1"}); err == nil {
  1443  		t.Errorf("Expected an error")
  1444  	} else if merr, ok := err.(MissingUsers); ok {
  1445  		if len(merr.Users) != 1 || merr.Users[0] != "notk8s/team1" {
  1446  			t.Errorf("Expected [notk8s/team1], not %v", merr.Users)
  1447  		}
  1448  		expErr := "could not request a PR review from the following user(s): notk8s/team1; team notk8s/team1 is not part of k8s org."
  1449  		if merr.Error() != expErr {
  1450  			t.Errorf("Expected error string %q, not %q", expErr, merr.Error())
  1451  		}
  1452  	} else {
  1453  		t.Errorf("Expected MissingUsers error")
  1454  	}
  1455  }
  1456  
  1457  func TestUnrequestReview(t *testing.T) {
  1458  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1459  		if r.Method != http.MethodDelete {
  1460  			t.Errorf("Bad method: %s", r.Method)
  1461  		}
  1462  		if r.URL.Path != "/repos/k8s/kuber/pulls/5/requested_reviewers" {
  1463  			t.Errorf("Bad request path: %s", r.URL.Path)
  1464  		}
  1465  		b, err := io.ReadAll(r.Body)
  1466  		if err != nil {
  1467  			t.Fatalf("Could not read request body: %v", err)
  1468  		}
  1469  		var ps map[string][]string
  1470  		if err := json.Unmarshal(b, &ps); err != nil {
  1471  			t.Errorf("Could not unmarshal request: %v", err)
  1472  		} else if len(ps) != 1 {
  1473  			t.Errorf("Wrong length patch: %v", ps)
  1474  		} else if len(ps["reviewers"]) == 3 {
  1475  			if ps["reviewers"][0] != "george" || ps["reviewers"][1] != "jungle" || ps["reviewers"][2] != "perma-reviewer" {
  1476  				t.Errorf("Wrong reviewers: %v", ps)
  1477  			}
  1478  		} else if len(ps["reviewers"]) == 2 {
  1479  			if ps["reviewers"][0] != "george" || ps["reviewers"][1] != "jungle" {
  1480  				t.Errorf("Wrong reviewers: %v", ps)
  1481  			}
  1482  		} else {
  1483  			t.Errorf("Wrong reviewers length: %v", ps)
  1484  		}
  1485  		json.NewEncoder(w).Encode(PullRequest{
  1486  			RequestedReviewers: []User{{Login: "perma-reviewer"}, {Login: "ignore-other"}},
  1487  		})
  1488  	}))
  1489  	defer ts.Close()
  1490  	c := getClient(ts.URL)
  1491  	if err := c.UnrequestReview("k8s", "kuber", 5, []string{"george", "jungle"}); err != nil {
  1492  		t.Errorf("Unexpected error: %v", err)
  1493  	}
  1494  	if err := c.UnrequestReview("k8s", "kuber", 5, []string{"george", "jungle", "perma-reviewer"}); err == nil {
  1495  		t.Errorf("Expected an error")
  1496  	} else if merr, ok := err.(ExtraUsers); ok {
  1497  		if len(merr.Users) != 1 || merr.Users[0] != "perma-reviewer" {
  1498  			t.Errorf("Expected [perma-reviewer], not %v", merr.Users)
  1499  		}
  1500  	} else {
  1501  		t.Errorf("Expected ExtraUsers error")
  1502  	}
  1503  }
  1504  
  1505  func TestCloseIssue(t *testing.T) {
  1506  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1507  		if r.Method != http.MethodPatch {
  1508  			t.Errorf("Bad method: %s", r.Method)
  1509  		}
  1510  		if r.URL.Path != "/repos/k8s/kuber/issues/5" {
  1511  			t.Errorf("Bad request path: %s", r.URL.Path)
  1512  		}
  1513  		b, err := io.ReadAll(r.Body)
  1514  		if err != nil {
  1515  			t.Fatalf("Could not read request body: %v", err)
  1516  		}
  1517  		var ps map[string]string
  1518  		if err := json.Unmarshal(b, &ps); err != nil {
  1519  			t.Errorf("Could not unmarshal request: %v", err)
  1520  		} else if len(ps) != 2 {
  1521  			t.Errorf("Wrong length patch: %v", ps)
  1522  		} else if ps["state"] != "closed" {
  1523  			t.Errorf("Wrong state: %s", ps["state"])
  1524  		} else if ps["state_reason"] != "completed" {
  1525  			t.Errorf("Wrong state_reason: %s", ps["state_reason"])
  1526  		}
  1527  	}))
  1528  	defer ts.Close()
  1529  	c := getClient(ts.URL)
  1530  	if err := c.CloseIssue("k8s", "kuber", 5); err != nil {
  1531  		t.Errorf("Didn't expect error: %v", err)
  1532  	}
  1533  }
  1534  
  1535  func TestCloseIssueAsNotPlanned(t *testing.T) {
  1536  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1537  		if r.Method != http.MethodPatch {
  1538  			t.Errorf("Bad method: %s", r.Method)
  1539  		}
  1540  		if r.URL.Path != "/repos/k8s/kuber/issues/5" {
  1541  			t.Errorf("Bad request path: %s", r.URL.Path)
  1542  		}
  1543  		b, err := io.ReadAll(r.Body)
  1544  		if err != nil {
  1545  			t.Fatalf("Could not read request body: %v", err)
  1546  		}
  1547  		var ps map[string]string
  1548  		if err := json.Unmarshal(b, &ps); err != nil {
  1549  			t.Errorf("Could not unmarshal request: %v", err)
  1550  		} else if len(ps) != 2 {
  1551  			t.Errorf("Wrong length patch: %v", ps)
  1552  		} else if ps["state"] != "closed" {
  1553  			t.Errorf("Wrong state: %s", ps["state"])
  1554  		} else if ps["state_reason"] != "not_planned" {
  1555  			t.Errorf("Wrong state_reason: %s", ps["state_reason"])
  1556  		}
  1557  	}))
  1558  	defer ts.Close()
  1559  	c := getClient(ts.URL)
  1560  	if err := c.CloseIssueAsNotPlanned("k8s", "kuber", 5); err != nil {
  1561  		t.Errorf("Didn't expect error: %v", err)
  1562  	}
  1563  }
  1564  
  1565  func TestReopenIssue(t *testing.T) {
  1566  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1567  		if r.Method != http.MethodPatch {
  1568  			t.Errorf("Bad method: %s", r.Method)
  1569  		}
  1570  		if r.URL.Path != "/repos/k8s/kuber/issues/5" {
  1571  			t.Errorf("Bad request path: %s", r.URL.Path)
  1572  		}
  1573  		b, err := io.ReadAll(r.Body)
  1574  		if err != nil {
  1575  			t.Fatalf("Could not read request body: %v", err)
  1576  		}
  1577  		var ps map[string]string
  1578  		if err := json.Unmarshal(b, &ps); err != nil {
  1579  			t.Errorf("Could not unmarshal request: %v", err)
  1580  		} else if len(ps) != 1 {
  1581  			t.Errorf("Wrong length patch: %v", ps)
  1582  		} else if ps["state"] != "open" {
  1583  			t.Errorf("Wrong state: %s", ps["state"])
  1584  		}
  1585  	}))
  1586  	defer ts.Close()
  1587  	c := getClient(ts.URL)
  1588  	if err := c.ReopenIssue("k8s", "kuber", 5); err != nil {
  1589  		t.Errorf("Didn't expect error: %v", err)
  1590  	}
  1591  }
  1592  
  1593  func TestClosePullRequest(t *testing.T) {
  1594  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1595  		if r.Method != http.MethodPatch {
  1596  			t.Errorf("Bad method: %s", r.Method)
  1597  		}
  1598  		if r.URL.Path != "/repos/k8s/kuber/pulls/5" {
  1599  			t.Errorf("Bad request path: %s", r.URL.Path)
  1600  		}
  1601  		b, err := io.ReadAll(r.Body)
  1602  		if err != nil {
  1603  			t.Fatalf("Could not read request body: %v", err)
  1604  		}
  1605  		var ps map[string]string
  1606  		if err := json.Unmarshal(b, &ps); err != nil {
  1607  			t.Errorf("Could not unmarshal request: %v", err)
  1608  		} else if len(ps) != 1 {
  1609  			t.Errorf("Wrong length patch: %v", ps)
  1610  		} else if ps["state"] != "closed" {
  1611  			t.Errorf("Wrong state: %s", ps["state"])
  1612  		}
  1613  	}))
  1614  	defer ts.Close()
  1615  	c := getClient(ts.URL)
  1616  	if err := c.ClosePullRequest("k8s", "kuber", 5); err != nil {
  1617  		t.Errorf("Didn't expect error: %v", err)
  1618  	}
  1619  }
  1620  
  1621  func TestReopenPullRequest(t *testing.T) {
  1622  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1623  		if r.Method != http.MethodPatch {
  1624  			t.Errorf("Bad method: %s", r.Method)
  1625  		}
  1626  		if r.URL.Path != "/repos/k8s/kuber/pulls/5" {
  1627  			t.Errorf("Bad request path: %s", r.URL.Path)
  1628  		}
  1629  		b, err := io.ReadAll(r.Body)
  1630  		if err != nil {
  1631  			t.Fatalf("Could not read request body: %v", err)
  1632  		}
  1633  		var ps map[string]string
  1634  		if err := json.Unmarshal(b, &ps); err != nil {
  1635  			t.Errorf("Could not unmarshal request: %v", err)
  1636  		} else if len(ps) != 1 {
  1637  			t.Errorf("Wrong length patch: %v", ps)
  1638  		} else if ps["state"] != "open" {
  1639  			t.Errorf("Wrong state: %s", ps["state"])
  1640  		}
  1641  	}))
  1642  	defer ts.Close()
  1643  	c := getClient(ts.URL)
  1644  	if err := c.ReopenPullRequest("k8s", "kuber", 5); err != nil {
  1645  		t.Errorf("Didn't expect error: %v", err)
  1646  	}
  1647  }
  1648  
  1649  func TestFindIssues(t *testing.T) {
  1650  	cases := []struct {
  1651  		name  string
  1652  		sort  bool
  1653  		order bool
  1654  	}{
  1655  		{
  1656  			name: "simple query",
  1657  		},
  1658  		{
  1659  			name: "sort no order",
  1660  			sort: true,
  1661  		},
  1662  		{
  1663  			name:  "sort and order",
  1664  			sort:  true,
  1665  			order: true,
  1666  		},
  1667  	}
  1668  
  1669  	issueNum := 5
  1670  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1671  		if r.Method != http.MethodGet {
  1672  			t.Errorf("Bad method: %s", r.Method)
  1673  		}
  1674  		if r.URL.Path != "/search/issues" {
  1675  			t.Errorf("Bad request path: %s", r.URL.Path)
  1676  		}
  1677  		issueList := IssuesSearchResult{
  1678  			Total: 1,
  1679  			Issues: []Issue{
  1680  				{
  1681  					Number: issueNum,
  1682  					Title:  r.URL.RawQuery,
  1683  				},
  1684  			},
  1685  		}
  1686  		b, err := json.Marshal(&issueList)
  1687  		if err != nil {
  1688  			t.Fatalf("Didn't expect error: %v", err)
  1689  		}
  1690  		fmt.Fprint(w, string(b))
  1691  	}))
  1692  	defer ts.Close()
  1693  	c := getClient(ts.URL)
  1694  
  1695  	for _, tc := range cases {
  1696  		var result []Issue
  1697  		var err error
  1698  		sort := ""
  1699  		if tc.sort {
  1700  			sort = "sort-strategy"
  1701  		}
  1702  		if result, err = c.FindIssues("commit_hash", sort, tc.order); err != nil {
  1703  			t.Errorf("%s: didn't expect error: %v", tc.name, err)
  1704  		}
  1705  		if len(result) != 1 {
  1706  			t.Fatalf("%s: unexpected number of results: %v", tc.name, len(result))
  1707  		}
  1708  		if result[0].Number != issueNum {
  1709  			t.Errorf("%s: expected issue number %+v, got %+v", tc.name, issueNum, result[0].Number)
  1710  		}
  1711  		if tc.sort && !strings.Contains(result[0].Title, "sort="+sort) {
  1712  			t.Errorf("%s: missing sort=%s from query: %s", tc.name, sort, result[0].Title)
  1713  		}
  1714  		if tc.order && !strings.Contains(result[0].Title, "order=asc") {
  1715  			t.Errorf("%s: missing order=asc from query: %s", tc.name, result[0].Title)
  1716  		}
  1717  	}
  1718  }
  1719  
  1720  func TestFindIssuesWithOrg(t *testing.T) {
  1721  	cases := []struct {
  1722  		name  string
  1723  		sort  bool
  1724  		order bool
  1725  	}{
  1726  		{
  1727  			name: "simple query",
  1728  		},
  1729  		{
  1730  			name: "sort no order",
  1731  			sort: true,
  1732  		},
  1733  		{
  1734  			name:  "sort and order",
  1735  			sort:  true,
  1736  			order: true,
  1737  		},
  1738  	}
  1739  
  1740  	issueNum := 5
  1741  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1742  		if r.Method != http.MethodGet {
  1743  			t.Errorf("Bad method: %s", r.Method)
  1744  		}
  1745  		if r.URL.Path != "/search/issues" {
  1746  			t.Errorf("Bad request path: %s", r.URL.Path)
  1747  		}
  1748  		issueList := IssuesSearchResult{
  1749  			Total: 1,
  1750  			Issues: []Issue{
  1751  				{
  1752  					Number: issueNum,
  1753  					Title:  r.URL.RawQuery,
  1754  				},
  1755  			},
  1756  		}
  1757  		b, err := json.Marshal(&issueList)
  1758  		if err != nil {
  1759  			t.Fatalf("Didn't expect error: %v", err)
  1760  		}
  1761  		fmt.Fprint(w, string(b))
  1762  	}))
  1763  	defer ts.Close()
  1764  	c := getClient(ts.URL)
  1765  
  1766  	for _, tc := range cases {
  1767  		var result []Issue
  1768  		var err error
  1769  		sort := ""
  1770  		if tc.sort {
  1771  			sort = "sort-strategy"
  1772  		}
  1773  		if result, err = c.FindIssuesWithOrg("k8s", "commit_hash", sort, tc.order); err != nil {
  1774  			t.Errorf("%s: didn't expect error: %v", tc.name, err)
  1775  		}
  1776  		if len(result) != 1 {
  1777  			t.Fatalf("%s: unexpected number of results: %v", tc.name, len(result))
  1778  		}
  1779  		if result[0].Number != issueNum {
  1780  			t.Errorf("%s: expected issue number %+v, got %+v", tc.name, issueNum, result[0].Number)
  1781  		}
  1782  		if tc.sort && !strings.Contains(result[0].Title, "sort="+sort) {
  1783  			t.Errorf("%s: missing sort=%s from query: %s", tc.name, sort, result[0].Title)
  1784  		}
  1785  		if tc.order && !strings.Contains(result[0].Title, "order=asc") {
  1786  			t.Errorf("%s: missing order=asc from query: %s", tc.name, result[0].Title)
  1787  		}
  1788  	}
  1789  }
  1790  
  1791  func TestGetFile(t *testing.T) {
  1792  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1793  		if r.Method != http.MethodGet {
  1794  			t.Errorf("Bad method: %s", r.Method)
  1795  		}
  1796  		if r.URL.Path != "/repos/k8s/kuber/contents/foo.txt" {
  1797  			t.Errorf("Bad request path: %s", r.URL.Path)
  1798  		}
  1799  		if r.URL.RawQuery != "" {
  1800  			t.Errorf("Bad request query: %s", r.URL.RawQuery)
  1801  		}
  1802  		c := &Content{
  1803  			Content: base64.StdEncoding.EncodeToString([]byte("abcde")),
  1804  		}
  1805  		b, err := json.Marshal(&c)
  1806  		if err != nil {
  1807  			t.Fatalf("Didn't expect error: %v", err)
  1808  		}
  1809  		fmt.Fprint(w, string(b))
  1810  	}))
  1811  	defer ts.Close()
  1812  	c := getClient(ts.URL)
  1813  	if content, err := c.GetFile("k8s", "kuber", "foo.txt", ""); err != nil {
  1814  		t.Errorf("Didn't expect error: %v", err)
  1815  	} else if string(content) != "abcde" {
  1816  		t.Errorf("Wrong content -- expect: abcde, got: %s", string(content))
  1817  	}
  1818  }
  1819  
  1820  func TestGetFileRef(t *testing.T) {
  1821  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1822  		if r.Method != http.MethodGet {
  1823  			t.Errorf("Bad method: %s", r.Method)
  1824  		}
  1825  		if r.URL.Path != "/repos/k8s/kuber/contents/foo/bar.txt" {
  1826  			t.Errorf("Bad request path: %s", r.URL)
  1827  		}
  1828  		if r.URL.RawQuery != "ref=12345" {
  1829  			t.Errorf("Bad request query: %s", r.URL.RawQuery)
  1830  		}
  1831  		c := &Content{
  1832  			Content: base64.StdEncoding.EncodeToString([]byte("abcde")),
  1833  		}
  1834  		b, err := json.Marshal(&c)
  1835  		if err != nil {
  1836  			t.Fatalf("Didn't expect error: %v", err)
  1837  		}
  1838  		fmt.Fprint(w, string(b))
  1839  	}))
  1840  	defer ts.Close()
  1841  	c := getClient(ts.URL)
  1842  	if content, err := c.GetFile("k8s", "kuber", "foo/bar.txt", "12345"); err != nil {
  1843  		t.Errorf("Didn't expect error: %v", err)
  1844  	} else if string(content) != "abcde" {
  1845  		t.Errorf("Wrong content -- expect: abcde, got: %s", string(content))
  1846  	}
  1847  }
  1848  
  1849  // TestGetLabels tests both GetRepoLabels and GetIssueLabels.
  1850  func TestGetLabels(t *testing.T) {
  1851  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1852  		if r.Method != http.MethodGet {
  1853  			t.Errorf("Bad method: %s", r.Method)
  1854  		}
  1855  		var labels []Label
  1856  		switch r.URL.Path {
  1857  		case "/repos/k8s/kuber/issues/5/labels":
  1858  			labels = []Label{{Name: "issue-label"}}
  1859  			w.Header().Set("Link", fmt.Sprintf(`<blorp>; rel="first", <https://%s/someotherpath>; rel="next"`, r.Host))
  1860  		case "/repos/k8s/kuber/labels":
  1861  			labels = []Label{{Name: "repo-label"}}
  1862  			w.Header().Set("Link", fmt.Sprintf(`<blorp>; rel="first", <https://%s/someotherpath>; rel="next"`, r.Host))
  1863  		case "/someotherpath":
  1864  			labels = []Label{{Name: "label2"}}
  1865  		default:
  1866  			t.Errorf("Bad request path: %s", r.URL.Path)
  1867  			return
  1868  		}
  1869  		b, err := json.Marshal(labels)
  1870  		if err != nil {
  1871  			t.Fatalf("Didn't expect error: %v", err)
  1872  		}
  1873  		fmt.Fprint(w, string(b))
  1874  	}))
  1875  	defer ts.Close()
  1876  	c := getClient(ts.URL)
  1877  	labels, err := c.GetIssueLabels("k8s", "kuber", 5)
  1878  	if err != nil {
  1879  		t.Errorf("Didn't expect error: %v", err)
  1880  	} else if len(labels) != 2 {
  1881  		t.Errorf("Expected two labels, found %d: %v", len(labels), labels)
  1882  	} else if labels[0].Name != "issue-label" || labels[1].Name != "label2" {
  1883  		t.Errorf("Wrong label names: %v", labels)
  1884  	}
  1885  
  1886  	labels, err = c.GetRepoLabels("k8s", "kuber")
  1887  	if err != nil {
  1888  		t.Errorf("Didn't expect error: %v", err)
  1889  	} else if len(labels) != 2 {
  1890  		t.Errorf("Expected two labels, found %d: %v", len(labels), labels)
  1891  	} else if labels[0].Name != "repo-label" || labels[1].Name != "label2" {
  1892  		t.Errorf("Wrong label names: %v", labels)
  1893  	}
  1894  }
  1895  
  1896  func simpleTestServer(t *testing.T, path string, v interface{}, statusCode int) *httptest.Server {
  1897  	return httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1898  		if r.URL.Path == path {
  1899  			b, err := json.Marshal(v)
  1900  			if err != nil {
  1901  				t.Fatalf("Didn't expect error: %v", err)
  1902  			}
  1903  			w.WriteHeader(statusCode)
  1904  			fmt.Fprint(w, string(b))
  1905  		} else {
  1906  			t.Fatalf("Bad request path: %s", r.URL.Path)
  1907  		}
  1908  	}))
  1909  }
  1910  
  1911  func TestListTeams(t *testing.T) {
  1912  	ts := simpleTestServer(t, "/orgs/foo/teams", []Team{{ID: 1}}, http.StatusOK)
  1913  	defer ts.Close()
  1914  	c := getClient(ts.URL)
  1915  	teams, err := c.ListTeams("foo")
  1916  	if err != nil {
  1917  		t.Errorf("Didn't expect error: %v", err)
  1918  	} else if len(teams) != 1 {
  1919  		t.Errorf("Expected one team, found %d: %v", len(teams), teams)
  1920  	} else if teams[0].ID != 1 {
  1921  		t.Errorf("Wrong team names: %v", teams)
  1922  	}
  1923  }
  1924  
  1925  func TestDeleteTeamBySlug(t *testing.T) {
  1926  	ts := simpleTestServer(t, "/orgs/foo/teams/bar", nil, http.StatusNoContent)
  1927  	c := getClient(ts.URL)
  1928  	err := c.DeleteTeamBySlug("foo", "bar")
  1929  	if err != nil {
  1930  		t.Fatalf("Didn't expect error: %v", err)
  1931  	}
  1932  }
  1933  
  1934  func TestCreateTeam(t *testing.T) {
  1935  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1936  		if r.Method != http.MethodPost {
  1937  			t.Errorf("Bad method: %s", r.Method)
  1938  		}
  1939  		if r.URL.Path != "/orgs/foo/teams" {
  1940  			t.Errorf("Bad request path: %s", r.URL.Path)
  1941  		}
  1942  		b, err := io.ReadAll(r.Body)
  1943  		if err != nil {
  1944  			t.Fatalf("Could not read request body: %v", err)
  1945  		}
  1946  		var team Team
  1947  		switch err := json.Unmarshal(b, &team); {
  1948  		case err != nil:
  1949  			t.Errorf("Could not unmarshal request: %v", err)
  1950  		case team.Name == "":
  1951  			t.Errorf("client should reject empty names")
  1952  		case team.Name != "frobber":
  1953  			t.Errorf("Bad name: %s", team.Name)
  1954  		}
  1955  		team.Name = "hello"
  1956  		team.Description = "world"
  1957  		team.Privacy = "special"
  1958  		b, err = json.Marshal(team)
  1959  		if err != nil {
  1960  			t.Fatalf("Didn't expect error: %v", err)
  1961  		}
  1962  		w.WriteHeader(http.StatusCreated) // 201
  1963  		fmt.Fprint(w, string(b))
  1964  	}))
  1965  	defer ts.Close()
  1966  	c := getClient(ts.URL)
  1967  	if _, err := c.CreateTeam("foo", Team{Name: ""}); err == nil {
  1968  		t.Errorf("client should reject empty name")
  1969  	}
  1970  	switch team, err := c.CreateTeam("foo", Team{Name: "frobber"}); {
  1971  	case err != nil:
  1972  		t.Errorf("unexpected error: %v", err)
  1973  	case team.Name != "hello":
  1974  		t.Errorf("bad name: %s", team.Name)
  1975  	case team.Description != "world":
  1976  		t.Errorf("bad description: %s", team.Description)
  1977  	case team.Privacy != "special":
  1978  		t.Errorf("bad privacy: %s", team.Privacy)
  1979  	}
  1980  }
  1981  
  1982  func TestEditTeam(t *testing.T) {
  1983  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1984  		if r.Method != http.MethodPatch {
  1985  			t.Errorf("Bad method: %s", r.Method)
  1986  		}
  1987  		if r.URL.Path != "/orgs/someOrg/teams/some-team" {
  1988  			t.Errorf("Bad request path: %s", r.URL.Path)
  1989  		}
  1990  		b, err := io.ReadAll(r.Body)
  1991  		if err != nil {
  1992  			t.Fatalf("Could not read request body: %v", err)
  1993  		}
  1994  		var team Team
  1995  		switch err := json.Unmarshal(b, &team); {
  1996  		case err != nil:
  1997  			t.Errorf("Could not unmarshal request: %v", err)
  1998  		case team.Name == "":
  1999  			t.Errorf("Bad name: %s", team.Name)
  2000  		}
  2001  		team.Name = "hello"
  2002  		team.Description = "world"
  2003  		team.Privacy = "special"
  2004  		b, err = json.Marshal(team)
  2005  		if err != nil {
  2006  			t.Fatalf("Didn't expect error: %v", err)
  2007  		}
  2008  		w.WriteHeader(http.StatusCreated) // 201
  2009  		fmt.Fprint(w, string(b))
  2010  	}))
  2011  	defer ts.Close()
  2012  	c := getClient(ts.URL)
  2013  	if _, err := c.EditTeam("", Team{Slug: "", Name: "frobber"}); err == nil {
  2014  		t.Errorf("client should reject an empty slug")
  2015  	}
  2016  	switch team, err := c.EditTeam("someOrg", Team{Slug: "some-team", Name: "frobber"}); {
  2017  	case err != nil:
  2018  		t.Errorf("unexpected error: %v", err)
  2019  	case team.Name != "hello":
  2020  		t.Errorf("bad name: %s", team.Name)
  2021  	case team.Description != "world":
  2022  		t.Errorf("bad description: %s", team.Description)
  2023  	case team.Privacy != "special":
  2024  		t.Errorf("bad privacy: %s", team.Privacy)
  2025  	}
  2026  }
  2027  
  2028  func TestListTeamMembers(t *testing.T) {
  2029  	ts := simpleTestServer(t, "/teams/1/members", []TeamMember{{Login: "foo"}}, http.StatusOK)
  2030  	defer ts.Close()
  2031  	c := getClient(ts.URL)
  2032  	teamMembers, err := c.ListTeamMembers("orgName", 1, RoleAll)
  2033  	if err != nil {
  2034  		t.Errorf("Didn't expect error: %v", err)
  2035  	} else if len(teamMembers) != 1 {
  2036  		t.Errorf("Expected one team member, found %d: %v", len(teamMembers), teamMembers)
  2037  	} else if teamMembers[0].Login != "foo" {
  2038  		t.Errorf("Wrong team names: %v", teamMembers)
  2039  	}
  2040  }
  2041  
  2042  func TestListTeamMembersBySlug(t *testing.T) {
  2043  	ts := simpleTestServer(t, "/orgs/orgName/teams/team-name/members", []TeamMember{{Login: "foo"}}, http.StatusOK)
  2044  	defer ts.Close()
  2045  	c := getClient(ts.URL)
  2046  	teamMembers, err := c.ListTeamMembersBySlug("orgName", "team-name", RoleAll)
  2047  	if err != nil {
  2048  		t.Errorf("Didn't expect error: %v", err)
  2049  	} else if len(teamMembers) != 1 {
  2050  		t.Errorf("Expected one team member, found %d: %v", len(teamMembers), teamMembers)
  2051  	} else if teamMembers[0].Login != "foo" {
  2052  		t.Errorf("Wrong team names: %v", teamMembers)
  2053  	}
  2054  }
  2055  
  2056  func TestIsCollaborator(t *testing.T) {
  2057  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2058  		if r.Method != http.MethodGet {
  2059  			t.Errorf("Bad method: %s", r.Method)
  2060  		}
  2061  		if r.URL.Path != "/repos/k8s/kuber/collaborators/person" {
  2062  			t.Errorf("Bad request path: %s", r.URL.Path)
  2063  		}
  2064  		http.Error(w, "204 No Content", http.StatusNoContent)
  2065  	}))
  2066  	defer ts.Close()
  2067  	c := getClient(ts.URL)
  2068  	mem, err := c.IsCollaborator("k8s", "kuber", "person")
  2069  	if err != nil {
  2070  		t.Errorf("Didn't expect error: %v", err)
  2071  	} else if !mem {
  2072  		t.Errorf("Should be member.")
  2073  	}
  2074  }
  2075  
  2076  func TestListCollaborators(t *testing.T) {
  2077  	ts := simpleTestServer(t, "/repos/org/repo/collaborators", []User{
  2078  		{Login: "foo", Permissions: RepoPermissions{Pull: true}},
  2079  		{Login: "bar", Permissions: RepoPermissions{Push: true}},
  2080  	}, http.StatusOK)
  2081  	defer ts.Close()
  2082  	c := getClient(ts.URL)
  2083  	users, err := c.ListCollaborators("org", "repo")
  2084  	if err != nil {
  2085  		t.Errorf("Didn't expect error: %v", err)
  2086  	} else if len(users) != 2 {
  2087  		t.Errorf("Expected two users, found %d: %v", len(users), users)
  2088  		return
  2089  	}
  2090  	if users[0].Login != "foo" {
  2091  		t.Errorf("Wrong user login for index 0: %v", users[0])
  2092  	}
  2093  	if !reflect.DeepEqual(users[0].Permissions, RepoPermissions{Pull: true}) {
  2094  		t.Errorf("Wrong permissions for index 0: %v", users[0])
  2095  	}
  2096  	if users[1].Login != "bar" {
  2097  		t.Errorf("Wrong user login for index 1: %v", users[1])
  2098  	}
  2099  	if !reflect.DeepEqual(users[1].Permissions, RepoPermissions{Push: true}) {
  2100  		t.Errorf("Wrong permissions for index 1: %v", users[1])
  2101  	}
  2102  }
  2103  
  2104  func TestListRepoTeams(t *testing.T) {
  2105  	expectedTeams := []Team{
  2106  		{ID: 1, Slug: "foo", Permission: RepoPull},
  2107  		{ID: 2, Slug: "bar", Permission: RepoPush},
  2108  		{ID: 3, Slug: "foobar", Permission: RepoAdmin},
  2109  	}
  2110  	ts := simpleTestServer(t, "/repos/org/repo/teams", expectedTeams, http.StatusOK)
  2111  	defer ts.Close()
  2112  	c := getClient(ts.URL)
  2113  	teams, err := c.ListRepoTeams("org", "repo")
  2114  	if err != nil {
  2115  		t.Errorf("Didn't expect error: %v", err)
  2116  	} else if len(teams) != 3 {
  2117  		t.Errorf("Expected three teams, found %d: %v", len(teams), teams)
  2118  		return
  2119  	}
  2120  	if !reflect.DeepEqual(teams, expectedTeams) {
  2121  		t.Errorf("Wrong list of teams, expected: %v, got: %v", expectedTeams, teams)
  2122  	}
  2123  }
  2124  func TestListIssueEvents(t *testing.T) {
  2125  	ts := simpleTestServer(t, "/repos/org/repo/issues/1/events", []ListedIssueEvent{
  2126  		{
  2127  			ID:       1,
  2128  			Event:    IssueActionClosed,
  2129  			CommitID: "6dcb09b5b57875f334f61aebed695e2e4193db5e",
  2130  		},
  2131  		{
  2132  			ID:    2,
  2133  			Event: IssueActionOpened,
  2134  		},
  2135  	}, http.StatusOK)
  2136  	defer ts.Close()
  2137  	c := getClient(ts.URL)
  2138  	events, err := c.ListIssueEvents("org", "repo", 1)
  2139  	if err != nil {
  2140  		t.Errorf("Didn't expect error: %v", err)
  2141  	} else if len(events) != 2 {
  2142  		t.Errorf("Expected two events, found %d: %v", len(events), events)
  2143  		return
  2144  	}
  2145  	if events[0].Event != IssueActionClosed {
  2146  		t.Errorf("Wrong event for index 0: %v", events[0])
  2147  	}
  2148  	if events[1].Event != IssueActionOpened {
  2149  		t.Errorf("Wrong event for index 1: %v", events[1])
  2150  	}
  2151  	if events[0].CommitID != "6dcb09b5b57875f334f61aebed695e2e4193db5e" {
  2152  		t.Errorf("Wrong commit id for index 0: %v", events[0])
  2153  	}
  2154  }
  2155  
  2156  func TestUpdateTeamMembershipBySlug(t *testing.T) {
  2157  	ts := simpleTestServer(t, "/orgs/foo/teams/bar/memberships/baz", TeamMembership{
  2158  		Membership: Membership{
  2159  			Role: RoleMaintainer,
  2160  		},
  2161  	}, http.StatusOK)
  2162  	c := getClient(ts.URL)
  2163  	tm, err := c.UpdateTeamMembershipBySlug("foo", "bar", "baz", true)
  2164  	if err != nil {
  2165  		t.Fatalf("Didn't expect error: %v", err)
  2166  	}
  2167  	if tm.Role != RoleMaintainer {
  2168  		t.Fatalf("Wrong role: %s, expected: %s", tm.Role, RoleMaintainer)
  2169  	}
  2170  }
  2171  
  2172  func TestRemoveTeamMembershipBySlug(t *testing.T) {
  2173  	ts := simpleTestServer(t, "/orgs/foo/teams/bar/memberships/baz", nil, http.StatusNoContent)
  2174  	c := getClient(ts.URL)
  2175  	err := c.RemoveTeamMembershipBySlug("foo", "bar", "baz")
  2176  	if err != nil {
  2177  		t.Fatalf("Didn't expect error: %v", err)
  2178  	}
  2179  }
  2180  
  2181  func TestGetBranches(t *testing.T) {
  2182  	ts := simpleTestServer(t, "/repos/org/repo/branches", []Branch{
  2183  		{Name: "master", Protected: false},
  2184  		{Name: "release-3.7", Protected: true},
  2185  	}, http.StatusOK)
  2186  	defer ts.Close()
  2187  	c := getClient(ts.URL)
  2188  	branches, err := c.GetBranches("org", "repo", true)
  2189  	if err != nil {
  2190  		t.Errorf("Unexpected error: %v", err)
  2191  	} else if len(branches) != 2 {
  2192  		t.Errorf("Expected two branches, found %d, %v", len(branches), branches)
  2193  		return
  2194  	}
  2195  	switch {
  2196  	case branches[0].Name != "master":
  2197  		t.Errorf("Wrong branch name for index 0: %v", branches[0])
  2198  	case branches[1].Name != "release-3.7":
  2199  		t.Errorf("Wrong branch name for index 1: %v", branches[1])
  2200  	case branches[1].Protected == false:
  2201  		t.Errorf("Wrong branch protection for index 1: %v", branches[1])
  2202  	}
  2203  }
  2204  
  2205  func TestGetBranchProtection(t *testing.T) {
  2206  	contexts := []string{"foo-pr-test", "other"}
  2207  	pushers := []Team{{Slug: "movers"}, {Slug: "awesome-team"}, {Slug: "shakers"}}
  2208  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2209  		if r.Method != http.MethodGet {
  2210  			t.Errorf("Bad method: %s", r.Method)
  2211  		}
  2212  		if r.URL.Path != "/repos/org/repo/branches/master/protection" {
  2213  			t.Errorf("Bad request path: %s", r.URL.Path)
  2214  		}
  2215  		bp := BranchProtection{
  2216  			RequiredStatusChecks: &RequiredStatusChecks{
  2217  				Contexts: contexts,
  2218  			},
  2219  			Restrictions: &Restrictions{
  2220  				Teams: pushers,
  2221  			},
  2222  			AllowForcePushes: AllowForcePushes{
  2223  				Enabled: true,
  2224  			},
  2225  		}
  2226  		b, err := json.Marshal(&bp)
  2227  		if err != nil {
  2228  			t.Fatalf("Didn't expect error: %v", err)
  2229  		}
  2230  		fmt.Fprint(w, string(b))
  2231  	}))
  2232  	defer ts.Close()
  2233  	c := getClient(ts.URL)
  2234  	bp, err := c.GetBranchProtection("org", "repo", "master")
  2235  	if err != nil {
  2236  		t.Errorf("Didn't expect error: %v", err)
  2237  	}
  2238  	switch {
  2239  	case !bp.AllowForcePushes.Enabled:
  2240  		t.Errorf("AllowForcePushes is not enabled")
  2241  	case bp.Restrictions == nil:
  2242  		t.Errorf("RestrictionsRequest unset")
  2243  	case bp.Restrictions.Teams == nil:
  2244  		t.Errorf("Teams unset")
  2245  	case len(bp.Restrictions.Teams) != len(pushers):
  2246  		t.Errorf("Bad teams: expected %v, got: %v", pushers, bp.Restrictions.Teams)
  2247  	case bp.RequiredStatusChecks == nil:
  2248  		t.Errorf("RequiredStatusChecks unset")
  2249  	case len(bp.RequiredStatusChecks.Contexts) != len(contexts):
  2250  		t.Errorf("Bad contexts: expected: %v, got: %v", contexts, bp.RequiredStatusChecks.Contexts)
  2251  	default:
  2252  		mc := map[string]bool{}
  2253  		for _, k := range bp.RequiredStatusChecks.Contexts {
  2254  			mc[k] = true
  2255  		}
  2256  		var missing []string
  2257  		for _, k := range contexts {
  2258  			if mc[k] != true {
  2259  				missing = append(missing, k)
  2260  			}
  2261  		}
  2262  		if n := len(missing); n > 0 {
  2263  			t.Errorf("missing %d required contexts: %v", n, missing)
  2264  		}
  2265  		mp := map[string]bool{}
  2266  		for _, k := range bp.Restrictions.Teams {
  2267  			mp[k.Slug] = true
  2268  		}
  2269  		missing = nil
  2270  		for _, k := range pushers {
  2271  			if mp[k.Slug] != true {
  2272  				missing = append(missing, k.Slug)
  2273  			}
  2274  		}
  2275  		if n := len(missing); n > 0 {
  2276  			t.Errorf("missing %d pushers: %v", n, missing)
  2277  		}
  2278  	}
  2279  }
  2280  
  2281  // GetBranchProtection should return nil if the github API call
  2282  // returns 404 with "Branch not protected" message
  2283  func TestGetBranchProtection404BranchNotProtected(t *testing.T) {
  2284  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2285  		if r.Method != http.MethodGet {
  2286  			t.Errorf("Bad method: %s", r.Method)
  2287  		}
  2288  		if r.URL.Path != "/repos/org/repo/branches/master/protection" {
  2289  			t.Errorf("Bad request path: %s", r.URL.Path)
  2290  		}
  2291  		ge := &githubError{
  2292  			Message: "Branch not protected",
  2293  		}
  2294  		b, err := json.Marshal(&ge)
  2295  		if err != nil {
  2296  			t.Fatalf("Didn't expect error: %v", err)
  2297  		}
  2298  		http.Error(w, string(b), http.StatusNotFound)
  2299  	}))
  2300  	defer ts.Close()
  2301  	c := getClient(ts.URL)
  2302  	bp, err := c.GetBranchProtection("org", "repo", "master")
  2303  	if err != nil {
  2304  		t.Errorf("Unexpected error: %v", err)
  2305  	}
  2306  	if bp != nil {
  2307  		t.Errorf("Expected nil as BranchProtection object, got: %v", *bp)
  2308  	}
  2309  }
  2310  
  2311  // GetBranchProtection should fail on any 404 which is NOT due to
  2312  // branch not being protected.
  2313  func TestGetBranchProtectionFailsOnOther404(t *testing.T) {
  2314  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2315  		if r.Method != http.MethodGet {
  2316  			t.Errorf("Bad method: %s", r.Method)
  2317  		}
  2318  		if r.URL.Path != "/repos/org/repo/branches/master/protection" {
  2319  			t.Errorf("Bad request path: %s", r.URL.Path)
  2320  		}
  2321  		ge := &githubError{
  2322  			Message: "Not Found",
  2323  		}
  2324  		b, err := json.Marshal(&ge)
  2325  		if err != nil {
  2326  			t.Fatalf("Didn't expect error: %v", err)
  2327  		}
  2328  		http.Error(w, string(b), http.StatusNotFound)
  2329  	}))
  2330  	defer ts.Close()
  2331  	c := getClient(ts.URL)
  2332  	_, err := c.GetBranchProtection("org", "repo", "master")
  2333  	if err == nil {
  2334  		t.Errorf("Expected error, got nil")
  2335  	}
  2336  }
  2337  
  2338  func TestRemoveBranchProtection(t *testing.T) {
  2339  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2340  		if r.Method != http.MethodDelete {
  2341  			t.Errorf("Bad method: %s", r.Method)
  2342  		}
  2343  		if r.URL.Path != "/repos/org/repo/branches/master/protection" {
  2344  			t.Errorf("Bad request path: %s", r.URL.Path)
  2345  		}
  2346  		http.Error(w, "204 No Content", http.StatusNoContent)
  2347  	}))
  2348  	defer ts.Close()
  2349  	c := getClient(ts.URL)
  2350  	if err := c.RemoveBranchProtection("org", "repo", "master"); err != nil {
  2351  		t.Errorf("Unexpected error: %v", err)
  2352  	}
  2353  }
  2354  
  2355  func TestUpdateBranchProtection(t *testing.T) {
  2356  	cases := []struct {
  2357  		name string
  2358  		// TODO(fejta): expand beyond contexts/pushers
  2359  		contexts []string
  2360  		pushers  []string
  2361  		err      bool
  2362  	}{
  2363  		{
  2364  			name:     "both",
  2365  			contexts: []string{"foo-pr-test", "other"},
  2366  			pushers:  []string{"movers", "awesome-team", "shakers"},
  2367  			err:      false,
  2368  		},
  2369  	}
  2370  
  2371  	for _, tc := range cases {
  2372  		ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2373  			if r.Method != http.MethodPut {
  2374  				t.Errorf("Bad method: %s", r.Method)
  2375  			}
  2376  			if r.URL.Path != "/repos/org/repo/branches/master/protection" {
  2377  				t.Errorf("Bad request path: %s", r.URL.Path)
  2378  			}
  2379  			b, err := io.ReadAll(r.Body)
  2380  			if err != nil {
  2381  				t.Fatalf("Could not read request body: %v", err)
  2382  			}
  2383  			var bpr BranchProtectionRequest
  2384  			if err := json.Unmarshal(b, &bpr); err != nil {
  2385  				t.Errorf("Could not unmarshal request: %v", err)
  2386  			}
  2387  			switch {
  2388  			case bpr.Restrictions != nil && bpr.Restrictions.Teams == nil:
  2389  				t.Errorf("Teams unset")
  2390  			case len(bpr.RequiredStatusChecks.Contexts) != len(tc.contexts):
  2391  				t.Errorf("Bad contexts: %v", bpr.RequiredStatusChecks.Contexts)
  2392  			case len(*bpr.Restrictions.Teams) != len(tc.pushers):
  2393  				t.Errorf("Bad teams: %v", *bpr.Restrictions.Teams)
  2394  			default:
  2395  				mc := map[string]bool{}
  2396  				for _, k := range tc.contexts {
  2397  					mc[k] = true
  2398  				}
  2399  				var missing []string
  2400  				for _, k := range bpr.RequiredStatusChecks.Contexts {
  2401  					if mc[k] != true {
  2402  						missing = append(missing, k)
  2403  					}
  2404  				}
  2405  				if n := len(missing); n > 0 {
  2406  					t.Errorf("%s: missing %d required contexts: %v", tc.name, n, missing)
  2407  				}
  2408  				mp := map[string]bool{}
  2409  				for _, k := range tc.pushers {
  2410  					mp[k] = true
  2411  				}
  2412  				missing = nil
  2413  				for _, k := range *bpr.Restrictions.Teams {
  2414  					if mp[k] != true {
  2415  						missing = append(missing, k)
  2416  					}
  2417  				}
  2418  				if n := len(missing); n > 0 {
  2419  					t.Errorf("%s: missing %d pushers: %v", tc.name, n, missing)
  2420  				}
  2421  			}
  2422  			http.Error(w, "200 OK", http.StatusOK)
  2423  		}))
  2424  		defer ts.Close()
  2425  		c := getClient(ts.URL)
  2426  
  2427  		err := c.UpdateBranchProtection("org", "repo", "master", BranchProtectionRequest{
  2428  			RequiredStatusChecks: &RequiredStatusChecks{
  2429  				Contexts: tc.contexts,
  2430  			},
  2431  			Restrictions: &RestrictionsRequest{
  2432  				Teams: &tc.pushers,
  2433  			},
  2434  		})
  2435  		if tc.err && err == nil {
  2436  			t.Errorf("%s: expected error failed to occur", tc.name)
  2437  		}
  2438  		if !tc.err && err != nil {
  2439  			t.Errorf("%s: received unexpected error: %v", tc.name, err)
  2440  		}
  2441  	}
  2442  }
  2443  
  2444  func TestClearMilestone(t *testing.T) {
  2445  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2446  		if r.Method != http.MethodPatch {
  2447  			t.Errorf("Bad method: %s", r.Method)
  2448  		}
  2449  		if r.URL.Path != "/repos/k8s/kuber/issues/5" {
  2450  			t.Errorf("Bad request path: %s", r.URL.Path)
  2451  		}
  2452  		b, err := io.ReadAll(r.Body)
  2453  		if err != nil {
  2454  			t.Fatalf("Could not read request body: %v", err)
  2455  		}
  2456  		var issue Issue
  2457  		if err := json.Unmarshal(b, &issue); err != nil {
  2458  			t.Errorf("Could not unmarshal request: %v", err)
  2459  		} else if issue.Milestone.Title != "" {
  2460  			t.Errorf("Milestone title not empty: %v", issue.Milestone.Title)
  2461  		}
  2462  	}))
  2463  	defer ts.Close()
  2464  	c := getClient(ts.URL)
  2465  	if err := c.ClearMilestone("k8s", "kuber", 5); err != nil {
  2466  		t.Errorf("Didn't expect error: %v", err)
  2467  	}
  2468  }
  2469  
  2470  func TestSetMilestone(t *testing.T) {
  2471  	newMilestone := 42
  2472  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2473  		if r.Method != http.MethodPatch {
  2474  			t.Errorf("Bad method: %s", r.Method)
  2475  		}
  2476  		if r.URL.Path != "/repos/k8s/kuber/issues/5" {
  2477  			t.Errorf("Bad request path: %s", r.URL.Path)
  2478  		}
  2479  		b, err := io.ReadAll(r.Body)
  2480  		if err != nil {
  2481  			t.Fatalf("Could not read request body: %v", err)
  2482  		}
  2483  		var issue struct {
  2484  			Milestone *int `json:"milestone,omitempty"`
  2485  		}
  2486  		if err := json.Unmarshal(b, &issue); err != nil {
  2487  			t.Fatalf("Could not unmarshal request: %v", err)
  2488  		}
  2489  		if issue.Milestone == nil {
  2490  			t.Fatal("Milestone was not set.")
  2491  		}
  2492  		if *issue.Milestone != newMilestone {
  2493  			t.Errorf("Expected milestone to be set to %d, but got %d.", newMilestone, *issue.Milestone)
  2494  		}
  2495  	}))
  2496  	defer ts.Close()
  2497  	c := getClient(ts.URL)
  2498  	if err := c.SetMilestone("k8s", "kuber", 5, newMilestone); err != nil {
  2499  		t.Errorf("Didn't expect error: %v", err)
  2500  	}
  2501  }
  2502  
  2503  func TestListMilestones(t *testing.T) {
  2504  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2505  		if r.Method != http.MethodGet {
  2506  			t.Errorf("Bad method: %s", r.Method)
  2507  		}
  2508  		if r.URL.Path != "/repos/k8s/kuber/milestones" {
  2509  			t.Errorf("Bad request path: %s", r.URL.Path)
  2510  		}
  2511  	}))
  2512  	defer ts.Close()
  2513  	c := getClient(ts.URL)
  2514  	if err, _ := c.ListMilestones("k8s", "kuber"); err != nil {
  2515  		t.Errorf("Didn't expect error: %v", err)
  2516  	}
  2517  }
  2518  
  2519  func TestListPRCommits(t *testing.T) {
  2520  	ts := simpleTestServer(t, "/repos/theorg/therepo/pulls/3/commits", []RepositoryCommit{
  2521  		{SHA: "sha"},
  2522  		{SHA: "sha2"},
  2523  	}, http.StatusOK)
  2524  	defer ts.Close()
  2525  	c := getClient(ts.URL)
  2526  	if commits, err := c.ListPullRequestCommits("theorg", "therepo", 3); err != nil {
  2527  		t.Errorf("Didn't expect error: %v", err)
  2528  	} else {
  2529  		if len(commits) != 2 {
  2530  			t.Errorf("Expected 2 commits to be returned, but got %d", len(commits))
  2531  		}
  2532  	}
  2533  }
  2534  
  2535  func TestUpdatePullRequestBranch(t *testing.T) {
  2536  	sha := "74053d555d71a14e3853b97e204d7d6415521375"
  2537  	mismatchedSha := "mismatchedSha"
  2538  
  2539  	testcases := []struct {
  2540  		name            string
  2541  		expectedHeadSha *string
  2542  		forceMismatch   bool
  2543  		err             bool
  2544  	}{
  2545  		{
  2546  			name:            "nil expectedHeadSha",
  2547  			expectedHeadSha: nil,
  2548  			err:             false,
  2549  		},
  2550  		{
  2551  			name:            "nil mismatched expectedHeadSha",
  2552  			expectedHeadSha: nil,
  2553  			forceMismatch:   true,
  2554  			err:             true,
  2555  		},
  2556  		{
  2557  			name:            "matched expectedHeadSha",
  2558  			expectedHeadSha: &sha,
  2559  			err:             false,
  2560  		},
  2561  		{
  2562  			name:            "mismatched expectedHeadSha",
  2563  			expectedHeadSha: &mismatchedSha,
  2564  			err:             true,
  2565  		},
  2566  	}
  2567  
  2568  	for _, tc := range testcases {
  2569  		ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2570  			if r.Method != http.MethodPut {
  2571  				t.Errorf("Bad method: %s", r.Method)
  2572  			}
  2573  
  2574  			if r.URL.Path != "/repos/k8s/kuber/pulls/5/update-branch" {
  2575  				t.Errorf("Bad request path: %s", r.URL.Path)
  2576  			}
  2577  
  2578  			b, err := io.ReadAll(r.Body)
  2579  			if err != nil {
  2580  				t.Fatalf("Could not read request body: %v", err)
  2581  			}
  2582  
  2583  			var data struct {
  2584  				ExpectedHeadSha *string `json:"expected_head_sha,omitempty"`
  2585  			}
  2586  			if err := json.Unmarshal(b, &data); err != nil {
  2587  				t.Errorf("Could not unmarshal request: %v", err)
  2588  			}
  2589  
  2590  			if data.ExpectedHeadSha != nil && *data.ExpectedHeadSha != sha {
  2591  				http.Error(w, "422 Unprocessable Entity", http.StatusUnprocessableEntity)
  2592  			} else if tc.forceMismatch == true {
  2593  				http.Error(w, "422 Unprocessable Entity", http.StatusUnprocessableEntity)
  2594  			} else {
  2595  				http.Error(w, "202 Accepted", http.StatusAccepted)
  2596  			}
  2597  		}))
  2598  		defer ts.Close()
  2599  
  2600  		c := getClient(ts.URL)
  2601  		err := c.UpdatePullRequestBranch("k8s", "kuber", 5, tc.expectedHeadSha)
  2602  		if tc.err && err == nil {
  2603  			t.Errorf("%s: expected error failed to occur", tc.name)
  2604  		}
  2605  		if !tc.err && err != nil {
  2606  			t.Errorf("%s: received unexpected error: %v", tc.name, err)
  2607  		}
  2608  	}
  2609  }
  2610  
  2611  func TestCombinedStatus(t *testing.T) {
  2612  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2613  		if r.Method != http.MethodGet {
  2614  			t.Errorf("Bad method: %s", r.Method)
  2615  		}
  2616  		if r.URL.Path == "/repos/k8s/kuber/commits/SHA/status" {
  2617  			statuses := CombinedStatus{
  2618  				SHA:      "SHA",
  2619  				Statuses: []Status{{Context: "foo"}},
  2620  			}
  2621  			b, err := json.Marshal(statuses)
  2622  			if err != nil {
  2623  				t.Fatalf("Didn't expect error: %v", err)
  2624  			}
  2625  			w.Header().Set("Link", fmt.Sprintf(`<blorp>; rel="first", <https://%s/someotherpath>; rel="next"`, r.Host))
  2626  			fmt.Fprint(w, string(b))
  2627  		} else if r.URL.Path == "/someotherpath" {
  2628  			statuses := CombinedStatus{
  2629  				SHA:      "SHA",
  2630  				Statuses: []Status{{Context: "bar"}},
  2631  			}
  2632  			b, err := json.Marshal(statuses)
  2633  			if err != nil {
  2634  				t.Fatalf("Didn't expect error: %v", err)
  2635  			}
  2636  			fmt.Fprint(w, string(b))
  2637  		} else {
  2638  			t.Errorf("Bad request path: %s", r.URL.Path)
  2639  		}
  2640  	}))
  2641  	defer ts.Close()
  2642  	c := getClient(ts.URL)
  2643  	combined, err := c.GetCombinedStatus("k8s", "kuber", "SHA")
  2644  	if err != nil {
  2645  		t.Errorf("Didn't expect error: %v", err)
  2646  	} else if combined.SHA != "SHA" {
  2647  		t.Errorf("Expected SHA 'SHA', found %s", combined.SHA)
  2648  	} else if len(combined.Statuses) != 2 {
  2649  		t.Errorf("Expected two statuses, found %d: %v", len(combined.Statuses), combined.Statuses)
  2650  	} else if combined.Statuses[0].Context != "foo" || combined.Statuses[1].Context != "bar" {
  2651  		t.Errorf("Wrong review IDs: %v", combined.Statuses)
  2652  	}
  2653  }
  2654  
  2655  func TestCreateRepo(t *testing.T) {
  2656  	org := "org"
  2657  	usersRepoName := "users-repository"
  2658  	orgsRepoName := "orgs-repository"
  2659  	repoDesc := "description of users-repository"
  2660  	testCases := []struct {
  2661  		description string
  2662  		isUser      bool
  2663  		repo        RepoCreateRequest
  2664  		statusCode  int
  2665  
  2666  		expectError bool
  2667  		expectRepo  *FullRepo
  2668  	}{
  2669  		{
  2670  			description: "create repo as user",
  2671  			isUser:      true,
  2672  			repo: RepoCreateRequest{
  2673  				RepoRequest: RepoRequest{
  2674  					Name:        &usersRepoName,
  2675  					Description: &repoDesc,
  2676  				},
  2677  			},
  2678  			statusCode: http.StatusCreated,
  2679  			expectRepo: &FullRepo{
  2680  				Repo: Repo{
  2681  					Name:        "users-repository",
  2682  					Description: "CREATED",
  2683  				},
  2684  			},
  2685  		},
  2686  		{
  2687  			description: "create repo as org",
  2688  			isUser:      false,
  2689  			repo: RepoCreateRequest{
  2690  				RepoRequest: RepoRequest{
  2691  					Name:        &orgsRepoName,
  2692  					Description: &repoDesc,
  2693  				},
  2694  			},
  2695  			statusCode: http.StatusCreated,
  2696  			expectRepo: &FullRepo{
  2697  				Repo: Repo{
  2698  					Name:        "orgs-repository",
  2699  					Description: "CREATED",
  2700  				},
  2701  			},
  2702  		},
  2703  		{
  2704  			description: "errors are handled",
  2705  			isUser:      false,
  2706  			repo: RepoCreateRequest{
  2707  				RepoRequest: RepoRequest{
  2708  					Name:        &orgsRepoName,
  2709  					Description: &repoDesc,
  2710  				},
  2711  			},
  2712  			statusCode:  http.StatusForbidden,
  2713  			expectError: true,
  2714  		},
  2715  	}
  2716  	for _, tc := range testCases {
  2717  		t.Run(tc.description, func(t *testing.T) {
  2718  			ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2719  				if r.Method != http.MethodPost {
  2720  					t.Errorf("Bad method: %s", r.Method)
  2721  				}
  2722  				if tc.isUser && r.URL.Path != "/user/repos" {
  2723  					t.Errorf("Bad request path to create user-owned repo: %s", r.URL.Path)
  2724  				} else if !tc.isUser && r.URL.Path != "/orgs/org/repos" {
  2725  					t.Errorf("Bad request path to create org-owned repo: %s", r.URL.Path)
  2726  				}
  2727  				b, err := io.ReadAll(r.Body)
  2728  				if err != nil {
  2729  					t.Fatalf("Could not read request body: %v", err)
  2730  				}
  2731  				var repo Repo
  2732  				switch err := json.Unmarshal(b, &repo); {
  2733  				case err != nil:
  2734  					t.Errorf("Could not unmarshal request: %v", err)
  2735  				case repo.Name == "":
  2736  					t.Errorf("client should reject empty names")
  2737  				}
  2738  				repo.Description = "CREATED"
  2739  				b, err = json.Marshal(repo)
  2740  				if err != nil {
  2741  					t.Fatalf("Didn't expect error: %v", err)
  2742  				}
  2743  				w.WriteHeader(tc.statusCode) // 201
  2744  				fmt.Fprint(w, string(b))
  2745  			}))
  2746  			defer ts.Close()
  2747  			c := getClient(ts.URL)
  2748  			switch repo, err := c.CreateRepo(org, tc.isUser, tc.repo); {
  2749  			case err != nil && !tc.expectError:
  2750  				t.Errorf("unexpected error: %v", err)
  2751  			case err == nil && tc.expectError:
  2752  				t.Errorf("expected error, but got none")
  2753  			case err == nil && !reflect.DeepEqual(repo, tc.expectRepo):
  2754  				t.Errorf("%s: repo differs from expected:\n%s", tc.description, diff.ObjectReflectDiff(tc.expectRepo, repo))
  2755  			}
  2756  		})
  2757  	}
  2758  }
  2759  
  2760  func TestUpdateRepo(t *testing.T) {
  2761  	org := "org"
  2762  	repoName := "repository"
  2763  	yes := true
  2764  	testCases := []struct {
  2765  		description string
  2766  		repo        RepoUpdateRequest
  2767  		statusCode  int
  2768  
  2769  		expectError bool
  2770  		expectRepo  *FullRepo
  2771  	}{
  2772  		{
  2773  			description: "Update repository",
  2774  			repo: RepoUpdateRequest{
  2775  				RepoRequest: RepoRequest{
  2776  					Name: &repoName,
  2777  				},
  2778  				Archived: &yes,
  2779  			},
  2780  			statusCode: http.StatusOK,
  2781  			expectRepo: &FullRepo{
  2782  				Repo: Repo{
  2783  					Name:        "repository",
  2784  					Description: "UPDATED",
  2785  					Archived:    true,
  2786  				},
  2787  			},
  2788  		},
  2789  		{
  2790  			description: "errors are handled",
  2791  			repo: RepoUpdateRequest{
  2792  				RepoRequest: RepoRequest{
  2793  					Name: &repoName,
  2794  				},
  2795  				Archived: &yes,
  2796  			},
  2797  			statusCode:  http.StatusForbidden,
  2798  			expectError: true,
  2799  		},
  2800  	}
  2801  	for _, tc := range testCases {
  2802  		t.Run(tc.description, func(t *testing.T) {
  2803  			ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2804  				if r.Method != http.MethodPatch {
  2805  					t.Errorf("Bad method: %s (expected %s)", r.Method, http.MethodPatch)
  2806  				}
  2807  				expectedPath := "/repos/org/repository"
  2808  				if r.URL.Path != expectedPath {
  2809  					t.Errorf("Bad request path to create user-owned repo: %s (expected %s)", r.URL.Path, expectedPath)
  2810  				}
  2811  				b, err := io.ReadAll(r.Body)
  2812  				if err != nil {
  2813  					t.Fatalf("Could not read request body: %v", err)
  2814  				}
  2815  				var repo Repo
  2816  				switch err := json.Unmarshal(b, &repo); {
  2817  				case err != nil:
  2818  					t.Errorf("Could not unmarshal request: %v", err)
  2819  				case repo.Name == "":
  2820  					t.Errorf("client should reject empty names")
  2821  				}
  2822  				repo.Description = "UPDATED"
  2823  				b, err = json.Marshal(repo)
  2824  				if err != nil {
  2825  					t.Fatalf("Didn't expect error: %v", err)
  2826  				}
  2827  				w.WriteHeader(tc.statusCode) // 200
  2828  				fmt.Fprint(w, string(b))
  2829  			}))
  2830  			defer ts.Close()
  2831  			c := getClient(ts.URL)
  2832  			switch repo, err := c.UpdateRepo(org, repoName, tc.repo); {
  2833  			case err != nil && !tc.expectError:
  2834  				t.Errorf("unexpected error: %v", err)
  2835  			case err == nil && tc.expectError:
  2836  				t.Errorf("expected error, but got none")
  2837  			case err == nil && !reflect.DeepEqual(repo, tc.expectRepo):
  2838  				t.Errorf("%s: repo differs from expected:\n%s", tc.description, diff.ObjectReflectDiff(tc.expectRepo, repo))
  2839  			}
  2840  		})
  2841  	}
  2842  }
  2843  
  2844  type fakeHttpClient struct {
  2845  	received []*http.Request
  2846  }
  2847  
  2848  func (fhc *fakeHttpClient) Do(req *http.Request) (*http.Response, error) {
  2849  	if fhc.received == nil {
  2850  		fhc.received = []*http.Request{}
  2851  	}
  2852  	fhc.received = append(fhc.received, req)
  2853  	return &http.Response{}, nil
  2854  }
  2855  
  2856  func TestAuthHeaderGetsSet(t *testing.T) {
  2857  	t.Parallel()
  2858  	testCases := []struct {
  2859  		name           string
  2860  		mod            func(*client)
  2861  		expectedHeader http.Header
  2862  	}{
  2863  		{
  2864  			name:           "Empty token, no auth header",
  2865  			mod:            func(c *client) { c.getToken = func() []byte { return []byte{} } },
  2866  			expectedHeader: http.Header{"X-GitHub-Api-Version": []string{"2022-11-28"}},
  2867  		},
  2868  		{
  2869  			name:           "Token, auth header",
  2870  			mod:            func(c *client) { c.getToken = func() []byte { return []byte("sup") } },
  2871  			expectedHeader: http.Header{"Authorization": []string{"Bearer sup"}, "X-GitHub-Api-Version": []string{"2022-11-28"}},
  2872  		},
  2873  	}
  2874  
  2875  	for _, tc := range testCases {
  2876  		t.Run(tc.name, func(t *testing.T) {
  2877  			fake := &fakeHttpClient{}
  2878  			c := &client{delegate: &delegate{client: fake}, logger: logrus.NewEntry(logrus.New())}
  2879  			tc.mod(c)
  2880  			if _, err := c.doRequest(context.Background(), "POST", "/hello", "", "", nil); err != nil {
  2881  				t.Fatalf("unexpected error: %v", err)
  2882  			}
  2883  			if tc.expectedHeader == nil {
  2884  				tc.expectedHeader = http.Header{}
  2885  			}
  2886  			tc.expectedHeader["Accept"] = []string{"application/vnd.github.v3+json"}
  2887  
  2888  			// Bazel injects some stuff in here, exclude it from comparison so both bazel test
  2889  			// and go test yield the same result.
  2890  			delete(fake.received[0].Header, "User-Agent")
  2891  			if diff := cmp.Diff(tc.expectedHeader, fake.received[0].Header); diff != "" {
  2892  				t.Errorf("expected header differs from actual: %s", diff)
  2893  			}
  2894  		})
  2895  	}
  2896  }
  2897  
  2898  func TestListTeamReposBySlug(t *testing.T) {
  2899  	ts := simpleTestServer(t, "/orgs/orgName/teams/team-name/repos", []Repo{
  2900  		{Name: "repo-bar", Permissions: RepoPermissions{Pull: true}},
  2901  		{Name: "repo-invalid-permission-level"}}, http.StatusOK)
  2902  	defer ts.Close()
  2903  	c := getClient(ts.URL)
  2904  	repos, err := c.ListTeamReposBySlug("orgName", "team-name")
  2905  	if err != nil {
  2906  		t.Errorf("Didn't expect error: %v", err)
  2907  	} else if len(repos) != 1 {
  2908  		t.Errorf("Expected one repo, found %d: %v", len(repos), repos)
  2909  	} else if repos[0].Name != "repo-bar" {
  2910  		t.Errorf("Wrong repos: %v", repos)
  2911  	}
  2912  }
  2913  
  2914  func TestUpdateTeamRepoBySlug(t *testing.T) {
  2915  	ts := simpleTestServer(t, "/orgs/orgName/teams/team-name/repos/orgName/repo-name", nil, http.StatusNoContent)
  2916  	defer ts.Close()
  2917  	c := getClient(ts.URL)
  2918  
  2919  	err := c.UpdateTeamRepoBySlug("orgName", "team-name", "repo-name", "admin")
  2920  	if err != nil {
  2921  		t.Fatalf("Didn't expect error: %v", err)
  2922  	}
  2923  }
  2924  
  2925  func TestRemoveTeamRepoBySlug(t *testing.T) {
  2926  	ts := simpleTestServer(t, "/orgs/orgName/teams/team-name/repos/orgName/repo-name", nil, http.StatusNoContent)
  2927  	defer ts.Close()
  2928  	c := getClient(ts.URL)
  2929  
  2930  	err := c.RemoveTeamRepoBySlug("orgName", "team-name", "repo-name")
  2931  	if err != nil {
  2932  		t.Fatalf("Didn't expect error: %v", err)
  2933  	}
  2934  }
  2935  
  2936  func TestListTeamInvitationsBySlug(t *testing.T) {
  2937  	ts := simpleTestServer(t, "/orgs/orgName/teams/team-name/invitations", []OrgInvitation{
  2938  		{
  2939  			TeamMember: TeamMember{Login: "new-person"},
  2940  			Email:      "some-person@gmail.com",
  2941  			Inviter:    TeamMember{Login: "existing-person"},
  2942  		},
  2943  	}, http.StatusOK)
  2944  	defer ts.Close()
  2945  	c := getClient(ts.URL)
  2946  
  2947  	invitations, err := c.ListTeamInvitationsBySlug("orgName", "team-name")
  2948  	if err != nil {
  2949  		t.Fatalf("Didn't expect error: %v", err)
  2950  	}
  2951  	if len(invitations) != 1 {
  2952  		t.Fatalf("Wrong amount of invitations received: %d, expected: %d", len(invitations), 1)
  2953  	}
  2954  	if invitations[0].Email != "some-person@gmail.com" {
  2955  		t.Fatalf("Wrong invitation content: %s, expected: %s", invitations[0].Email, "some-person@gmail.com")
  2956  	}
  2957  }
  2958  
  2959  func TestCreateFork(t *testing.T) {
  2960  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2961  		if r.Method != http.MethodPost {
  2962  			t.Errorf("Bad method: %s", r.Method)
  2963  		}
  2964  		if r.URL.Path != "/repos/k8s/kuber/forks" {
  2965  			t.Errorf("Bad request path: %s", r.URL.Path)
  2966  		}
  2967  		w.WriteHeader(202)
  2968  		w.Write([]byte(`{"name":"other"}`))
  2969  	}))
  2970  	defer ts.Close()
  2971  	c := getClient(ts.URL)
  2972  	if name, err := c.CreateFork("k8s", "kuber"); err != nil {
  2973  		t.Errorf("Unexpected error: %v", err)
  2974  	} else {
  2975  		if name != "other" {
  2976  			t.Errorf("Unexpected fork name: %v", name)
  2977  		}
  2978  	}
  2979  }
  2980  
  2981  func TestToCurl(t *testing.T) {
  2982  	testCases := []struct {
  2983  		name     string
  2984  		request  *http.Request
  2985  		expected string
  2986  	}{
  2987  		{
  2988  			name:     "Authorization Header with bearer type gets masked",
  2989  			request:  &http.Request{Method: http.MethodGet, URL: &url.URL{Scheme: "https", Host: "api.github.com"}, Header: http.Header{"Authorization": []string{"Bearer secret-token"}}},
  2990  			expected: `curl -k -v -XGET  -H "Authorization: Bearer <masked>" 'https://api.github.com'`,
  2991  		},
  2992  		{
  2993  			name:     "Authorization Header with unknown type gets masked",
  2994  			request:  &http.Request{Method: http.MethodGet, URL: &url.URL{Scheme: "https", Host: "api.github.com"}, Header: http.Header{"Authorization": []string{"Definitely-not-valid secret-token"}}},
  2995  			expected: `curl -k -v -XGET  -H "Authorization: <masked>" 'https://api.github.com'`,
  2996  		},
  2997  	}
  2998  
  2999  	for _, tc := range testCases {
  3000  		t.Run(tc.name, func(t *testing.T) {
  3001  			if result := toCurl(tc.request); result != tc.expected {
  3002  				t.Errorf("result %s differs from expected %s", result, tc.expected)
  3003  			}
  3004  		})
  3005  	}
  3006  }
  3007  
  3008  type testRoundTripper struct {
  3009  	rt func(*http.Request) (*http.Response, error)
  3010  }
  3011  
  3012  func (rt testRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
  3013  	return rt.rt(r)
  3014  }
  3015  
  3016  // TestAllMethodsThatDoRequestSetOrgHeader uses reflect to find all methods of the Client and
  3017  // their arguments and calls them with an empty argument, then verifies via a RoundTripper that
  3018  // all requests made had an org header set.
  3019  func TestAllMethodsThatDoRequestSetOrgHeader(t *testing.T) {
  3020  	_, _, ghClient, err := NewAppsAuthClientWithFields(logrus.Fields{}, func(_ []byte) []byte { return nil }, "some-app-id", func() *rsa.PrivateKey { return nil }, "", "https://api.github.com")
  3021  	if err != nil {
  3022  		t.Fatalf("failed to construct github client: %v", err)
  3023  	}
  3024  	toSkip := sets.New[string](
  3025  		// TODO: Split the search query by org when app auth is used
  3026  		"FindIssues",
  3027  		// Bound to user, not org specific
  3028  		"ListCurrentUserRepoInvitations",
  3029  		// Bound to user, not org specific
  3030  		"AcceptUserRepoInvitation",
  3031  		// Bound to user, not org specific
  3032  		"ListCurrentUserOrgInvitations",
  3033  	)
  3034  
  3035  	clientMethods := getCallForAllClientMethodsThroughReflection(
  3036  		ghClient,
  3037  		func(method reflect.Method) bool { return toSkip.Has(method.Name) },
  3038  		func(typeName string) interface{} {
  3039  			if typeName == "string" {
  3040  				return "org"
  3041  			}
  3042  			return nil
  3043  		},
  3044  	)
  3045  	for _, clientMethod := range clientMethods {
  3046  		methodName, call := clientMethod()
  3047  		t.Run(methodName, func(t *testing.T) {
  3048  			checkingRoundTripper := testRoundTripper{func(r *http.Request) (*http.Response, error) {
  3049  				if !strings.HasPrefix(r.URL.Path, "/app") {
  3050  					var orgVal string
  3051  					if v := r.Context().Value(githubOrgContextKey); v != nil {
  3052  						orgVal = v.(string)
  3053  					}
  3054  					if expected := "org"; orgVal != expected {
  3055  						t.Errorf("Request didn't have github org key in context set to %q", expected)
  3056  					}
  3057  				}
  3058  				return &http.Response{Body: io.NopCloser(&bytes.Buffer{})}, nil
  3059  			}}
  3060  			ghClient.(*client).client.(*ghThrottler).http.(*http.Client).Transport = checkingRoundTripper
  3061  			ghClient.(*client).gqlc.(*ghThrottler).graph.(*graphQLGitHubAppsAuthClientWrapper).Client = githubv4.NewClient(&http.Client{Transport: checkingRoundTripper})
  3062  
  3063  			// We don't care about the result at all, the verification happens via the roundTripper
  3064  			_ = call()
  3065  		})
  3066  	}
  3067  }
  3068  
  3069  func getCallForAllClientMethodsThroughReflection(
  3070  	c Client,
  3071  	skip func(reflect.Method) bool,
  3072  	typeOverrides ...func(typeName string) (override interface{}),
  3073  ) (getCalls []func() (methodName string, call func() error)) {
  3074  
  3075  	clientType := reflect.TypeOf(c)
  3076  	clientValue := reflect.ValueOf(c)
  3077  
  3078  	for i := 0; i < clientType.NumMethod(); i++ {
  3079  		i := i
  3080  		if skip(clientType.Method(i)) {
  3081  			continue
  3082  		}
  3083  		var args []reflect.Value
  3084  		// First arg is self, so start with second arg
  3085  		for j := 1; j < clientType.Method(i).Func.Type().NumIn(); j++ {
  3086  			arg := reflect.New(clientType.Method(i).Func.Type().In(j)).Elem()
  3087  			setValue(&arg, typeOverrides)
  3088  
  3089  			args = append(args, arg)
  3090  		}
  3091  
  3092  		if clientType.Method(i).Type.IsVariadic() {
  3093  			args[len(args)-1] = reflect.New(args[len(args)-1].Type().Elem()).Elem()
  3094  		}
  3095  
  3096  		getCalls = append(getCalls, func() (methodName string, call func() error) {
  3097  			return clientType.Method(i).Name, func() (err error) {
  3098  				returnsValues := clientValue.Method(i).Call(args)
  3099  
  3100  				// If there are returns and the last return is a non-nil interface that has an Error method,
  3101  				// we assume it is the error.
  3102  				if len(returnsValues) > 0 &&
  3103  					returnsValues[len(returnsValues)-1].Kind() == reflect.Interface &&
  3104  					!returnsValues[len(returnsValues)-1].IsNil() &&
  3105  					!reflect.DeepEqual(returnsValues[len(returnsValues)-1].MethodByName("Error"), reflect.Value{}) {
  3106  					err = returnsValues[len(returnsValues)-1].Interface().(error)
  3107  				}
  3108  				return
  3109  			}
  3110  		})
  3111  	}
  3112  
  3113  	return getCalls
  3114  }
  3115  
  3116  func setValue(target *reflect.Value, typeOverrides []func(typeName string) (override interface{})) {
  3117  	for _, typeOverride := range typeOverrides {
  3118  		if override := typeOverride(target.Type().String()); override != nil {
  3119  			target.Set(reflect.ValueOf(override))
  3120  			return
  3121  		}
  3122  	}
  3123  
  3124  	if target.Kind() == reflect.Ptr && target.IsNil() {
  3125  		target.Set(reflect.New(target.Type().Elem()))
  3126  	}
  3127  
  3128  	// We can not deal with interface types genererically, as there
  3129  	// is no automatic way to figure out the concrete values they
  3130  	// can or should be set to.
  3131  	if target.Type().String() == "context.Context" {
  3132  		target.Set(reflect.ValueOf(context.Background()))
  3133  	}
  3134  	if target.Type().String() == "interface {}" {
  3135  		target.Set(reflect.ValueOf(map[string]interface{}{}))
  3136  	}
  3137  	if target.Type().String() == "githubv4.Input" {
  3138  		target.Set(reflect.ValueOf(struct{}{}))
  3139  	}
  3140  }
  3141  
  3142  func TestBotUserChecker(t *testing.T) {
  3143  	const savedLogin = "botName"
  3144  	testCases := []struct {
  3145  		name         string
  3146  		checkFor     string
  3147  		usesAppsAuth bool
  3148  		expectMatch  bool
  3149  	}{
  3150  		{
  3151  			name:         "Bot suffix with apps auth is recognized",
  3152  			checkFor:     savedLogin + "[bot]",
  3153  			usesAppsAuth: true,
  3154  			expectMatch:  true,
  3155  		},
  3156  		{
  3157  			name:         "No suffix with apps auth is recognized",
  3158  			checkFor:     savedLogin,
  3159  			usesAppsAuth: true,
  3160  			expectMatch:  true,
  3161  		},
  3162  		{
  3163  			name:         "No suffix without apps auth is recognized",
  3164  			checkFor:     savedLogin,
  3165  			usesAppsAuth: false,
  3166  			expectMatch:  true,
  3167  		},
  3168  		{
  3169  			name:         "Suffix without apps auth is not recognized",
  3170  			checkFor:     savedLogin + "[bot]",
  3171  			usesAppsAuth: false,
  3172  			expectMatch:  false,
  3173  		},
  3174  	}
  3175  
  3176  	for _, tc := range testCases {
  3177  		c := &client{delegate: &delegate{usesAppsAuth: tc.usesAppsAuth, userData: &UserData{Login: savedLogin}}}
  3178  
  3179  		checker, err := c.BotUserChecker()
  3180  		if err != nil {
  3181  			t.Fatalf("failed to get user checker: %v", err)
  3182  		}
  3183  		if actualMatch := checker(tc.checkFor); actualMatch != tc.expectMatch {
  3184  			t.Errorf("expect match: %t, got match: %t", tc.expectMatch, actualMatch)
  3185  		}
  3186  	}
  3187  }
  3188  
  3189  func TestV4ClientSetsUserAgent(t *testing.T) {
  3190  	// Make sure this is deterministic in tests
  3191  	version.Version = "0"
  3192  	var expectedUserAgent string
  3193  	roundTripper := testRoundTripper{func(r *http.Request) (*http.Response, error) {
  3194  		if got := r.Header.Get("User-Agent"); got != expectedUserAgent {
  3195  			return nil, fmt.Errorf("expected User-Agent %q, got %q", expectedUserAgent, got)
  3196  		}
  3197  		return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewBufferString("{}"))}, nil
  3198  	}}
  3199  
  3200  	_, _, client, err := NewClientFromOptions(
  3201  		logrus.Fields{},
  3202  		ClientOptions{
  3203  			Censor:           func(b []byte) []byte { return b },
  3204  			GetToken:         func() []byte { return nil },
  3205  			AppID:            "",
  3206  			AppPrivateKey:    nil,
  3207  			GraphqlEndpoint:  "",
  3208  			Bases:            []string{"https://api.github.com"},
  3209  			DryRun:           false,
  3210  			BaseRoundTripper: roundTripper,
  3211  		}.Default(),
  3212  	)
  3213  	if err != nil {
  3214  		t.Fatalf("failed to construct github client: %v", err)
  3215  	}
  3216  
  3217  	t.Run("User agent gets set initially", func(t *testing.T) {
  3218  		expectedUserAgent = "unset/0"
  3219  		if err := client.QueryWithGitHubAppsSupport(context.Background(), struct{}{}, nil, ""); err != nil {
  3220  			t.Error(err)
  3221  		}
  3222  		if err := client.MutateWithGitHubAppsSupport(context.Background(), struct{}{}, githubv4.Input(struct{}{}), nil, ""); err != nil {
  3223  			t.Error(err)
  3224  		}
  3225  	})
  3226  
  3227  	t.Run("ForPlugin changes the user agent accordingly", func(t *testing.T) {
  3228  		client := client.ForPlugin("test-plugin")
  3229  		expectedUserAgent = "unset.test-plugin/0"
  3230  		if err := client.QueryWithGitHubAppsSupport(context.Background(), struct{}{}, nil, ""); err != nil {
  3231  			t.Error(err)
  3232  		}
  3233  		if err := client.MutateWithGitHubAppsSupport(context.Background(), struct{}{}, githubv4.Input(struct{}{}), nil, ""); err != nil {
  3234  			t.Error(err)
  3235  		}
  3236  	})
  3237  
  3238  	t.Run("The ForPlugin call doesn't manipulate the original client", func(t *testing.T) {
  3239  		expectedUserAgent = "unset/0"
  3240  		if err := client.QueryWithGitHubAppsSupport(context.Background(), struct{}{}, nil, ""); err != nil {
  3241  			t.Error(err)
  3242  		}
  3243  		if err := client.MutateWithGitHubAppsSupport(context.Background(), struct{}{}, githubv4.Input(struct{}{}), nil, ""); err != nil {
  3244  			t.Error(err)
  3245  		}
  3246  	})
  3247  
  3248  	t.Run("ForSubcomponent changes the user agent accordingly", func(t *testing.T) {
  3249  		client := client.ForSubcomponent("test-plugin")
  3250  		expectedUserAgent = "unset.test-plugin/0"
  3251  		if err := client.QueryWithGitHubAppsSupport(context.Background(), struct{}{}, nil, ""); err != nil {
  3252  			t.Error(err)
  3253  		}
  3254  		if err := client.MutateWithGitHubAppsSupport(context.Background(), struct{}{}, githubv4.Input(struct{}{}), nil, ""); err != nil {
  3255  			t.Error(err)
  3256  		}
  3257  	})
  3258  
  3259  	t.Run("The ForSubcomponent call doesn't manipulate the original client", func(t *testing.T) {
  3260  		expectedUserAgent = "unset/0"
  3261  		if err := client.QueryWithGitHubAppsSupport(context.Background(), struct{}{}, nil, ""); err != nil {
  3262  			t.Error(err)
  3263  		}
  3264  		if err := client.MutateWithGitHubAppsSupport(context.Background(), struct{}{}, githubv4.Input(struct{}{}), nil, ""); err != nil {
  3265  			t.Error(err)
  3266  		}
  3267  	})
  3268  }
  3269  
  3270  func TestGetDirectory(t *testing.T) {
  3271  	expectedContents := []DirectoryContent{
  3272  		{
  3273  			Type: "file",
  3274  			Name: "bar",
  3275  			Path: "foo/bar",
  3276  		},
  3277  		{
  3278  			Type: "dir",
  3279  			Name: "hello",
  3280  			Path: "foo/hello",
  3281  		},
  3282  	}
  3283  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3284  		if r.Method != http.MethodGet {
  3285  			t.Errorf("Bad method: %s", r.Method)
  3286  		}
  3287  		if r.URL.Path != "/repos/k8s/kuber/contents/foo" {
  3288  			t.Errorf("Bad request path: %s", r.URL.Path)
  3289  		}
  3290  		if r.URL.RawQuery != "" {
  3291  			t.Errorf("Bad request query: %s", r.URL.RawQuery)
  3292  		}
  3293  		b, err := json.Marshal(&expectedContents)
  3294  		if err != nil {
  3295  			t.Fatalf("Didn't expect error: %v", err)
  3296  		}
  3297  		fmt.Fprint(w, string(b))
  3298  	}))
  3299  	defer ts.Close()
  3300  	c := getClient(ts.URL)
  3301  	if contents, err := c.GetDirectory("k8s", "kuber", "foo", ""); err != nil {
  3302  		t.Errorf("Didn't expect error: %v", err)
  3303  	} else if len(contents) != 2 {
  3304  		t.Errorf("Expected two contents, found %d: %v", len(contents), contents)
  3305  		return
  3306  	} else if !reflect.DeepEqual(contents, expectedContents) {
  3307  		t.Errorf("Wrong list of teams, expected: %v, got: %v", expectedContents, contents)
  3308  	}
  3309  }
  3310  
  3311  func TestGetDirectoryRef(t *testing.T) {
  3312  	expectedContents := []DirectoryContent{
  3313  		{
  3314  			Type: "file",
  3315  			Name: "bar.go",
  3316  			Path: "foo/bar.go",
  3317  		},
  3318  		{
  3319  			Type: "dir",
  3320  			Name: "hello",
  3321  			Path: "foo/hello",
  3322  		},
  3323  	}
  3324  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3325  		if r.Method != http.MethodGet {
  3326  			t.Errorf("Bad method: %s", r.Method)
  3327  		}
  3328  		if r.URL.Path != "/repos/k8s/kuber/contents/foo" {
  3329  			t.Errorf("Bad request path: %s", r.URL.Path)
  3330  		}
  3331  		if r.URL.RawQuery != "ref=12345" {
  3332  			t.Errorf("Bad request query: %s", r.URL.RawQuery)
  3333  		}
  3334  		b, err := json.Marshal(&expectedContents)
  3335  		if err != nil {
  3336  			t.Fatalf("Didn't expect error: %v", err)
  3337  		}
  3338  		fmt.Fprint(w, string(b))
  3339  	}))
  3340  	defer ts.Close()
  3341  	c := getClient(ts.URL)
  3342  	if contents, err := c.GetDirectory("k8s", "kuber", "foo", "12345"); err != nil {
  3343  		t.Errorf("Didn't expect error: %v", err)
  3344  	} else if len(contents) != 2 {
  3345  		t.Errorf("Expected two contents, found %d: %v", len(contents), contents)
  3346  		return
  3347  	} else if !reflect.DeepEqual(contents, expectedContents) {
  3348  		t.Errorf("Wrong list of teams, expected: %v, got: %v", expectedContents, contents)
  3349  	}
  3350  }
  3351  
  3352  func TestCreatePullRequestReviewComment(t *testing.T) {
  3353  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3354  		if r.Method != http.MethodPost {
  3355  			t.Errorf("Bad method: %s", r.Method)
  3356  		}
  3357  		if r.URL.Path != "/repos/k8s/kuber/pulls/5/comments" {
  3358  			t.Errorf("Bad request path: %s", r.URL.Path)
  3359  		}
  3360  		b, err := io.ReadAll(r.Body)
  3361  		if err != nil {
  3362  			t.Fatalf("Could not read request body: %v", err)
  3363  		}
  3364  		var rc ReviewComment
  3365  		if err := json.Unmarshal(b, &rc); err != nil {
  3366  			t.Errorf("Could not unmarshal request: %v", err)
  3367  		} else if rc.Body != "hello" {
  3368  			t.Errorf("Wrong body: %s", rc.Body)
  3369  		}
  3370  		http.Error(w, "201 Created", http.StatusCreated)
  3371  	}))
  3372  	defer ts.Close()
  3373  	c := getClient(ts.URL)
  3374  	if err := c.CreatePullRequestReviewComment("k8s", "kuber", 5, ReviewComment{Body: "hello"}); err != nil {
  3375  		t.Errorf("Didn't expect error: %v", err)
  3376  	}
  3377  }
  3378  
  3379  func TestThrottlerRespectsContexts(t *testing.T) {
  3380  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3381  		w.WriteHeader(200)
  3382  	}))
  3383  	defer ts.Close()
  3384  	c := getClient(ts.URL)
  3385  
  3386  	// Set the throttler, use up the one token we have for this hour
  3387  	c.Throttle(1, 1)
  3388  	if err := c.CreateReview("", "", 0, DraftReview{}); err != nil {
  3389  		t.Fatalf("failed to use up the throttlers token: %v", err)
  3390  	}
  3391  
  3392  	// Use a very low timeout so we don't have to wait
  3393  	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
  3394  	defer cancel()
  3395  
  3396  	clientMethods := getCallForAllClientMethodsThroughReflection(c,
  3397  		// Skip all method whose first arg is not of type context.Context (self is the actual first arg)
  3398  		func(m reflect.Method) bool {
  3399  			return m.Func.Type().NumIn() < 2 || m.Func.Type().In(1).String() != "context.Context"
  3400  		},
  3401  		// Insert our custom ctx for any arg of type context.Context
  3402  		func(typeName string) interface{} {
  3403  			if typeName == "context.Context" {
  3404  				return ctx
  3405  			}
  3406  			return nil
  3407  		},
  3408  	)
  3409  
  3410  	for _, clientMethod := range clientMethods {
  3411  		methodName, callMethod := clientMethod()
  3412  		t.Run(methodName, func(t *testing.T) {
  3413  			if actualErr := callMethod(); !errors.Is(actualErr, context.DeadlineExceeded) {
  3414  				t.Errorf("expected to get %v error, got %v", context.DeadlineExceeded, actualErr)
  3415  			}
  3416  		})
  3417  	}
  3418  }
  3419  
  3420  func TestCreateCheckRun(t *testing.T) {
  3421  	checkRun := CheckRun{
  3422  		Name:    "foo",
  3423  		HeadSHA: "someref",
  3424  	}
  3425  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3426  		if r.Method != http.MethodPost {
  3427  			t.Errorf("Bad method: %s", r.Method)
  3428  		}
  3429  		if r.URL.Path != "/repos/k8s/kuber/check-runs" {
  3430  			t.Errorf("Bad request path: %s", r.URL.Path)
  3431  		}
  3432  		b, err := io.ReadAll(r.Body)
  3433  		if err != nil {
  3434  			t.Fatalf("Could not read request body: %v", err)
  3435  		}
  3436  		var cr CheckRun
  3437  		if err := json.Unmarshal(b, &cr); err != nil {
  3438  			t.Errorf("Could not unmarshal request: %v", err)
  3439  		} else if !reflect.DeepEqual(checkRun, cr) {
  3440  			t.Errorf("expected checkrun differs from actual: %s", cmp.Diff(checkRun, cr))
  3441  		}
  3442  		http.Error(w, "201 Created", http.StatusCreated)
  3443  	}))
  3444  	defer ts.Close()
  3445  	c := getClient(ts.URL)
  3446  	if err := c.CreateCheckRun("k8s", "kuber", checkRun); err != nil {
  3447  		t.Errorf("Didn't expect error: %v", err)
  3448  	}
  3449  }
  3450  
  3451  func TestIsAppInstalled(t *testing.T) {
  3452  	testCases := []struct {
  3453  		name     string
  3454  		org      string
  3455  		repo     string
  3456  		expected bool
  3457  	}{
  3458  		{
  3459  			name:     "App is installed",
  3460  			org:      "k8s",
  3461  			repo:     "kuber",
  3462  			expected: true,
  3463  		},
  3464  		{
  3465  			name:     "App is not installed",
  3466  			org:      "k8s",
  3467  			repo:     "other",
  3468  			expected: false,
  3469  		},
  3470  	}
  3471  
  3472  	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3473  		if r.Method != http.MethodGet {
  3474  			t.Errorf("Bad method: %s", r.Method)
  3475  		}
  3476  		if r.URL.Path == "/repos/k8s/kuber/installation" {
  3477  			w.WriteHeader(http.StatusOK)
  3478  		} else {
  3479  			w.WriteHeader(http.StatusNotFound)
  3480  		}
  3481  	}))
  3482  	defer ts.Close()
  3483  	c := getClient(ts.URL)
  3484  	c.usesAppsAuth = true
  3485  
  3486  	for _, tc := range testCases {
  3487  		t.Run(tc.name, func(t *testing.T) {
  3488  			installed, err := c.IsAppInstalled(tc.org, tc.repo)
  3489  			if err != nil {
  3490  				t.Fatalf("unexpected error received: %v", err)
  3491  			}
  3492  			if installed != tc.expected {
  3493  				t.Fatalf("response: %v doesn't match expected: %v", installed, tc.expected)
  3494  			}
  3495  		})
  3496  	}
  3497  }