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 }