sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/override/override_test.go (about) 1 /* 2 Copyright 2018 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 override 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "reflect" 24 "strings" 25 "testing" 26 27 "github.com/google/go-cmp/cmp" 28 "github.com/sirupsen/logrus" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/util/sets" 31 32 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 33 "sigs.k8s.io/prow/pkg/config" 34 "sigs.k8s.io/prow/pkg/github" 35 "sigs.k8s.io/prow/pkg/layeredsets" 36 "sigs.k8s.io/prow/pkg/plugins" 37 "sigs.k8s.io/prow/pkg/plugins/ownersconfig" 38 "sigs.k8s.io/prow/pkg/repoowners" 39 ) 40 41 const ( 42 fakeOrg = "fake-org" 43 fakeRepo = "fake-repo" 44 fakePR = 33 45 fakeSHA = "deadbeef" 46 faseBaseRef = "fake-branch" 47 fakeBaseSHA = "fffffff" 48 adminUser = "admin-user" 49 ) 50 51 type fakeRepoownersClient struct { 52 foc *fakeOwnersClient 53 } 54 55 func (froc *fakeRepoownersClient) LoadRepoOwners(org, repo, base string) (repoowners.RepoOwner, error) { 56 return froc.foc, nil 57 } 58 59 type fakeOwnersClient struct { 60 topLevelApprovers sets.Set[string] 61 } 62 63 func (foc *fakeOwnersClient) AllApprovers() sets.Set[string] { 64 return sets.Set[string]{} 65 } 66 67 func (foc *fakeOwnersClient) AllOwners() sets.Set[string] { 68 return sets.Set[string]{} 69 } 70 71 func (foc *fakeOwnersClient) AllReviewers() sets.Set[string] { 72 return sets.Set[string]{} 73 } 74 75 func (foc *fakeOwnersClient) Filenames() ownersconfig.Filenames { 76 return ownersconfig.FakeFilenames 77 } 78 79 func (foc *fakeOwnersClient) TopLevelApprovers() sets.Set[string] { 80 return foc.topLevelApprovers 81 } 82 83 func (foc *fakeOwnersClient) Approvers(path string) layeredsets.String { 84 return layeredsets.String{} 85 } 86 87 func (foc *fakeOwnersClient) LeafApprovers(path string) sets.Set[string] { 88 return sets.Set[string]{} 89 } 90 91 func (foc *fakeOwnersClient) FindApproverOwnersForFile(path string) string { 92 return "" 93 } 94 95 func (foc *fakeOwnersClient) Reviewers(path string) layeredsets.String { 96 return layeredsets.String{} 97 } 98 99 func (foc *fakeOwnersClient) RequiredReviewers(path string) sets.Set[string] { 100 return sets.Set[string]{} 101 } 102 103 func (foc *fakeOwnersClient) LeafReviewers(path string) sets.Set[string] { 104 return sets.Set[string]{} 105 } 106 107 func (foc *fakeOwnersClient) FindReviewersOwnersForFile(path string) string { 108 return "" 109 } 110 111 func (foc *fakeOwnersClient) FindLabelsForFile(path string) sets.Set[string] { 112 return sets.Set[string]{} 113 } 114 115 func (foc *fakeOwnersClient) IsNoParentOwners(path string) bool { 116 return false 117 } 118 119 func (foc *fakeOwnersClient) IsAutoApproveUnownedSubfolders(path string) bool { 120 return false 121 } 122 123 func (foc *fakeOwnersClient) ParseSimpleConfig(path string) (repoowners.SimpleConfig, error) { 124 return repoowners.SimpleConfig{}, nil 125 } 126 127 func (foc *fakeOwnersClient) ParseFullConfig(path string) (repoowners.FullConfig, error) { 128 return repoowners.FullConfig{}, nil 129 } 130 131 type fakeClient struct { 132 comments []string 133 statuses []github.Status 134 branchProtection *github.BranchProtection 135 ps []config.Presubmit 136 jobs sets.Set[string] 137 owners ownersClient 138 checkruns *github.CheckRunList 139 usesAppsAuth bool 140 } 141 142 func (c *fakeClient) presubmits(_, _ string, _ config.RefGetter, _ string) ([]config.Presubmit, error) { 143 var result []config.Presubmit 144 result = append(result, c.ps...) 145 return result, nil 146 } 147 148 func (c *fakeClient) CreateComment(org, repo string, number int, comment string) error { 149 c.comments = append(c.comments, comment) 150 return nil 151 } 152 153 func (c *fakeClient) CreateStatus(org, repo, ref string, s github.Status) error { 154 switch { 155 case s.Context == "fail-create": 156 return errors.New("injected CreateStatus failure") 157 case org != fakeOrg: 158 return fmt.Errorf("bad org: %s", org) 159 case repo != fakeRepo: 160 return fmt.Errorf("bad repo: %s", repo) 161 case ref != fakeSHA: 162 return fmt.Errorf("bad ref: %s", ref) 163 } 164 for i, status := range c.statuses { 165 if status.State != github.StatusSuccess && status.Context == s.Context { 166 c.statuses[i] = s 167 return nil 168 } 169 } 170 //handle branch protection case 171 if len(c.statuses) == 0 { 172 c.statuses = append(c.statuses, s) 173 } 174 return nil 175 } 176 177 func (c *fakeClient) GetPullRequest(org, repo string, number int) (*github.PullRequest, error) { 178 switch { 179 case number < 0: 180 return nil, errors.New("injected GetPullRequest failure") 181 case org != fakeOrg: 182 return nil, fmt.Errorf("bad org: %s", org) 183 case repo != fakeRepo: 184 return nil, fmt.Errorf("bad repo: %s", repo) 185 case number != fakePR: 186 return nil, fmt.Errorf("bad number: %d", number) 187 } 188 var pr github.PullRequest 189 pr.Head.SHA = fakeSHA 190 pr.Base.Ref = faseBaseRef 191 return &pr, nil 192 } 193 194 func (c *fakeClient) ListStatuses(org, repo, ref string) ([]github.Status, error) { 195 switch { 196 case org != fakeOrg: 197 return nil, fmt.Errorf("bad org: %s", org) 198 case repo != fakeRepo: 199 return nil, fmt.Errorf("bad repo: %s", repo) 200 case ref != fakeSHA: 201 return nil, fmt.Errorf("bad ref: %s", ref) 202 } 203 var out []github.Status 204 for _, s := range c.statuses { 205 if s.Context == "fail-list" { 206 return nil, errors.New("injected ListStatuses failure") 207 } 208 out = append(out, s) 209 } 210 return out, nil 211 } 212 213 func (c *fakeClient) ListCheckRuns(org, repo, ref string) (*github.CheckRunList, error) { 214 if c.checkruns != nil { 215 return c.checkruns, nil 216 } 217 return &github.CheckRunList{}, nil 218 } 219 220 func (c *fakeClient) CreateCheckRun(org, repo string, checkRun github.CheckRun) error { 221 for _, checkrun := range c.checkruns.CheckRuns { 222 if checkrun.CompletedAt == "" { 223 continue 224 } else if strings.ToUpper(checkrun.Conclusion) == "NEUTRAL" { 225 continue 226 } else if strings.ToUpper(checkrun.Conclusion) == "SUCCESS" { 227 continue 228 } else if checkrun.Name == checkRun.Name { 229 prowOverrideCR := github.CheckRun{ 230 Name: checkrun.Name, 231 HeadSHA: checkrun.HeadSHA, 232 CompletedAt: checkrun.CompletedAt, 233 Status: "completed", 234 Conclusion: "success", 235 Output: github.CheckRunOutput{ 236 Title: fmt.Sprintf("Prow override - %s", checkrun.Name), 237 Summary: fmt.Sprintf("Prow has received override command for the %s checkrun.", checkrun.Name), 238 }, 239 } 240 c.checkruns.CheckRuns = append(c.checkruns.CheckRuns, prowOverrideCR) 241 } 242 } 243 return nil 244 } 245 246 func (c *fakeClient) GetBranchProtection(org, repo, branch string) (*github.BranchProtection, error) { 247 switch { 248 case org != fakeOrg: 249 return nil, fmt.Errorf("bad org: %s", org) 250 case repo != fakeRepo: 251 return nil, fmt.Errorf("bad repo: %s", repo) 252 case branch != faseBaseRef: 253 return nil, fmt.Errorf("bad branch: %s", branch) 254 } 255 256 if c.branchProtection != nil && c.branchProtection.RequiredStatusChecks != nil && 257 len(c.branchProtection.RequiredStatusChecks.Contexts) > 0 && 258 c.branchProtection.RequiredStatusChecks.Contexts[0] == "fail-protection" { 259 return nil, errors.New("injected GetBranchProtection failure") 260 } 261 262 return c.branchProtection, nil 263 } 264 265 func (c *fakeClient) HasPermission(org, repo, user string, roles ...string) (bool, error) { 266 switch { 267 case org != fakeOrg: 268 return false, fmt.Errorf("bad org: %s", org) 269 case repo != fakeRepo: 270 return false, fmt.Errorf("bad repo: %s", repo) 271 case roles[0] != github.RoleAdmin: 272 return false, fmt.Errorf("bad roles: %s", roles) 273 case user == "fail": 274 return true, errors.New("injected HasPermission error") 275 } 276 return user == adminUser, nil 277 } 278 279 func (c *fakeClient) GetRef(org, repo, ref string) (string, error) { 280 if repo == "fail-ref" { 281 return "", errors.New("injected GetRef error") 282 } 283 return fakeBaseSHA, nil 284 } 285 286 func (c *fakeClient) ListTeams(org string) ([]github.Team, error) { 287 if org == fakeOrg { 288 return []github.Team{ 289 { 290 ID: 1, 291 Name: "team foo", 292 Slug: "team-foo", 293 }, 294 }, nil 295 } 296 return []github.Team{}, nil 297 } 298 299 func (c *fakeClient) ListTeamMembersBySlug(org, teamSlug, role string) ([]github.TeamMember, error) { 300 if teamSlug == "team-foo" { 301 return []github.TeamMember{ 302 {Login: "user1"}, 303 {Login: "user2"}, 304 }, nil 305 } 306 return []github.TeamMember{}, nil 307 } 308 309 func (c *fakeClient) Create(_ context.Context, pj *prowapi.ProwJob, _ metav1.CreateOptions) (*prowapi.ProwJob, error) { 310 if s := pj.Status.State; s != prowapi.SuccessState { 311 return pj, fmt.Errorf("bad status state: %s", s) 312 } 313 if pj.Spec.Context == "fail-create" { 314 return pj, errors.New("injected CreateProwJob error") 315 } 316 c.jobs.Insert(pj.Spec.Context) 317 return pj, nil 318 } 319 320 func (c *fakeClient) LoadRepoOwners(org, repo, base string) (repoowners.RepoOwner, error) { 321 return c.owners.LoadRepoOwners(org, repo, base) 322 } 323 324 func (c *fakeClient) UsesAppAuth() bool { 325 return c.usesAppsAuth 326 } 327 328 func TestAuthorizedUser(t *testing.T) { 329 cases := []struct { 330 name string 331 user string 332 expected bool 333 }{ 334 { 335 name: "fail closed", 336 user: "fail", 337 }, 338 { 339 name: "reject rando", 340 user: "random", 341 }, 342 { 343 name: "accept admin", 344 user: adminUser, 345 expected: true, 346 }, 347 } 348 349 log := logrus.WithField("plugin", pluginName) 350 for _, tc := range cases { 351 t.Run(tc.name, func(t *testing.T) { 352 if actual := authorizedUser(&fakeClient{}, log, fakeOrg, fakeRepo, tc.user); actual != tc.expected { 353 t.Errorf("actual %t != expected %t", actual, tc.expected) 354 } 355 }) 356 } 357 } 358 359 func TestHandle(t *testing.T) { 360 cases := []struct { 361 name string 362 action github.GenericCommentEventAction 363 issue bool 364 state string 365 comment string 366 contexts []github.Status 367 branchProtection *github.BranchProtection 368 presubmits []config.Presubmit 369 user string 370 number int 371 expected []github.Status 372 expectedCheckRuns *github.CheckRunList 373 jobs sets.Set[string] 374 checkComments []string 375 options plugins.Override 376 approvers []string 377 err bool 378 checkruns *github.CheckRunList 379 usesAppsAuth bool 380 }{ 381 { 382 name: "successfully override failure", 383 comment: "/override broken-test", 384 contexts: []github.Status{ 385 { 386 Context: "broken-test", 387 State: github.StatusFailure, 388 }, 389 }, 390 expected: []github.Status{ 391 { 392 Context: "broken-test", 393 Description: description(adminUser), 394 State: github.StatusSuccess, 395 }, 396 }, 397 checkComments: []string{"on behalf of " + adminUser}, 398 }, 399 { 400 name: "successfully override unknown context derived from checkruns", 401 comment: "/override failure-checkrun", 402 checkruns: &github.CheckRunList{ 403 CheckRuns: []github.CheckRun{ 404 {Name: "incomplete-checkrun"}, 405 {Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"}, 406 }, 407 }, 408 expected: []github.Status{}, 409 expectedCheckRuns: &github.CheckRunList{ 410 CheckRuns: []github.CheckRun{ 411 {Name: "incomplete-checkrun"}, 412 {Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"}, 413 {Name: "failure-checkrun", CompletedAt: "1800 BC", Status: "completed", Conclusion: "success", Output: github.CheckRunOutput{ 414 Title: fmt.Sprintf("Prow override - %s", "failure-checkrun"), 415 Summary: fmt.Sprintf("Prow has received override command for the %s checkrun.", "failure-checkrun"), 416 }}, 417 }, 418 }, 419 usesAppsAuth: true, 420 }, 421 { 422 name: "successfully override unknown context with special characters derived from checkruns", 423 comment: `/override "test / Unit Tests"`, 424 checkruns: &github.CheckRunList{ 425 CheckRuns: []github.CheckRun{ 426 {Name: "incomplete-checkrun"}, 427 {Name: "test / Unit Tests", CompletedAt: "1800 BC", Conclusion: "failure"}, 428 }, 429 }, 430 expected: []github.Status{}, 431 expectedCheckRuns: &github.CheckRunList{ 432 CheckRuns: []github.CheckRun{ 433 {Name: "incomplete-checkrun"}, 434 {Name: "test / Unit Tests", CompletedAt: "1800 BC", Conclusion: "failure"}, 435 {Name: "test / Unit Tests", CompletedAt: "1800 BC", Status: "completed", Conclusion: "success", Output: github.CheckRunOutput{ 436 Title: fmt.Sprintf("Prow override - %s", "test / Unit Tests"), 437 Summary: fmt.Sprintf("Prow has received override command for the %s checkrun.", "test / Unit Tests"), 438 }}, 439 }, 440 }, 441 usesAppsAuth: true, 442 }, 443 { 444 name: "successfully override a mix of checkruns and prowjobs", 445 comment: `/override broken-test "test / Unit Tests" hung-test`, 446 checkruns: &github.CheckRunList{ 447 CheckRuns: []github.CheckRun{ 448 {Name: "incomplete-checkrun"}, 449 {Name: "test / Unit Tests", CompletedAt: "1800 BC", Conclusion: "failure"}, 450 }, 451 }, 452 contexts: []github.Status{ 453 { 454 Context: "broken-test", 455 State: github.StatusFailure, 456 }, 457 { 458 Context: "hung-test", 459 State: github.StatusPending, 460 }, 461 }, 462 expected: []github.Status{ 463 { 464 Context: "broken-test", 465 Description: description(adminUser), 466 State: github.StatusSuccess, 467 }, 468 { 469 Context: "hung-test", 470 Description: description(adminUser), 471 State: github.StatusSuccess, 472 }, 473 }, 474 expectedCheckRuns: &github.CheckRunList{ 475 CheckRuns: []github.CheckRun{ 476 {Name: "incomplete-checkrun"}, 477 {Name: "test / Unit Tests", CompletedAt: "1800 BC", Conclusion: "failure"}, 478 {Name: "test / Unit Tests", CompletedAt: "1800 BC", Status: "completed", Conclusion: "success", Output: github.CheckRunOutput{ 479 Title: fmt.Sprintf("Prow override - %s", "test / Unit Tests"), 480 Summary: fmt.Sprintf("Prow has received override command for the %s checkrun.", "test / Unit Tests"), 481 }}, 482 }, 483 }, 484 usesAppsAuth: true, 485 }, 486 { 487 name: "override a successful unknown context derived from checkruns", 488 comment: "/override success-checkrun", 489 checkruns: &github.CheckRunList{ 490 CheckRuns: []github.CheckRun{ 491 {Name: "incomplete-checkrun"}, 492 {Name: "success-checkrun", CompletedAt: "1800 BC", Conclusion: "success"}, 493 }, 494 }, 495 expected: []github.Status{}, 496 expectedCheckRuns: &github.CheckRunList{ 497 CheckRuns: []github.CheckRun{ 498 {Name: "incomplete-checkrun"}, 499 {Name: "success-checkrun", CompletedAt: "1800 BC", Conclusion: "success"}, 500 }, 501 }, 502 usesAppsAuth: true, 503 checkComments: []string{ 504 "The following unknown contexts/checkruns were given:", "`success-checkrun`", 505 }, 506 }, 507 { 508 name: "override failure-checkrun checkrun, usesAppsAuth is false", 509 comment: "/override failure-checkrun", 510 checkruns: &github.CheckRunList{ 511 CheckRuns: []github.CheckRun{ 512 {Name: "incomplete-checkrun"}, 513 {Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"}, 514 }, 515 }, 516 expected: []github.Status{}, 517 expectedCheckRuns: &github.CheckRunList{ 518 CheckRuns: []github.CheckRun{ 519 {Name: "incomplete-checkrun"}, 520 {Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"}, 521 }, 522 }, 523 usesAppsAuth: false, 524 }, 525 { 526 name: "override nonexistant checkrun", 527 comment: "/override foobar", 528 checkruns: &github.CheckRunList{ 529 CheckRuns: []github.CheckRun{ 530 {Name: "incomplete-checkrun"}, 531 {Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"}, 532 }, 533 }, 534 expected: []github.Status{}, 535 expectedCheckRuns: &github.CheckRunList{ 536 CheckRuns: []github.CheckRun{ 537 {Name: "incomplete-checkrun"}, 538 {Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"}, 539 }, 540 }, 541 usesAppsAuth: true, 542 }, 543 544 { 545 name: "successfully override pending", 546 comment: "/override hung-test", 547 contexts: []github.Status{ 548 { 549 Context: "hung-test", 550 State: github.StatusPending, 551 }, 552 }, 553 expected: []github.Status{ 554 { 555 Context: "hung-test", 556 Description: description(adminUser), 557 State: github.StatusSuccess, 558 }, 559 }, 560 usesAppsAuth: true, 561 }, 562 { 563 name: "comment for incorrect context", 564 comment: "/override whatever-you-want", 565 contexts: []github.Status{ 566 { 567 Context: "hung-test", 568 State: github.StatusPending, 569 }, 570 }, 571 presubmits: []config.Presubmit{ 572 { 573 JobBase: config.JobBase{ 574 Name: "hung-prow-job", 575 }, 576 Reporter: config.Reporter{ 577 Context: "hung-test", 578 }, 579 }, 580 }, 581 expected: []github.Status{ 582 { 583 Context: "hung-test", 584 State: github.StatusPending, 585 }, 586 }, 587 checkComments: []string{ 588 "The following unknown contexts/checkruns were given", "whatever-you-want", 589 "Only the following failed contexts/checkruns were expected", "hung-test", "hung-prow-job", 590 }, 591 }, 592 { 593 name: "refuse override from non-admin", 594 comment: "/override broken-test", 595 contexts: []github.Status{ 596 { 597 Context: "broken-test", 598 State: github.StatusPending, 599 }, 600 }, 601 user: "rando", 602 checkComments: []string{"unauthorized"}, 603 expected: []github.Status{ 604 { 605 Context: "broken-test", 606 State: github.StatusPending, 607 }, 608 }, 609 }, 610 { 611 name: "comment for override with no target", 612 comment: "/override", 613 contexts: []github.Status{ 614 { 615 Context: "broken-test", 616 State: github.StatusPending, 617 }, 618 }, 619 user: "rando", 620 checkComments: []string{"but none was given"}, 621 expected: []github.Status{ 622 { 623 Context: "broken-test", 624 State: github.StatusPending, 625 }, 626 }, 627 }, 628 { 629 name: "override multiple", 630 comment: "/override broken-test\n/override hung-test", 631 contexts: []github.Status{ 632 { 633 Context: "broken-test", 634 State: github.StatusFailure, 635 }, 636 { 637 Context: "hung-test", 638 State: github.StatusPending, 639 }, 640 }, 641 expected: []github.Status{ 642 { 643 Context: "broken-test", 644 Description: description(adminUser), 645 State: github.StatusSuccess, 646 }, 647 { 648 Context: "hung-test", 649 Description: description(adminUser), 650 State: github.StatusSuccess, 651 }, 652 }, 653 checkComments: []string{fmt.Sprintf("%s: broken-test, hung-test", adminUser)}, 654 }, 655 { 656 name: "override multiple contexts inline", 657 comment: "/override broken-test hung-test", 658 contexts: []github.Status{ 659 { 660 Context: "broken-test", 661 State: github.StatusFailure, 662 }, 663 { 664 Context: "hung-test", 665 State: github.StatusPending, 666 }, 667 }, 668 expected: []github.Status{ 669 { 670 Context: "broken-test", 671 Description: description(adminUser), 672 State: github.StatusSuccess, 673 }, 674 { 675 Context: "hung-test", 676 Description: description(adminUser), 677 State: github.StatusSuccess, 678 }, 679 }, 680 checkComments: []string{fmt.Sprintf("%s: broken-test, hung-test", adminUser)}, 681 }, 682 { 683 name: "override with extra whitespace", 684 // Note two spaces here to start, and trailing whitespace 685 comment: "/override broken-test \r\n", // github ends lines with \r\n 686 contexts: []github.Status{ 687 { 688 Context: "broken-test", 689 State: github.StatusFailure, 690 }, 691 }, 692 expected: []github.Status{ 693 { 694 Context: "broken-test", 695 Description: description(adminUser), 696 State: github.StatusSuccess, 697 }, 698 }, 699 checkComments: []string{fmt.Sprintf("%s: broken-test", adminUser)}, 700 }, 701 { 702 name: "ignore non-PRs", 703 issue: true, 704 comment: "/override broken-test", 705 contexts: []github.Status{ 706 { 707 Context: "broken-test", 708 State: github.StatusPending, 709 }, 710 }, 711 expected: []github.Status{ 712 { 713 Context: "broken-test", 714 State: github.StatusPending, 715 }, 716 }, 717 }, 718 { 719 name: "ignore closed issues", 720 state: "closed", 721 comment: "/override broken-test", 722 contexts: []github.Status{ 723 { 724 Context: "broken-test", 725 State: github.StatusPending, 726 }, 727 }, 728 expected: []github.Status{ 729 { 730 Context: "broken-test", 731 State: github.StatusPending, 732 }, 733 }, 734 }, 735 { 736 name: "ignore edits", 737 action: github.GenericCommentActionEdited, 738 comment: "/override broken-test", 739 contexts: []github.Status{ 740 { 741 Context: "broken-test", 742 State: github.StatusPending, 743 }, 744 }, 745 expected: []github.Status{ 746 { 747 Context: "broken-test", 748 State: github.StatusPending, 749 }, 750 }, 751 }, 752 { 753 name: "ignore random text", 754 comment: "/test broken-test", 755 contexts: []github.Status{ 756 { 757 Context: "broken-test", 758 State: github.StatusPending, 759 }, 760 }, 761 expected: []github.Status{ 762 { 763 Context: "broken-test", 764 State: github.StatusPending, 765 }, 766 }, 767 }, 768 { 769 name: "comment on get pr failure", 770 number: fakePR * 2, 771 comment: "/override broken-test", 772 contexts: []github.Status{ 773 { 774 Context: "broken-test", 775 State: github.StatusFailure, 776 }, 777 }, 778 expected: []github.Status{ 779 { 780 Context: "broken-test", 781 State: github.StatusFailure, 782 }, 783 }, 784 checkComments: []string{"Cannot get PR"}, 785 }, 786 { 787 name: "comment on list statuses failure", 788 comment: "/override fail-list", 789 contexts: []github.Status{ 790 { 791 Context: "fail-list", 792 State: github.StatusFailure, 793 }, 794 }, 795 expected: []github.Status{ 796 { 797 Context: "fail-list", 798 State: github.StatusFailure, 799 }, 800 }, 801 checkComments: []string{"Cannot get commit statuses"}, 802 }, 803 { 804 name: "comment on get branch protection failure", 805 comment: "/override fail-list", 806 branchProtection: &github.BranchProtection{RequiredStatusChecks: &github.RequiredStatusChecks{ 807 Contexts: []string{"fail-protection"}, 808 }}, 809 contexts: []github.Status{ 810 { 811 Context: "broken-test", 812 State: github.StatusFailure, 813 }, 814 }, 815 expected: []github.Status{ 816 { 817 Context: "broken-test", 818 State: github.StatusFailure, 819 }, 820 }, 821 checkComments: []string{"Cannot get branch protection"}, 822 }, 823 { 824 name: "do not override passing contexts", 825 comment: "/override passing-test", 826 contexts: []github.Status{ 827 { 828 Context: "passing-test", 829 Description: "preserve description", 830 State: github.StatusSuccess, 831 }, 832 }, 833 expected: []github.Status{ 834 { 835 Context: "passing-test", 836 State: github.StatusSuccess, 837 Description: "preserve description", 838 }, 839 }, 840 }, 841 { 842 name: "create successful prow job", 843 comment: "/override prow-job", 844 contexts: []github.Status{ 845 { 846 Context: "prow-job", 847 Description: "failed", 848 State: github.StatusFailure, 849 }, 850 }, 851 presubmits: []config.Presubmit{ 852 { 853 JobBase: config.JobBase{ 854 Name: "prow-job", 855 }, 856 Reporter: config.Reporter{ 857 Context: "prow-job", 858 }, 859 }, 860 }, 861 jobs: sets.New[string]("prow-job"), 862 expected: []github.Status{ 863 { 864 Context: "prow-job", 865 State: github.StatusSuccess, 866 Description: description(adminUser), 867 }, 868 }, 869 }, 870 { 871 name: "successfully override prow job name", 872 comment: "/override prow-job", 873 contexts: []github.Status{ 874 { 875 Context: "ci/prow/pkg-job", 876 Description: "failed", 877 State: github.StatusFailure, 878 }, 879 }, 880 presubmits: []config.Presubmit{ 881 { 882 JobBase: config.JobBase{ 883 Name: "prow-job", 884 }, 885 Reporter: config.Reporter{ 886 Context: "ci/prow/pkg-job", 887 }, 888 }, 889 }, 890 jobs: sets.New[string]("ci/prow/pkg-job"), 891 expected: []github.Status{ 892 { 893 Context: "ci/prow/pkg-job", 894 State: github.StatusSuccess, 895 Description: description(adminUser), 896 }, 897 }, 898 }, 899 { 900 name: "override prow job and context", 901 comment: "/override prow-job\n/override ci/prow/context", 902 contexts: []github.Status{ 903 { 904 Context: "ci/prow/context", 905 Description: "failed", 906 State: github.StatusFailure, 907 }, 908 { 909 Context: "ci/prow/pkg-job", 910 Description: "failed", 911 State: github.StatusFailure, 912 }, 913 }, 914 presubmits: []config.Presubmit{ 915 { 916 JobBase: config.JobBase{ 917 Name: "prow-job", 918 }, 919 Reporter: config.Reporter{ 920 Context: "ci/prow/pkg-job", 921 }, 922 }, 923 }, 924 jobs: sets.New[string]("ci/prow/pkg-job"), 925 expected: []github.Status{ 926 { 927 Context: "ci/prow/context", 928 State: github.StatusSuccess, 929 Description: description(adminUser), 930 }, 931 { 932 Context: "ci/prow/pkg-job", 933 State: github.StatusSuccess, 934 Description: description(adminUser), 935 }, 936 }, 937 }, 938 { 939 name: "override same context and prow job", 940 comment: "/override ci/prow/pkg-job\n/override prow-job", 941 contexts: []github.Status{ 942 { 943 Context: "ci/prow/pkg-job", 944 Description: "failed", 945 State: github.StatusFailure, 946 }, 947 }, 948 presubmits: []config.Presubmit{ 949 { 950 JobBase: config.JobBase{ 951 Name: "prow-job", 952 }, 953 Reporter: config.Reporter{ 954 Context: "ci/prow/pkg-job", 955 }, 956 }, 957 }, 958 jobs: sets.New[string]("ci/prow/pkg-job"), 959 expected: []github.Status{ 960 { 961 Context: "ci/prow/pkg-job", 962 State: github.StatusSuccess, 963 Description: description(adminUser), 964 }, 965 }, 966 }, 967 { 968 name: "override with explanation works", 969 comment: "/override job\r\nobnoxious flake", // github ends lines with \r\n 970 contexts: []github.Status{ 971 { 972 Context: "job", 973 Description: "failed", 974 State: github.StatusFailure, 975 }, 976 }, 977 expected: []github.Status{ 978 { 979 Context: "job", 980 Description: description(adminUser), 981 State: github.StatusSuccess, 982 }, 983 }, 984 }, 985 { 986 name: "override with allow_top_level_owners works", 987 comment: "/override job", 988 user: "code_owner", 989 options: plugins.Override{AllowTopLevelOwners: true}, 990 approvers: []string{"code_owner"}, 991 contexts: []github.Status{ 992 { 993 Context: "job", 994 Description: "failed", 995 State: github.StatusFailure, 996 }, 997 }, 998 expected: []github.Status{ 999 { 1000 Context: "job", 1001 Description: description("code_owner"), 1002 State: github.StatusSuccess, 1003 }, 1004 }, 1005 }, 1006 { 1007 name: "override with allow_top_level_owners works for uppercase user", 1008 comment: "/override job", 1009 user: "Code_owner", 1010 options: plugins.Override{AllowTopLevelOwners: true}, 1011 approvers: []string{"code_owner"}, 1012 contexts: []github.Status{ 1013 { 1014 Context: "job", 1015 Description: "failed", 1016 State: github.StatusFailure, 1017 }, 1018 }, 1019 expected: []github.Status{ 1020 { 1021 Context: "job", 1022 Description: description("Code_owner"), 1023 State: github.StatusSuccess, 1024 }, 1025 }, 1026 }, 1027 { 1028 name: "override with allow_top_level_owners fails if user is not in OWNERS file", 1029 comment: "/override job", 1030 user: "non_code_owner", 1031 options: plugins.Override{AllowTopLevelOwners: true}, 1032 contexts: []github.Status{ 1033 { 1034 Context: "job", 1035 Description: "failed", 1036 State: github.StatusFailure, 1037 }, 1038 }, 1039 expected: []github.Status{ 1040 { 1041 Context: "job", 1042 Description: "failed", 1043 State: github.StatusFailure, 1044 }, 1045 }, 1046 }, 1047 { 1048 name: "override with allowed_github_team allowed if user is in specified github team", 1049 comment: "/override job", 1050 user: "user1", 1051 options: plugins.Override{ 1052 AllowedGitHubTeams: map[string][]string{ 1053 fmt.Sprintf("%s/%s", fakeOrg, fakeRepo): {"team-foo"}, 1054 }, 1055 }, 1056 contexts: []github.Status{ 1057 { 1058 Context: "job", 1059 Description: "failed", 1060 State: github.StatusFailure, 1061 }, 1062 }, 1063 expected: []github.Status{ 1064 { 1065 Context: "job", 1066 Description: description("user1"), 1067 State: github.StatusSuccess, 1068 }, 1069 }, 1070 }, 1071 { 1072 name: "override does not fail due to invalid github team slug", 1073 comment: "/override job", 1074 user: "user1", 1075 options: plugins.Override{ 1076 AllowedGitHubTeams: map[string][]string{ 1077 fmt.Sprintf("%s/%s", fakeOrg, fakeRepo): {"team-foo", "invalid-team-slug"}, 1078 }, 1079 }, 1080 contexts: []github.Status{ 1081 { 1082 Context: "job", 1083 Description: "failed", 1084 State: github.StatusFailure, 1085 }, 1086 }, 1087 expected: []github.Status{ 1088 { 1089 Context: "job", 1090 Description: description("user1"), 1091 State: github.StatusSuccess, 1092 }, 1093 }, 1094 }, 1095 { 1096 name: "override with empty branch protection", 1097 comment: "/override job", 1098 branchProtection: &github.BranchProtection{}, 1099 expected: []github.Status{}, 1100 checkComments: []string{}, 1101 }, 1102 { 1103 name: "override with branch protection empty status checks", 1104 comment: "/override job", 1105 branchProtection: &github.BranchProtection{RequiredStatusChecks: &github.RequiredStatusChecks{}}, 1106 expected: []github.Status{}, 1107 checkComments: []string{}, 1108 }, 1109 { 1110 name: "override with branch protection status checks", 1111 comment: "/override job", 1112 branchProtection: &github.BranchProtection{RequiredStatusChecks: &github.RequiredStatusChecks{ 1113 Contexts: []string{"job"}, 1114 }}, 1115 expected: []github.Status{ 1116 { 1117 Context: "job", 1118 Description: description(adminUser), 1119 State: github.StatusSuccess, 1120 }, 1121 }, 1122 checkComments: []string{"on behalf of " + adminUser}, 1123 }, 1124 { 1125 name: "override with same branch protection status check and status", 1126 comment: "/override job", 1127 branchProtection: &github.BranchProtection{RequiredStatusChecks: &github.RequiredStatusChecks{ 1128 Contexts: []string{"job"}, 1129 }}, 1130 contexts: []github.Status{ 1131 { 1132 Context: "job", 1133 State: github.StatusFailure, 1134 }, 1135 }, 1136 expected: []github.Status{ 1137 { 1138 Context: "job", 1139 Description: description(adminUser), 1140 State: github.StatusSuccess, 1141 }, 1142 }, 1143 checkComments: []string{"on behalf of " + adminUser}, 1144 }, 1145 { 1146 name: "handle only one status when multiple statuses have the same context", 1147 comment: "/override problematic-test", 1148 contexts: []github.Status{ 1149 { 1150 Context: "problematic-test", 1151 State: github.StatusPending, 1152 }, 1153 { 1154 Context: "problematic-test", 1155 State: github.StatusFailure, 1156 }, 1157 { 1158 Context: "problematic-test", 1159 State: github.StatusPending, 1160 }, 1161 }, 1162 presubmits: []config.Presubmit{ 1163 { 1164 JobBase: config.JobBase{ 1165 Name: "problematic-test", 1166 }, 1167 Reporter: config.Reporter{ 1168 Context: "problematic-test", 1169 }, 1170 }, 1171 }, 1172 jobs: sets.New[string]("problematic-test"), 1173 expected: []github.Status{ 1174 { 1175 Context: "problematic-test", 1176 Description: description(adminUser), 1177 State: github.StatusSuccess, 1178 }, 1179 { 1180 Context: "problematic-test", 1181 State: github.StatusFailure, 1182 }, 1183 { 1184 Context: "problematic-test", 1185 State: github.StatusPending, 1186 }, 1187 }, 1188 }, 1189 } 1190 1191 log := logrus.WithField("plugin", pluginName) 1192 log.Logger.SetLevel(logrus.DebugLevel) 1193 for _, tc := range cases { 1194 t.Run(tc.name, func(t *testing.T) { 1195 if tc.number == 0 { 1196 tc.number = fakePR 1197 } 1198 if tc.user == "" { 1199 tc.user = adminUser 1200 } 1201 if tc.state == "" { 1202 tc.state = "open" 1203 } 1204 if tc.action == "" { 1205 tc.action = github.GenericCommentActionCreated 1206 } 1207 if tc.contexts == nil { 1208 tc.contexts = []github.Status{} 1209 } 1210 1211 event := github.GenericCommentEvent{ 1212 Repo: github.Repo{ 1213 Owner: github.User{ 1214 Login: fakeOrg, 1215 }, 1216 Name: fakeRepo, 1217 }, 1218 User: github.User{ 1219 Login: tc.user, 1220 }, 1221 Body: tc.comment, 1222 Number: tc.number, 1223 IsPR: !tc.issue, 1224 IssueState: tc.state, 1225 Action: tc.action, 1226 } 1227 1228 froc := &fakeRepoownersClient{ 1229 foc: &fakeOwnersClient{ 1230 topLevelApprovers: sets.New[string](tc.approvers...), 1231 }, 1232 } 1233 fc := fakeClient{ 1234 statuses: tc.contexts, 1235 branchProtection: tc.branchProtection, 1236 ps: tc.presubmits, 1237 jobs: sets.Set[string]{}, 1238 owners: froc, 1239 checkruns: tc.checkruns, 1240 usesAppsAuth: tc.usesAppsAuth, 1241 } 1242 1243 if tc.jobs == nil { 1244 tc.jobs = sets.Set[string]{} 1245 } 1246 1247 err := handle(&fc, log, &event, tc.options) 1248 switch { 1249 case err != nil: 1250 if !tc.err { 1251 t.Errorf("unexpected error: %v", err) 1252 } 1253 case tc.err: 1254 t.Error("failed to receive an error") 1255 case !reflect.DeepEqual(fc.statuses, tc.expected): 1256 t.Errorf("bad statuses: actual %#v != expected %#v", fc.statuses, tc.expected) 1257 case !reflect.DeepEqual(fc.jobs, tc.jobs): 1258 t.Errorf("bad jobs: actual %#v != expected %#v", fc.jobs, tc.jobs) 1259 case !reflect.DeepEqual(fc.checkruns, tc.expectedCheckRuns): 1260 t.Errorf("expected checkruns differs from actual: %s", cmp.Diff(fc.checkruns, tc.expectedCheckRuns)) 1261 1262 } 1263 for _, expectedComment := range tc.checkComments { 1264 if !strings.Contains(strings.Join(fc.comments, "\n"), expectedComment) { 1265 t.Errorf("bad comments: expected %#v to be in %#v", expectedComment, fc.comments) 1266 } 1267 } 1268 }) 1269 } 1270 } 1271 1272 func TestHelpProvider(t *testing.T) { 1273 cases := []struct { 1274 name string 1275 config plugins.Configuration 1276 org string 1277 repo string 1278 expectedWho string 1279 }{ 1280 { 1281 name: "WhoCanUse restricted to Repo administrators if no other options specified", 1282 config: plugins.Configuration{}, 1283 expectedWho: "Repo administrators.", 1284 }, 1285 { 1286 name: "WhoCanUse includes top level code OWNERS if allow_top_level_owners is set", 1287 config: plugins.Configuration{ 1288 Override: plugins.Override{ 1289 AllowTopLevelOwners: true, 1290 }, 1291 }, 1292 expectedWho: "Repo administrators, approvers in top level OWNERS file.", 1293 }, 1294 { 1295 name: "WhoCanUse includes specified github teams", 1296 config: plugins.Configuration{ 1297 Override: plugins.Override{ 1298 AllowedGitHubTeams: map[string][]string{ 1299 "org1/repo1": {"team-foo", "team-bar"}, 1300 }, 1301 }, 1302 }, 1303 expectedWho: "Repo administrators, and the following github teams:" + 1304 "org1/repo1: team-foo team-bar.", 1305 }, 1306 } 1307 1308 for _, tc := range cases { 1309 help, err := helpProvider(&tc.config, []config.OrgRepo{}) 1310 if err != nil { 1311 t.Errorf("%s: unexpected error: %v", tc.name, err) 1312 } 1313 switch { 1314 case help == nil: 1315 t.Errorf("%s: expected a valid plugin help object, got nil", tc.name) 1316 case len(help.Commands) != 1: 1317 t.Errorf("%s: expected a single command from plugin help, got: %v", tc.name, help.Commands) 1318 case help.Commands[0].WhoCanUse != tc.expectedWho: 1319 t.Errorf("%s: expected a single command with WhoCanUse set to %s, got %s instead", tc.name, tc.expectedWho, help.Commands[0].WhoCanUse) 1320 } 1321 } 1322 } 1323 1324 func TestWhoCanUse(t *testing.T) { 1325 override := plugins.Override{ 1326 AllowedGitHubTeams: map[string][]string{ 1327 "org1/repo1": {"team-foo", "team-bar"}, 1328 "org2/repo2": {"team-bar"}, 1329 "org1": {"team-foo-bar"}, 1330 }, 1331 } 1332 expectedWho := "Repo administrators, and the following github teams:" + 1333 "org1/repo1: team-foo team-bar, org1: team-foo-bar." 1334 1335 who := whoCanUse(override, "org1", "repo1") 1336 if who != expectedWho { 1337 t.Errorf("expected %q, got %q", expectedWho, who) 1338 } 1339 } 1340 1341 func TestAuthorizedGitHubTeamMember(t *testing.T) { 1342 repoRef := fmt.Sprintf("%s/%s", fakeOrg, fakeRepo) 1343 cases := []struct { 1344 name string 1345 slugs map[string][]string 1346 org string 1347 repo string 1348 user string 1349 expected bool 1350 }{ 1351 { 1352 name: "members of specified teams are authorized", 1353 slugs: map[string][]string{ 1354 repoRef: {"team-foo"}, 1355 }, 1356 user: "user1", 1357 expected: true, 1358 }, 1359 { 1360 name: "non-members of specified teams are not authorized", 1361 slugs: map[string][]string{ 1362 repoRef: {"team-foo"}, 1363 }, 1364 user: "non-member", 1365 }, 1366 { 1367 name: "only teams corresponding to the org/repo are considered", 1368 slugs: map[string][]string{ 1369 "org/repo": {"team-foo"}, 1370 }, 1371 user: "member", 1372 }, 1373 { 1374 name: "members of specified teams are authorized to org", 1375 slugs: map[string][]string{ 1376 fakeOrg: {"team-foo"}, 1377 }, 1378 user: "user1", 1379 expected: true, 1380 }, 1381 } 1382 log := logrus.WithField("plugin", pluginName) 1383 log.Logger.SetLevel(logrus.DebugLevel) 1384 for _, tc := range cases { 1385 authorized := authorizedGitHubTeamMember(&fakeClient{}, log, tc.slugs, fakeOrg, fakeRepo, tc.user) 1386 if authorized != tc.expected { 1387 t.Errorf("%s: actual: %v != expected %v", tc.name, authorized, tc.expected) 1388 } 1389 } 1390 } 1391 1392 func TestValidateGitHubTeamSlugs(t *testing.T) { 1393 githubTeams := []github.Team{ 1394 { 1395 ID: 2, 1396 Slug: "team-bar", 1397 }, 1398 { 1399 ID: 3, 1400 Slug: "team-baz", 1401 }, 1402 } 1403 1404 repoRef := fmt.Sprintf("%s/%s", fakeOrg, fakeRepo) 1405 cases := []struct { 1406 name string 1407 teamSlugs map[string][]string 1408 err error 1409 }{ 1410 { 1411 name: "validation failure for invalid team slug", 1412 teamSlugs: map[string][]string{ 1413 repoRef: {"foo"}, 1414 }, 1415 err: fmt.Errorf("invalid team slug(s): foo"), 1416 }, 1417 { 1418 name: "no errors for valid team slugs", 1419 teamSlugs: map[string][]string{ 1420 repoRef: {"team-bar", "team-baz"}, 1421 }, 1422 }, 1423 } 1424 1425 for _, tc := range cases { 1426 err := validateGitHubTeamSlugs(tc.teamSlugs, fakeOrg, fakeRepo, githubTeams) 1427 if !reflect.DeepEqual(err, tc.err) { 1428 t.Errorf("%s: actual: %v != expected %v", tc.name, err, tc.err) 1429 } 1430 } 1431 }