github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/branchprotector/protect_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 main 18 19 import ( 20 "errors" 21 "fmt" 22 "reflect" 23 "sort" 24 "strings" 25 "testing" 26 27 "github.com/google/go-cmp/cmp" 28 "k8s.io/apimachinery/pkg/util/diff" 29 "sigs.k8s.io/yaml" 30 31 "sigs.k8s.io/prow/pkg/config" 32 "sigs.k8s.io/prow/pkg/flagutil" 33 configflagutil "sigs.k8s.io/prow/pkg/flagutil/config" 34 "sigs.k8s.io/prow/pkg/github" 35 ) 36 37 func TestOptions_Validate(t *testing.T) { 38 var testCases = []struct { 39 name string 40 opt options 41 expectedErr bool 42 }{ 43 { 44 name: "all ok", 45 opt: options{ 46 config: configflagutil.ConfigOptions{ 47 ConfigPath: "dummy", 48 }, 49 github: flagutil.GitHubOptions{TokenPath: "fake", ThrottleHourlyTokens: defaultTokens, ThrottleAllowBurst: defaultBurst}, 50 }, 51 expectedErr: false, 52 }, 53 { 54 name: "no config", 55 opt: options{ 56 github: flagutil.GitHubOptions{TokenPath: "fake", ThrottleHourlyTokens: defaultTokens, ThrottleAllowBurst: defaultBurst}, 57 }, 58 expectedErr: true, 59 }, 60 { 61 name: "no token, allow", 62 opt: options{ 63 config: configflagutil.ConfigOptions{ 64 ConfigPath: "dummy", 65 }, 66 github: flagutil.GitHubOptions{ThrottleHourlyTokens: defaultTokens, ThrottleAllowBurst: defaultBurst}, 67 }, 68 expectedErr: false, 69 }, 70 } 71 72 for _, testCase := range testCases { 73 err := testCase.opt.Validate() 74 if testCase.expectedErr && err == nil { 75 t.Errorf("%s: expected an error but got none", testCase.name) 76 } 77 if !testCase.expectedErr && err != nil { 78 t.Errorf("%s: expected no error but got one: %v", testCase.name, err) 79 } 80 } 81 } 82 83 type fakeClient struct { 84 repos map[string][]github.Repo 85 branches map[string][]github.Branch 86 deleted map[string]bool 87 updated map[string]github.BranchProtectionRequest 88 branchProtections map[string]github.BranchProtection 89 appInstallations []github.AppInstallation 90 collaborators []github.User 91 teams []github.Team 92 } 93 94 func (c fakeClient) GetRepo(org string, repo string) (github.FullRepo, error) { 95 r, ok := c.repos[org] 96 if !ok { 97 return github.FullRepo{}, fmt.Errorf("Unknown org: %s", org) 98 } 99 for _, item := range r { 100 if item.Name == repo { 101 return github.FullRepo{Repo: item}, nil 102 } 103 } 104 return github.FullRepo{}, fmt.Errorf("Unknown repo: %s", repo) 105 } 106 107 func (c fakeClient) GetRepos(org string, user bool) ([]github.Repo, error) { 108 r, ok := c.repos[org] 109 if !ok { 110 return nil, fmt.Errorf("Unknown org: %s", org) 111 } 112 return r, nil 113 } 114 115 func (c fakeClient) GetBranches(org, repo string, onlyProtected bool) ([]github.Branch, error) { 116 b, ok := c.branches[org+"/"+repo] 117 if !ok { 118 return nil, fmt.Errorf("Unknown repo: %s/%s", org, repo) 119 } 120 if onlyProtected { 121 for _, item := range b { 122 if !item.Protected { 123 continue 124 } 125 } 126 } else { 127 // when !onlyProtected, github does not set Protected 128 // match that behavior here to ensure we handle this correctly 129 for _, item := range b { 130 item.Protected = false 131 } 132 } 133 return b, nil 134 } 135 136 func (c *fakeClient) GetBranchProtection(org, repo, branch string) (*github.BranchProtection, error) { 137 ctx := org + "/" + repo + "=" + branch 138 if bp, ok := c.branchProtections[ctx]; ok { 139 return &bp, nil 140 } 141 return nil, nil 142 } 143 144 func (c *fakeClient) UpdateBranchProtection(org, repo, branch string, config github.BranchProtectionRequest) error { 145 if branch == "error" { 146 return errors.New("failed to update branch protection") 147 } 148 if c.updated == nil { 149 c.updated = map[string]github.BranchProtectionRequest{} 150 } 151 ctx := org + "/" + repo + "=" + branch 152 c.updated[ctx] = config 153 return nil 154 } 155 156 func (c *fakeClient) RemoveBranchProtection(org, repo, branch string) error { 157 if branch == "error" { 158 return errors.New("failed to remove branch protection") 159 } 160 if c.deleted == nil { 161 c.deleted = map[string]bool{} 162 } 163 ctx := org + "/" + repo + "=" + branch 164 c.deleted[ctx] = true 165 return nil 166 } 167 168 func (c *fakeClient) ListAppInstallationsForOrg(org string) ([]github.AppInstallation, error) { 169 return c.appInstallations, nil 170 } 171 172 func (c *fakeClient) ListCollaborators(org, repo string) ([]github.User, error) { 173 return c.collaborators, nil 174 } 175 176 func (c *fakeClient) ListRepoTeams(org, repo string) ([]github.Team, error) { 177 return c.teams, nil 178 } 179 180 func TestConfigureBranches(t *testing.T) { 181 yes := true 182 183 prot := github.BranchProtectionRequest{} 184 diffprot := github.BranchProtectionRequest{ 185 EnforceAdmins: &yes, 186 } 187 188 cases := []struct { 189 name string 190 updates []requirements 191 deletes map[string]bool 192 sets map[string]github.BranchProtectionRequest 193 errors int 194 }{ 195 { 196 name: "remove-protection", 197 updates: []requirements{ 198 {Org: "one", Repo: "1", Branch: "delete", Request: nil}, 199 {Org: "one", Repo: "1", Branch: "remove", Request: nil}, 200 {Org: "two", Repo: "2", Branch: "remove", Request: nil}, 201 }, 202 deletes: map[string]bool{ 203 "one/1=delete": true, 204 "one/1=remove": true, 205 "two/2=remove": true, 206 }, 207 }, 208 { 209 name: "error-remove-protection", 210 updates: []requirements{ 211 {Org: "one", Repo: "1", Branch: "error", Request: nil}, 212 }, 213 errors: 1, 214 }, 215 { 216 name: "update-protection-context", 217 updates: []requirements{ 218 { 219 Org: "one", 220 Repo: "1", 221 Branch: "master", 222 Request: &prot, 223 }, 224 { 225 Org: "one", 226 Repo: "1", 227 Branch: "other", 228 Request: &diffprot, 229 }, 230 }, 231 sets: map[string]github.BranchProtectionRequest{ 232 "one/1=master": prot, 233 "one/1=other": diffprot, 234 }, 235 }, 236 { 237 name: "complex", 238 updates: []requirements{ 239 {Org: "update", Repo: "1", Branch: "master", Request: &prot}, 240 {Org: "update", Repo: "2", Branch: "error", Request: &prot}, 241 {Org: "remove", Repo: "3", Branch: "master", Request: nil}, 242 {Org: "remove", Repo: "4", Branch: "error", Request: nil}, 243 }, 244 errors: 2, // four and five 245 deletes: map[string]bool{ 246 "remove/3=master": true, 247 }, 248 sets: map[string]github.BranchProtectionRequest{ 249 "update/1=master": prot, 250 }, 251 }, 252 } 253 254 for _, tc := range cases { 255 fc := fakeClient{} 256 p := protector{ 257 client: &fc, 258 updates: make(chan requirements), 259 done: make(chan []error), 260 } 261 go p.configureBranches() 262 for _, u := range tc.updates { 263 p.updates <- u 264 } 265 close(p.updates) 266 errs := <-p.done 267 if len(errs) != tc.errors { 268 t.Errorf("%s: %d errors != expected %d: %v", tc.name, len(errs), tc.errors, errs) 269 } 270 if !reflect.DeepEqual(fc.deleted, tc.deletes) { 271 t.Errorf("%s: deletes %v != expected %v", tc.name, fc.deleted, tc.deletes) 272 } 273 if !reflect.DeepEqual(fc.updated, tc.sets) { 274 t.Errorf("%s: updates %v != expected %v", tc.name, fc.updated, tc.sets) 275 } 276 277 } 278 } 279 280 func split(branch string) (string, string, string) { 281 parts := strings.Split(branch, "=") 282 b := parts[1] 283 parts = strings.Split(parts[0], "/") 284 return parts[0], parts[1], b 285 } 286 287 func TestProtect(t *testing.T) { 288 yes := true 289 no := false 290 291 cases := []struct { 292 name string 293 branches []string 294 startUnprotected bool 295 config string 296 archived string 297 expected []requirements 298 branchProtections map[string]github.BranchProtection 299 appInstallations []github.AppInstallation 300 collaborators []github.User 301 teams []github.Team 302 skipVerifyRestrictions bool 303 enableAppsRestrictions bool 304 errors int 305 306 enabled func(org, repo string) bool 307 }{ 308 { 309 name: "nothing", 310 }, 311 { 312 name: "unknown org", 313 config: ` 314 branch-protection: 315 protect: true 316 orgs: 317 unknown: 318 `, 319 errors: 1, 320 }, 321 { 322 name: "protect org via config default", 323 branches: []string{"cfgdef/repo1=master", "cfgdef/repo1=branch", "cfgdef/repo2=master"}, 324 config: ` 325 branch-protection: 326 protect: true 327 orgs: 328 cfgdef: 329 `, 330 expected: []requirements{ 331 { 332 Org: "cfgdef", 333 Repo: "repo1", 334 Branch: "master", 335 Request: &github.BranchProtectionRequest{ 336 EnforceAdmins: &no, 337 }, 338 }, 339 { 340 Org: "cfgdef", 341 Repo: "repo1", 342 Branch: "branch", 343 Request: &github.BranchProtectionRequest{ 344 EnforceAdmins: &no, 345 }, 346 }, 347 { 348 Org: "cfgdef", 349 Repo: "repo2", 350 Branch: "master", 351 Request: &github.BranchProtectionRequest{ 352 EnforceAdmins: &no, 353 }, 354 }, 355 }, 356 }, 357 { 358 name: "protect this but not that org", 359 branches: []string{"this/yes=master", "that/no=master"}, 360 config: ` 361 branch-protection: 362 protect: false 363 orgs: 364 this: 365 protect: true 366 that: 367 `, 368 expected: []requirements{ 369 { 370 Org: "this", 371 Repo: "yes", 372 Branch: "master", 373 Request: &github.BranchProtectionRequest{ 374 EnforceAdmins: &no, 375 }, 376 }, 377 { 378 Org: "that", 379 Repo: "no", 380 Branch: "master", 381 Request: nil, 382 }, 383 }, 384 branchProtections: map[string]github.BranchProtection{"that/no=master": {}}, 385 }, 386 { 387 name: "protect all repos when protection configured at org level", 388 branches: []string{"kubernetes/test-infra=master", "kubernetes/publishing-bot=master"}, 389 config: ` 390 branch-protection: 391 orgs: 392 kubernetes: 393 protect: true 394 repos: 395 test-infra: 396 required_status_checks: 397 contexts: 398 - hello-world 399 `, 400 expected: []requirements{ 401 { 402 Org: "kubernetes", 403 Repo: "test-infra", 404 Branch: "master", 405 Request: &github.BranchProtectionRequest{ 406 EnforceAdmins: &no, 407 RequiredStatusChecks: &github.RequiredStatusChecks{ 408 Contexts: []string{"hello-world"}, 409 }, 410 }, 411 }, 412 { 413 Org: "kubernetes", 414 Repo: "publishing-bot", 415 Branch: "master", 416 Request: &github.BranchProtectionRequest{ 417 EnforceAdmins: &no, 418 }, 419 }, 420 }, 421 }, 422 { 423 name: "require a defined branch to make a protection decision", 424 branches: []string{"org/repo=branch"}, 425 config: ` 426 branch-protection: 427 orgs: 428 org: 429 repos: 430 repo: 431 branches: 432 branch: # empty 433 `, 434 errors: 1, 435 }, 436 { 437 name: "require pushers to set protection", 438 branches: []string{"org/repo=push"}, 439 config: ` 440 branch-protection: 441 protect: false 442 restrictions: 443 teams: 444 - oncall 445 orgs: 446 org: 447 `, 448 errors: 1, 449 }, 450 { 451 name: "required contexts must set protection", 452 branches: []string{"org/repo=context"}, 453 config: ` 454 branch-protection: 455 protect: false 456 required_status_checks: 457 contexts: 458 - test-foo 459 orgs: 460 org: 461 `, 462 errors: 1, 463 }, 464 { 465 name: "protect org but skip a repo", 466 branches: []string{"org/repo1=master", "org/repo1=branch", "org/skip=master"}, 467 config: ` 468 branch-protection: 469 protect: false 470 orgs: 471 org: 472 protect: true 473 repos: 474 skip: 475 protect: false 476 `, 477 expected: []requirements{ 478 { 479 Org: "org", 480 Repo: "repo1", 481 Branch: "master", 482 Request: &github.BranchProtectionRequest{ 483 EnforceAdmins: &no, 484 }, 485 }, 486 { 487 Org: "org", 488 Repo: "repo1", 489 Branch: "branch", 490 Request: &github.BranchProtectionRequest{ 491 EnforceAdmins: &no, 492 }, 493 }, 494 { 495 Org: "org", 496 Repo: "skip", 497 Branch: "master", 498 Request: nil, 499 }, 500 }, 501 branchProtections: map[string]github.BranchProtection{"org/skip=master": {}}, 502 }, 503 { 504 name: "protect org but branchprotector is not enabled for this org, nothing happens", 505 branches: []string{"org/repo1=master", "org/repo1=branch"}, 506 config: ` 507 branch-protection: 508 protect: false 509 orgs: 510 org: 511 protect: true 512 `, 513 enabled: func(org, repo string) bool { return org != "org" }, 514 }, 515 { 516 name: "protect org, branchprotector is disabled for different org, org gets protected", 517 branches: []string{"org/repo1=master", "org/repo1=branch"}, 518 config: ` 519 branch-protection: 520 protect: false 521 orgs: 522 org: 523 protect: true 524 `, 525 expected: []requirements{ 526 { 527 Org: "org", 528 Repo: "repo1", 529 Branch: "master", 530 Request: &github.BranchProtectionRequest{ 531 EnforceAdmins: &no, 532 }, 533 }, 534 { 535 Org: "org", 536 Repo: "repo1", 537 Branch: "branch", 538 Request: &github.BranchProtectionRequest{ 539 EnforceAdmins: &no, 540 }, 541 }, 542 }, 543 enabled: func(org, repo string) bool { return org != "other-org" }, 544 }, 545 { 546 name: "protect org, branchprotector is disabled for one repo so it gets skipped", 547 branches: []string{"org/repo1=master", "org/repo2=master"}, 548 config: ` 549 branch-protection: 550 protect: false 551 orgs: 552 org: 553 protect: true 554 `, 555 expected: []requirements{{ 556 Org: "org", 557 Repo: "repo1", 558 Branch: "master", 559 Request: &github.BranchProtectionRequest{ 560 EnforceAdmins: &no, 561 }, 562 }}, 563 enabled: func(org, repo string) bool { return org == "org" && repo != "repo2" }, 564 }, 565 { 566 name: "protect org but skip a repo due to archival", 567 branches: []string{"org/repo1=master", "org/repo1=branch", "org/skip=master"}, 568 config: ` 569 branch-protection: 570 protect: false 571 orgs: 572 org: 573 protect: true 574 `, 575 archived: "skip", 576 expected: []requirements{ 577 { 578 Org: "org", 579 Repo: "repo1", 580 Branch: "master", 581 Request: &github.BranchProtectionRequest{ 582 EnforceAdmins: &no, 583 }, 584 }, 585 { 586 Org: "org", 587 Repo: "repo1", 588 Branch: "branch", 589 Request: &github.BranchProtectionRequest{ 590 EnforceAdmins: &no, 591 }, 592 }, 593 }, 594 }, 595 { 596 name: "collapse duplicated contexts", 597 branches: []string{"org/repo=master"}, 598 config: ` 599 branch-protection: 600 protect: true 601 required_status_checks: 602 contexts: 603 - hello-world 604 - duplicate-context 605 - duplicate-context 606 - hello-world 607 orgs: 608 org: 609 `, 610 expected: []requirements{ 611 { 612 Org: "org", 613 Repo: "repo", 614 Branch: "master", 615 Request: &github.BranchProtectionRequest{ 616 EnforceAdmins: &no, 617 RequiredStatusChecks: &github.RequiredStatusChecks{ 618 Contexts: []string{"duplicate-context", "hello-world"}, 619 }, 620 }, 621 }, 622 }, 623 }, 624 { 625 name: "append contexts", 626 branches: []string{"org/repo=master"}, 627 config: ` 628 branch-protection: 629 protect: true 630 required_status_checks: 631 contexts: 632 - config-presubmit 633 orgs: 634 org: 635 required_status_checks: 636 contexts: 637 - org-presubmit 638 repos: 639 repo: 640 required_status_checks: 641 contexts: 642 - repo-presubmit 643 branches: 644 master: 645 required_status_checks: 646 contexts: 647 - branch-presubmit 648 `, 649 expected: []requirements{ 650 { 651 Org: "org", 652 Repo: "repo", 653 Branch: "master", 654 Request: &github.BranchProtectionRequest{ 655 EnforceAdmins: &no, 656 RequiredStatusChecks: &github.RequiredStatusChecks{ 657 Contexts: []string{"config-presubmit", "org-presubmit", "repo-presubmit", "branch-presubmit"}, 658 }, 659 }, 660 }, 661 }, 662 }, 663 { 664 name: "append pushers", 665 branches: []string{"org/repo=master"}, 666 teams: []github.Team{ 667 { 668 Slug: "config-team", 669 Permission: github.RepoPush, 670 }, 671 { 672 Slug: "org-team", 673 Permission: github.RepoPush, 674 }, 675 { 676 Slug: "repo-team", 677 Permission: github.RepoPush, 678 }, 679 { 680 Slug: "branch-team", 681 Permission: github.RepoPush, 682 }, 683 }, 684 config: ` 685 branch-protection: 686 protect: true 687 restrictions: 688 teams: 689 - config-team 690 orgs: 691 org: 692 restrictions: 693 teams: 694 - org-team 695 repos: 696 repo: 697 restrictions: 698 teams: 699 - repo-team 700 branches: 701 master: 702 restrictions: 703 teams: 704 - branch-team 705 `, 706 expected: []requirements{ 707 { 708 Org: "org", 709 Repo: "repo", 710 Branch: "master", 711 Request: &github.BranchProtectionRequest{ 712 EnforceAdmins: &no, 713 Restrictions: &github.RestrictionsRequest{ 714 Users: &[]string{}, 715 Teams: &[]string{"config-team", "org-team", "repo-team", "branch-team"}, 716 }, 717 }, 718 }, 719 }, 720 }, 721 { 722 name: "all modern fields", 723 branches: []string{"all/modern=master"}, 724 enableAppsRestrictions: true, 725 appInstallations: []github.AppInstallation{ 726 { 727 AppSlug: "content-app", 728 Permissions: github.InstallationPermissions{ 729 Contents: string(github.Write), 730 }, 731 }, 732 }, 733 collaborators: []github.User{ 734 { 735 Login: "cindy", 736 Permissions: github.RepoPermissions{Push: true}, 737 }, 738 }, 739 teams: []github.Team{ 740 { 741 Slug: "config-team", 742 Permission: github.RepoPush, 743 }, 744 { 745 Slug: "org-team", 746 Permission: github.RepoPush, 747 }, 748 }, 749 config: ` 750 branch-protection: 751 protect: true 752 enforce_admins: true 753 required_status_checks: 754 contexts: 755 - config-presubmit 756 strict: true 757 required_pull_request_reviews: 758 required_approving_review_count: 3 759 dismiss_stale: false 760 require_code_owner_reviews: true 761 dismissal_restrictions: 762 users: 763 - bob 764 - jane 765 teams: 766 - oncall 767 - sres 768 bypass_pull_request_allowances: 769 users: 770 - bypass_bob 771 - bypass_jane 772 teams: 773 - bypass_oncall 774 - bypass_sres 775 restrictions: 776 apps: 777 - content-app 778 teams: 779 - config-team 780 users: 781 - cindy 782 orgs: 783 all: 784 required_status_checks: 785 contexts: 786 - org-presubmit 787 restrictions: 788 teams: 789 - org-team 790 `, 791 expected: []requirements{ 792 { 793 Org: "all", 794 Repo: "modern", 795 Branch: "master", 796 Request: &github.BranchProtectionRequest{ 797 EnforceAdmins: &yes, 798 RequiredStatusChecks: &github.RequiredStatusChecks{ 799 Strict: true, 800 Contexts: []string{"config-presubmit", "org-presubmit"}, 801 }, 802 RequiredPullRequestReviews: &github.RequiredPullRequestReviewsRequest{ 803 DismissStaleReviews: false, 804 RequireCodeOwnerReviews: true, 805 RequiredApprovingReviewCount: 3, 806 DismissalRestrictions: github.DismissalRestrictionsRequest{ 807 Users: &[]string{"bob", "jane"}, 808 Teams: &[]string{"oncall", "sres"}, 809 }, 810 BypassRestrictions: github.BypassRestrictionsRequest{ 811 Users: &[]string{"bypass_bob", "bypass_jane"}, 812 Teams: &[]string{"bypass_oncall", "bypass_sres"}, 813 }, 814 }, 815 Restrictions: &github.RestrictionsRequest{ 816 Apps: &[]string{"content-app"}, 817 Users: &[]string{"cindy"}, 818 Teams: &[]string{"config-team", "org-team"}, 819 }, 820 }, 821 }, 822 }, 823 }, 824 { 825 name: "child cannot disable parent policy by default", 826 branches: []string{"parent/child=unprotected"}, 827 config: ` 828 branch-protection: 829 protect: true 830 enforce_admins: true 831 orgs: 832 parent: 833 protect: false 834 `, 835 errors: 1, 836 }, 837 { 838 name: "child disables parent", 839 branches: []string{"parent/child=unprotected"}, 840 config: ` 841 branch-protection: 842 allow_disabled_policies: true 843 protect: true 844 enforce_admins: true 845 orgs: 846 parent: 847 protect: false 848 `, 849 expected: []requirements{ 850 { 851 Org: "parent", 852 Repo: "child", 853 Branch: "unprotected", 854 }, 855 }, 856 branchProtections: map[string]github.BranchProtection{"parent/child=unprotected": {}}, 857 }, 858 { 859 name: "do not unprotect unprotected", 860 branches: []string{"protect/update=master", "unprotected/skip=master"}, 861 config: ` 862 branch-protection: 863 protect: true 864 orgs: 865 protect: 866 protect: true 867 unprotected: 868 protect: false 869 `, 870 startUnprotected: true, 871 expected: []requirements{ 872 { 873 Org: "protect", 874 Repo: "update", 875 Branch: "master", 876 Request: &github.BranchProtectionRequest{ 877 EnforceAdmins: &no, 878 }, 879 }, 880 }, 881 }, 882 { 883 name: "do not make update request if the branch is already up-to-date", 884 enableAppsRestrictions: true, 885 branches: []string{"kubernetes/test-infra=master"}, 886 appInstallations: []github.AppInstallation{ 887 { 888 AppSlug: "content-app", 889 Permissions: github.InstallationPermissions{ 890 Contents: string(github.Write), 891 }, 892 }, 893 }, 894 collaborators: []github.User{ 895 { 896 Login: "cindy", 897 Permissions: github.RepoPermissions{Push: true}, 898 }, 899 }, 900 teams: []github.Team{ 901 { 902 Slug: "config-team", 903 Permission: github.RepoPush, 904 }, 905 }, 906 config: ` 907 branch-protection: 908 enforce_admins: true 909 required_status_checks: 910 contexts: 911 - config-presubmit 912 strict: true 913 required_pull_request_reviews: 914 required_approving_review_count: 3 915 dismiss_stale: false 916 require_code_owner_reviews: true 917 dismissal_restrictions: 918 users: 919 - bob 920 - jane 921 teams: 922 - oncall 923 - sres 924 bypass_pull_request_allowances: 925 users: 926 - bypass_bob 927 - bypass_jane 928 teams: 929 - bypass_oncall 930 - bypass_sres 931 restrictions: 932 apps: 933 - content-app 934 teams: 935 - config-team 936 users: 937 - cindy 938 protect: true 939 orgs: 940 kubernetes: 941 repos: 942 test-infra: 943 `, 944 branchProtections: map[string]github.BranchProtection{ 945 "kubernetes/test-infra=master": { 946 EnforceAdmins: github.EnforceAdmins{Enabled: true}, 947 RequiredStatusChecks: &github.RequiredStatusChecks{ 948 Strict: true, 949 Contexts: []string{"config-presubmit"}, 950 }, 951 RequiredPullRequestReviews: &github.RequiredPullRequestReviews{ 952 DismissStaleReviews: false, 953 RequireCodeOwnerReviews: true, 954 RequiredApprovingReviewCount: 3, 955 DismissalRestrictions: &github.DismissalRestrictions{ 956 Users: []github.User{{Login: "bob"}, {Login: "jane"}}, 957 Teams: []github.Team{{Slug: "oncall"}, {Slug: "sres"}}, 958 }, 959 BypassRestrictions: &github.BypassRestrictions{ 960 Users: []github.User{{Login: "bypass_bob"}, {Login: "bypass_jane"}}, 961 Teams: []github.Team{{Slug: "bypass_oncall"}, {Slug: "bypass_sres"}}, 962 }, 963 }, 964 Restrictions: &github.Restrictions{ 965 Apps: []github.App{{Slug: "content-app"}}, 966 Users: []github.User{{Login: "cindy"}}, 967 Teams: []github.Team{{Slug: "config-team"}}, 968 }, 969 }, 970 }, 971 }, 972 // TODO: consider harmonizing apps handling with teams and users 973 { 974 name: "do not make update request if the only change is a unspecified app request", 975 branches: []string{"kubernetes/test-infra=master"}, 976 appInstallations: []github.AppInstallation{ 977 { 978 AppSlug: "content-app", 979 Permissions: github.InstallationPermissions{ 980 Contents: string(github.Write), 981 }, 982 }, 983 }, 984 collaborators: []github.User{ 985 { 986 Login: "cindy", 987 Permissions: github.RepoPermissions{Push: true}, 988 }, 989 }, 990 teams: []github.Team{ 991 { 992 Slug: "config-team", 993 Permission: github.RepoPush, 994 }, 995 }, 996 config: ` 997 branch-protection: 998 enforce_admins: true 999 required_status_checks: 1000 contexts: 1001 - config-presubmit 1002 strict: true 1003 required_pull_request_reviews: 1004 required_approving_review_count: 3 1005 dismiss_stale: false 1006 require_code_owner_reviews: true 1007 dismissal_restrictions: 1008 users: 1009 - bob 1010 - jane 1011 teams: 1012 - oncall 1013 - sres 1014 bypass_pull_request_allowances: 1015 users: 1016 - bypass_bob 1017 - bypass_jane 1018 teams: 1019 - bypass_oncall 1020 - bypass_sres 1021 restrictions: 1022 teams: 1023 - config-team 1024 users: 1025 - cindy 1026 protect: true 1027 orgs: 1028 kubernetes: 1029 repos: 1030 test-infra: 1031 `, 1032 branchProtections: map[string]github.BranchProtection{ 1033 "kubernetes/test-infra=master": { 1034 EnforceAdmins: github.EnforceAdmins{Enabled: true}, 1035 RequiredStatusChecks: &github.RequiredStatusChecks{ 1036 Strict: true, 1037 Contexts: []string{"config-presubmit"}, 1038 }, 1039 RequiredPullRequestReviews: &github.RequiredPullRequestReviews{ 1040 DismissStaleReviews: false, 1041 RequireCodeOwnerReviews: true, 1042 RequiredApprovingReviewCount: 3, 1043 DismissalRestrictions: &github.DismissalRestrictions{ 1044 Users: []github.User{{Login: "bob"}, {Login: "jane"}}, 1045 Teams: []github.Team{{Slug: "oncall"}, {Slug: "sres"}}, 1046 }, 1047 BypassRestrictions: &github.BypassRestrictions{ 1048 Users: []github.User{{Login: "bypass_bob"}, {Login: "bypass_jane"}}, 1049 Teams: []github.Team{{Slug: "bypass_oncall"}, {Slug: "bypass_sres"}}, 1050 }, 1051 }, 1052 Restrictions: &github.Restrictions{ 1053 Apps: []github.App{{Slug: "content-app"}}, 1054 Users: []github.User{{Login: "cindy"}}, 1055 Teams: []github.Team{{Slug: "config-team"}}, 1056 }, 1057 }, 1058 }, 1059 }, 1060 { 1061 name: "make request if branch protection is present, but out of date", 1062 branches: []string{"kubernetes/test-infra=master"}, 1063 config: ` 1064 branch-protection: 1065 enforce_admins: true 1066 required_pull_request_reviews: 1067 required_approving_review_count: 3 1068 protect: true 1069 orgs: 1070 kubernetes: 1071 repos: 1072 test-infra: 1073 `, 1074 branchProtections: map[string]github.BranchProtection{ 1075 "kubernetes/test-infra=master": { 1076 EnforceAdmins: github.EnforceAdmins{Enabled: true}, 1077 RequiredStatusChecks: &github.RequiredStatusChecks{ 1078 Strict: true, 1079 Contexts: []string{"config-presubmit"}, 1080 }, 1081 }, 1082 }, 1083 expected: []requirements{ 1084 { 1085 Org: "kubernetes", 1086 Repo: "test-infra", 1087 Branch: "master", 1088 Request: &github.BranchProtectionRequest{ 1089 EnforceAdmins: &yes, 1090 RequiredPullRequestReviews: &github.RequiredPullRequestReviewsRequest{ 1091 RequiredApprovingReviewCount: 3, 1092 }, 1093 }, 1094 }, 1095 }, 1096 }, 1097 { 1098 name: "excluded branches are not protected", 1099 branches: []string{"kubernetes/test-infra=master", "kubernetes/test-infra=skip"}, 1100 config: ` 1101 branch-protection: 1102 protect: true 1103 orgs: 1104 kubernetes: 1105 repos: 1106 test-infra: 1107 exclude: 1108 - sk.* 1109 `, 1110 1111 expected: []requirements{ 1112 { 1113 Org: "kubernetes", 1114 Repo: "test-infra", 1115 Branch: "master", 1116 Request: &github.BranchProtectionRequest{EnforceAdmins: &no}, 1117 }, 1118 }, 1119 }, 1120 { 1121 name: "org and repo level branch exclusions are combined", 1122 branches: []string{"kubernetes/test-infra=master", "kubernetes/test-infra=skip", "kubernetes/test-infra=foobar1"}, 1123 config: ` 1124 branch-protection: 1125 protect: true 1126 orgs: 1127 kubernetes: 1128 exclude: 1129 - foo.* 1130 repos: 1131 test-infra: 1132 exclude: 1133 - sk.* 1134 `, 1135 expected: []requirements{ 1136 { 1137 Org: "kubernetes", 1138 Repo: "test-infra", 1139 Branch: "master", 1140 Request: &github.BranchProtectionRequest{EnforceAdmins: &no}, 1141 }, 1142 }, 1143 }, 1144 { 1145 name: "explicitly specified branches are not affected by Exclude", 1146 branches: []string{"kubernetes/test-infra=master"}, 1147 config: ` 1148 branch-protection: 1149 protect: true 1150 orgs: 1151 kubernetes: 1152 exclude: 1153 - master.* 1154 repos: 1155 test-infra: 1156 branches: 1157 master: 1158 `, 1159 expected: []requirements{ 1160 { 1161 Org: "kubernetes", 1162 Repo: "test-infra", 1163 Branch: "master", 1164 Request: &github.BranchProtectionRequest{EnforceAdmins: &no}, 1165 }, 1166 }, 1167 }, 1168 { 1169 name: "do not make update request if the app, team or collaborator is not authorized", 1170 branches: []string{"org/unauthorized-app=master", "org/unauthorized-collaborator=master", "org/unauthorized-team=master"}, 1171 enableAppsRestrictions: true, 1172 config: ` 1173 branch-protection: 1174 protect: true 1175 orgs: 1176 org: 1177 repos: 1178 unauthorized-app: 1179 restrictions: 1180 apps: 1181 - nocontent-app 1182 unauthorized-collaborator: 1183 restrictions: 1184 users: 1185 - cindy 1186 unauthorized-team: 1187 restrictions: 1188 teams: 1189 - config-team 1190 `, 1191 errors: 1, 1192 }, 1193 { 1194 name: "make request for unauthorized collaborators/teams if the verify-restrictions feature flag is not set", 1195 branches: []string{"org/unauthorized=master"}, 1196 config: ` 1197 branch-protection: 1198 restrictions: 1199 teams: 1200 - config-team 1201 users: 1202 - cindy 1203 protect: true 1204 orgs: 1205 org: 1206 repos: 1207 unauthorized: 1208 protect: true 1209 `, 1210 skipVerifyRestrictions: true, 1211 expected: []requirements{ 1212 { 1213 Org: "org", 1214 Repo: "unauthorized", 1215 Branch: "master", 1216 Request: &github.BranchProtectionRequest{ 1217 EnforceAdmins: &no, 1218 Restrictions: &github.RestrictionsRequest{ 1219 Users: &[]string{"cindy"}, 1220 Teams: &[]string{"config-team"}, 1221 }, 1222 }, 1223 }, 1224 }, 1225 }, 1226 { 1227 name: "protect branches with special characters", 1228 branches: []string{"cfgdef/repo1=test_#123"}, 1229 config: ` 1230 branch-protection: 1231 protect: true 1232 orgs: 1233 cfgdef: 1234 `, 1235 expected: []requirements{ 1236 { 1237 Org: "cfgdef", 1238 Repo: "repo1", 1239 Branch: "test_#123", 1240 Request: &github.BranchProtectionRequest{ 1241 EnforceAdmins: &no, 1242 }, 1243 }, 1244 }, 1245 }, 1246 { 1247 name: "require linear history", 1248 branches: []string{"cfgdef/repo1=master", "cfgdef/repo1=branch", "cfgdef/repo2=master"}, 1249 config: ` 1250 branch-protection: 1251 protect: true 1252 required_linear_history: true 1253 orgs: 1254 cfgdef: 1255 `, 1256 expected: []requirements{ 1257 { 1258 Org: "cfgdef", 1259 Repo: "repo1", 1260 Branch: "master", 1261 Request: &github.BranchProtectionRequest{ 1262 EnforceAdmins: &no, 1263 RequiredLinearHistory: true, 1264 }, 1265 }, 1266 { 1267 Org: "cfgdef", 1268 Repo: "repo1", 1269 Branch: "branch", 1270 Request: &github.BranchProtectionRequest{ 1271 EnforceAdmins: &no, 1272 RequiredLinearHistory: true, 1273 }, 1274 }, 1275 { 1276 Org: "cfgdef", 1277 Repo: "repo2", 1278 Branch: "master", 1279 Request: &github.BranchProtectionRequest{ 1280 EnforceAdmins: &no, 1281 RequiredLinearHistory: true, 1282 }, 1283 }, 1284 }, 1285 }, 1286 { 1287 name: "allow force pushes", 1288 branches: []string{"cfgdef/repo1=master", "cfgdef/repo1=branch", "cfgdef/repo2=master"}, 1289 config: ` 1290 branch-protection: 1291 protect: true 1292 allow_force_pushes: true 1293 orgs: 1294 cfgdef: 1295 `, 1296 expected: []requirements{ 1297 { 1298 Org: "cfgdef", 1299 Repo: "repo1", 1300 Branch: "master", 1301 Request: &github.BranchProtectionRequest{ 1302 EnforceAdmins: &no, 1303 AllowForcePushes: true, 1304 }, 1305 }, 1306 { 1307 Org: "cfgdef", 1308 Repo: "repo1", 1309 Branch: "branch", 1310 Request: &github.BranchProtectionRequest{ 1311 EnforceAdmins: &no, 1312 AllowForcePushes: true, 1313 }, 1314 }, 1315 { 1316 Org: "cfgdef", 1317 Repo: "repo2", 1318 Branch: "master", 1319 Request: &github.BranchProtectionRequest{ 1320 EnforceAdmins: &no, 1321 AllowForcePushes: true, 1322 }, 1323 }, 1324 }, 1325 }, 1326 { 1327 name: "allow deletions", 1328 branches: []string{"cfgdef/repo1=master", "cfgdef/repo1=branch", "cfgdef/repo2=master"}, 1329 config: ` 1330 branch-protection: 1331 protect: true 1332 allow_deletions: true 1333 orgs: 1334 cfgdef: 1335 `, 1336 expected: []requirements{ 1337 { 1338 Org: "cfgdef", 1339 Repo: "repo1", 1340 Branch: "master", 1341 Request: &github.BranchProtectionRequest{ 1342 EnforceAdmins: &no, 1343 AllowDeletions: true, 1344 }, 1345 }, 1346 { 1347 Org: "cfgdef", 1348 Repo: "repo1", 1349 Branch: "branch", 1350 Request: &github.BranchProtectionRequest{ 1351 EnforceAdmins: &no, 1352 AllowDeletions: true, 1353 }, 1354 }, 1355 { 1356 Org: "cfgdef", 1357 Repo: "repo2", 1358 Branch: "master", 1359 Request: &github.BranchProtectionRequest{ 1360 EnforceAdmins: &no, 1361 AllowDeletions: true, 1362 }, 1363 }, 1364 }, 1365 }, 1366 { 1367 name: "Global unmanaged: true makes us not do anything", 1368 branches: []string{"cfgdef/repo1=master", "cfgdef/repo1=branch", "cfgdef/repo2=master"}, 1369 config: ` 1370 branch-protection: 1371 unmanaged: true 1372 orgs: 1373 cfgdef: 1374 repos: 1375 repo1: 1376 required_status_checks: 1377 contexts: 1378 - foo 1379 branches: 1380 master: 1381 required_status_checks: 1382 contexts: 1383 - foo 1384 `, 1385 }, 1386 { 1387 name: "Org-level unmanaged: true makes us ignore everything in that org", 1388 branches: []string{"cfgdef/repo1=master", "cfgdef/repo1=branch", "cfgdef/repo2=master"}, 1389 config: ` 1390 branch-protection: 1391 orgs: 1392 cfgdef: 1393 unmanaged: true 1394 repos: 1395 repo1: 1396 required_status_checks: 1397 contexts: 1398 - foo 1399 branches: 1400 master: 1401 required_status_checks: 1402 contexts: 1403 - foo 1404 `, 1405 }, 1406 { 1407 name: "Repo-level unmanaged: true makes us ignore everything in that repo", 1408 branches: []string{"cfgdef/repo1=master", "cfgdef/repo1=branch", "cfgdef/repo2=master"}, 1409 config: ` 1410 branch-protection: 1411 orgs: 1412 cfgdef: 1413 repos: 1414 repo1: 1415 unmanaged: true 1416 required_status_checks: 1417 contexts: 1418 - foo 1419 branches: 1420 master: 1421 required_status_checks: 1422 contexts: 1423 - foo 1424 repo2: 1425 required_status_checks: 1426 contexts: 1427 - foo 1428 branches: 1429 master: 1430 protect: true 1431 required_status_checks: 1432 contexts: 1433 - foo 1434 `, 1435 expected: []requirements{ 1436 { 1437 Org: "cfgdef", 1438 Repo: "repo2", 1439 Branch: "master", 1440 Request: &github.BranchProtectionRequest{ 1441 RequiredStatusChecks: &github.RequiredStatusChecks{Contexts: []string{"foo"}}, 1442 EnforceAdmins: &no, 1443 }, 1444 }, 1445 }, 1446 }, 1447 { 1448 name: "existing app restrictions with apps restrictions feature flag disabled", 1449 branches: []string{"org/apps-restrictions-disabled=master"}, 1450 enableAppsRestrictions: false, 1451 appInstallations: []github.AppInstallation{ 1452 { 1453 AppSlug: "content-app", 1454 Permissions: github.InstallationPermissions{ 1455 Contents: string(github.Write), 1456 }, 1457 }, 1458 }, 1459 collaborators: []github.User{ 1460 { 1461 Login: "cindy", 1462 Permissions: github.RepoPermissions{Push: true}, 1463 }, 1464 }, 1465 teams: []github.Team{ 1466 { 1467 Slug: "org-team", 1468 Permission: github.RepoPush, 1469 }, 1470 }, 1471 config: ` 1472 branch-protection: 1473 protect: true 1474 restrictions: 1475 users: 1476 - cindy 1477 orgs: 1478 org: 1479 restrictions: 1480 teams: 1481 - org-team 1482 `, 1483 expected: []requirements{ 1484 { 1485 Org: "org", 1486 Repo: "apps-restrictions-disabled", 1487 Branch: "master", 1488 Request: &github.BranchProtectionRequest{ 1489 EnforceAdmins: &no, 1490 Restrictions: &github.RestrictionsRequest{ 1491 Apps: nil, 1492 Users: &[]string{"cindy"}, 1493 Teams: &[]string{"org-team"}, 1494 }, 1495 }, 1496 }, 1497 }, 1498 }, 1499 { 1500 name: "configured app restrictions with app restriction feature gate disabled", 1501 branches: []string{"org/apps-restrictions-disabled=master"}, 1502 enableAppsRestrictions: false, 1503 config: ` 1504 branch-protection: 1505 protect: true 1506 restrictions: 1507 users: 1508 - cindy 1509 orgs: 1510 org: 1511 restrictions: 1512 apps: 1513 - content-app 1514 `, 1515 errors: 1, 1516 }, 1517 } 1518 1519 for _, tc := range cases { 1520 t.Run(tc.name, func(t *testing.T) { 1521 repos := map[string]map[string]bool{} 1522 branches := map[string][]github.Branch{} 1523 for _, b := range tc.branches { 1524 org, repo, branch := split(b) 1525 k := org + "/" + repo 1526 branches[k] = append(branches[k], github.Branch{ 1527 Name: branch, 1528 Protected: !tc.startUnprotected, 1529 }) 1530 r := repos[org] 1531 if r == nil { 1532 repos[org] = make(map[string]bool) 1533 } 1534 repos[org][repo] = true 1535 } 1536 fc := fakeClient{ 1537 branches: branches, 1538 repos: map[string][]github.Repo{}, 1539 branchProtections: tc.branchProtections, 1540 appInstallations: tc.appInstallations, 1541 collaborators: tc.collaborators, 1542 teams: tc.teams, 1543 } 1544 for org, r := range repos { 1545 for rname := range r { 1546 fc.repos[org] = append(fc.repos[org], github.Repo{Name: rname, FullName: org + "/" + rname, Archived: rname == tc.archived}) 1547 } 1548 } 1549 1550 var cfg config.Config 1551 if err := yaml.Unmarshal([]byte(tc.config), &cfg); err != nil { 1552 t.Fatalf("failed to parse config: %v", err) 1553 } 1554 1555 if tc.enabled == nil { 1556 tc.enabled = func(org, repo string) bool { return true } 1557 } 1558 p := protector{ 1559 client: &fc, 1560 cfg: &cfg, 1561 errors: Errors{}, 1562 updates: make(chan requirements), 1563 done: make(chan []error), 1564 completedRepos: make(map[string]bool), 1565 verifyRestrictions: !tc.skipVerifyRestrictions, 1566 enableAppsRestrictions: tc.enableAppsRestrictions, 1567 enabled: tc.enabled, 1568 } 1569 go func() { 1570 p.protect() 1571 close(p.updates) 1572 }() 1573 1574 var actual []requirements 1575 for r := range p.updates { 1576 actual = append(actual, r) 1577 } 1578 errors := p.errors.errs 1579 if len(errors) != tc.errors { 1580 t.Errorf("actual errors %d != expected %d: %v", len(errors), tc.errors, errors) 1581 } 1582 switch { 1583 case len(actual) != len(tc.expected): 1584 t.Errorf("%+v %+v", cfg.BranchProtection, actual) 1585 t.Errorf("actual updates differ from expected: %s", cmp.Diff(actual, tc.expected)) 1586 default: 1587 for _, a := range actual { 1588 found := false 1589 for _, e := range tc.expected { 1590 if e.Org == a.Org && e.Repo == a.Repo && e.Branch == a.Branch { 1591 found = true 1592 fixup(&a) 1593 fixup(&e) 1594 if !reflect.DeepEqual(e, a) { 1595 t.Errorf("actual != expected: %s", diff.ObjectDiff(a.Request, e.Request)) 1596 } 1597 break 1598 } 1599 } 1600 if !found { 1601 t.Errorf("actual updates %v not in expected %v", a, tc.expected) 1602 } 1603 } 1604 } 1605 }) 1606 } 1607 } 1608 1609 func fixup(r *requirements) { 1610 if r == nil || r.Request == nil { 1611 return 1612 } 1613 req := r.Request 1614 if req.RequiredStatusChecks != nil { 1615 sort.Strings(req.RequiredStatusChecks.Contexts) 1616 } 1617 if restr := req.Restrictions; restr != nil { 1618 sort.Strings(*restr.Teams) 1619 sort.Strings(*restr.Users) 1620 } 1621 } 1622 1623 func TestIgnoreArchivedRepos(t *testing.T) { 1624 testBranches := []string{"organization/repository=branch", "organization/archived=branch"} 1625 repos := map[string]map[string]bool{} 1626 branches := map[string][]github.Branch{} 1627 for _, b := range testBranches { 1628 org, repo, branch := split(b) 1629 k := org + "/" + repo 1630 branches[k] = append(branches[k], github.Branch{ 1631 Name: branch, 1632 }) 1633 r := repos[org] 1634 if r == nil { 1635 repos[org] = make(map[string]bool) 1636 } 1637 repos[org][repo] = true 1638 } 1639 fc := fakeClient{ 1640 branches: branches, 1641 repos: map[string][]github.Repo{}, 1642 } 1643 for org, r := range repos { 1644 for rname := range r { 1645 fc.repos[org] = append(fc.repos[org], github.Repo{Name: rname, FullName: org + "/" + rname, Archived: rname == "archived"}) 1646 } 1647 } 1648 1649 var cfg config.Config 1650 if err := yaml.Unmarshal([]byte(` 1651 branch-protection: 1652 protect: true 1653 orgs: 1654 organization: 1655 `), &cfg); err != nil { 1656 t.Fatalf("failed to parse config: %v", err) 1657 } 1658 p := protector{ 1659 client: &fc, 1660 cfg: &cfg, 1661 errors: Errors{}, 1662 updates: make(chan requirements), 1663 done: make(chan []error), 1664 completedRepos: make(map[string]bool), 1665 enabled: func(org, repo string) bool { return true }, 1666 } 1667 go func() { 1668 p.protect() 1669 close(p.updates) 1670 }() 1671 1672 protectionErrors := p.errors.errs 1673 if len(protectionErrors) != 0 { 1674 t.Errorf("expected no errors, got %d errors: %v", len(protectionErrors), protectionErrors) 1675 } 1676 var actual []requirements 1677 for r := range p.updates { 1678 actual = append(actual, r) 1679 } 1680 if len(actual) != 1 { 1681 t.Errorf("expected one update, got: %v", actual) 1682 } 1683 } 1684 1685 func TestIgnorePrivateSecurityRepos(t *testing.T) { 1686 testBranches := []string{"organization/repository=branch", "organization/repo-ghsa-1234abcd=branch"} 1687 repos := map[string]map[string]bool{} 1688 branches := map[string][]github.Branch{} 1689 for _, b := range testBranches { 1690 org, repo, branch := split(b) 1691 k := org + "/" + repo 1692 branches[k] = append(branches[k], github.Branch{ 1693 Name: branch, 1694 }) 1695 r := repos[org] 1696 if r == nil { 1697 repos[org] = make(map[string]bool) 1698 } 1699 repos[org][repo] = true 1700 } 1701 fc := fakeClient{ 1702 branches: branches, 1703 repos: map[string][]github.Repo{}, 1704 } 1705 for org, r := range repos { 1706 for rname := range r { 1707 fc.repos[org] = append(fc.repos[org], github.Repo{Name: rname, FullName: org + "/" + rname, Private: true}) 1708 } 1709 } 1710 1711 var cfg config.Config 1712 if err := yaml.Unmarshal([]byte(` 1713 branch-protection: 1714 protect: true 1715 orgs: 1716 organization: 1717 `), &cfg); err != nil { 1718 t.Fatalf("failed to parse config: %v", err) 1719 } 1720 p := protector{ 1721 client: &fc, 1722 cfg: &cfg, 1723 errors: Errors{}, 1724 updates: make(chan requirements), 1725 done: make(chan []error), 1726 completedRepos: make(map[string]bool), 1727 enabled: func(org, repo string) bool { return true }, 1728 } 1729 go func() { 1730 p.protect() 1731 close(p.updates) 1732 }() 1733 1734 protectionErrors := p.errors.errs 1735 if len(protectionErrors) != 0 { 1736 t.Errorf("expected no errors, got %d errors: %v", len(protectionErrors), protectionErrors) 1737 } 1738 var actual []requirements 1739 for r := range p.updates { 1740 actual = append(actual, r) 1741 } 1742 if len(actual) != 1 { 1743 t.Errorf("expected one update, got: %v", actual) 1744 } 1745 } 1746 1747 func TestEqualBranchProtection(t *testing.T) { 1748 yes := true 1749 var testCases = []struct { 1750 name string 1751 state *github.BranchProtection 1752 request *github.BranchProtectionRequest 1753 expected bool 1754 }{ 1755 { 1756 name: "neither set matches", 1757 expected: true, 1758 }, 1759 { 1760 name: "request unset doesn't match", 1761 state: &github.BranchProtection{}, 1762 expected: false, 1763 }, 1764 { 1765 name: "state unset doesn't match", 1766 request: &github.BranchProtectionRequest{}, 1767 expected: false, 1768 }, 1769 { 1770 name: "matching requests work", 1771 state: &github.BranchProtection{ 1772 RequiredStatusChecks: &github.RequiredStatusChecks{ 1773 Strict: true, 1774 Contexts: []string{"a", "b", "c"}, 1775 }, 1776 EnforceAdmins: github.EnforceAdmins{ 1777 Enabled: true, 1778 }, 1779 RequiredPullRequestReviews: &github.RequiredPullRequestReviews{ 1780 DismissStaleReviews: true, 1781 RequireCodeOwnerReviews: true, 1782 RequiredApprovingReviewCount: 1, 1783 DismissalRestrictions: &github.DismissalRestrictions{ 1784 Users: []github.User{{Login: "user"}}, 1785 Teams: []github.Team{{Slug: "team"}}, 1786 }, 1787 BypassRestrictions: &github.BypassRestrictions{ 1788 Users: []github.User{{Login: "user"}}, 1789 Teams: []github.Team{{Slug: "team"}}, 1790 }, 1791 }, 1792 Restrictions: &github.Restrictions{ 1793 Apps: []github.App{{Slug: "app"}}, 1794 Users: []github.User{{Login: "user"}}, 1795 Teams: []github.Team{{Slug: "team"}}, 1796 }, 1797 }, 1798 request: &github.BranchProtectionRequest{ 1799 RequiredStatusChecks: &github.RequiredStatusChecks{ 1800 Strict: true, 1801 Contexts: []string{"a", "b", "c"}, 1802 }, 1803 EnforceAdmins: &yes, 1804 RequiredPullRequestReviews: &github.RequiredPullRequestReviewsRequest{ 1805 DismissStaleReviews: true, 1806 RequireCodeOwnerReviews: true, 1807 RequiredApprovingReviewCount: 1, 1808 DismissalRestrictions: github.DismissalRestrictionsRequest{ 1809 Users: &[]string{"user"}, 1810 Teams: &[]string{"team"}, 1811 }, 1812 BypassRestrictions: github.BypassRestrictionsRequest{ 1813 Users: &[]string{"user"}, 1814 Teams: &[]string{"team"}, 1815 }, 1816 }, 1817 Restrictions: &github.RestrictionsRequest{ 1818 Apps: &[]string{"app"}, 1819 Users: &[]string{"user"}, 1820 Teams: &[]string{"team"}, 1821 }, 1822 }, 1823 expected: true, 1824 }, 1825 { 1826 name: "apps unspecified in request is not considered as change", 1827 state: &github.BranchProtection{ 1828 RequiredStatusChecks: &github.RequiredStatusChecks{ 1829 Strict: true, 1830 Contexts: []string{"a", "b", "c"}, 1831 }, 1832 EnforceAdmins: github.EnforceAdmins{ 1833 Enabled: true, 1834 }, 1835 RequiredPullRequestReviews: &github.RequiredPullRequestReviews{ 1836 DismissStaleReviews: true, 1837 RequireCodeOwnerReviews: true, 1838 RequiredApprovingReviewCount: 1, 1839 DismissalRestrictions: &github.DismissalRestrictions{ 1840 Users: []github.User{{Login: "user"}}, 1841 Teams: []github.Team{{Slug: "team"}}, 1842 }, 1843 BypassRestrictions: &github.BypassRestrictions{ 1844 Users: []github.User{{Login: "user"}}, 1845 Teams: []github.Team{{Slug: "team"}}, 1846 }, 1847 }, 1848 Restrictions: &github.Restrictions{ 1849 Apps: []github.App{{Slug: "app"}}, 1850 Users: []github.User{{Login: "user"}}, 1851 Teams: []github.Team{{Slug: "team"}}, 1852 }, 1853 }, 1854 request: &github.BranchProtectionRequest{ 1855 RequiredStatusChecks: &github.RequiredStatusChecks{ 1856 Strict: true, 1857 Contexts: []string{"a", "b", "c"}, 1858 }, 1859 EnforceAdmins: &yes, 1860 RequiredPullRequestReviews: &github.RequiredPullRequestReviewsRequest{ 1861 DismissStaleReviews: true, 1862 RequireCodeOwnerReviews: true, 1863 RequiredApprovingReviewCount: 1, 1864 DismissalRestrictions: github.DismissalRestrictionsRequest{ 1865 Users: &[]string{"user"}, 1866 Teams: &[]string{"team"}, 1867 }, 1868 BypassRestrictions: github.BypassRestrictionsRequest{ 1869 Users: &[]string{"user"}, 1870 Teams: &[]string{"team"}, 1871 }, 1872 }, 1873 Restrictions: &github.RestrictionsRequest{ 1874 Users: &[]string{"user"}, 1875 Teams: &[]string{"team"}, 1876 }, 1877 }, 1878 expected: true, 1879 }, 1880 { 1881 name: "AllowForcePushes is recognized", 1882 state: &github.BranchProtection{ 1883 AllowForcePushes: github.AllowForcePushes{ 1884 Enabled: false, 1885 }, 1886 }, 1887 request: &github.BranchProtectionRequest{ 1888 AllowForcePushes: true, 1889 }, 1890 }, 1891 } 1892 1893 for _, testCase := range testCases { 1894 if actual, expected := equalBranchProtections(testCase.state, testCase.request), testCase.expected; actual != expected { 1895 t.Errorf("%s: didn't compute equality correctly, expected %v got %v", testCase.name, expected, actual) 1896 } 1897 } 1898 } 1899 1900 func TestEqualStatusChecks(t *testing.T) { 1901 var testCases = []struct { 1902 name string 1903 state *github.RequiredStatusChecks 1904 request *github.RequiredStatusChecks 1905 expected bool 1906 }{ 1907 { 1908 name: "neither set matches", 1909 expected: true, 1910 }, 1911 { 1912 name: "request unset doesn't match", 1913 state: &github.RequiredStatusChecks{}, 1914 expected: false, 1915 }, 1916 { 1917 name: "state unset doesn't match", 1918 request: &github.RequiredStatusChecks{}, 1919 expected: false, 1920 }, 1921 { 1922 name: "matching requests work", 1923 state: &github.RequiredStatusChecks{ 1924 Strict: true, 1925 Contexts: []string{"a", "b", "c"}, 1926 }, 1927 1928 request: &github.RequiredStatusChecks{ 1929 Strict: true, 1930 Contexts: []string{"a", "b", "c"}, 1931 }, 1932 expected: true, 1933 }, 1934 { 1935 name: "not matching on strict", 1936 state: &github.RequiredStatusChecks{ 1937 Strict: true, 1938 Contexts: []string{"a", "b", "c"}, 1939 }, 1940 1941 request: &github.RequiredStatusChecks{ 1942 Strict: false, 1943 Contexts: []string{"a", "b", "c"}, 1944 }, 1945 expected: false, 1946 }, 1947 { 1948 name: "not matching on contexts", 1949 state: &github.RequiredStatusChecks{ 1950 Strict: true, 1951 Contexts: []string{"a", "b", "d"}, 1952 }, 1953 1954 request: &github.RequiredStatusChecks{ 1955 Strict: true, 1956 Contexts: []string{"a", "b", "c"}, 1957 }, 1958 expected: false, 1959 }, 1960 } 1961 1962 for _, testCase := range testCases { 1963 if actual, expected := equalRequiredStatusChecks(testCase.state, testCase.request), testCase.expected; actual != expected { 1964 t.Errorf("%s: didn't compute equality correctly, expected %v got %v", testCase.name, expected, actual) 1965 } 1966 } 1967 } 1968 1969 func TestEqualStringSlices(t *testing.T) { 1970 var testCases = []struct { 1971 name string 1972 state *[]string 1973 request *[]string 1974 expected bool 1975 }{ 1976 { 1977 name: "no slices", 1978 expected: true, 1979 }, 1980 { 1981 name: "a unset doesn't match", 1982 state: &[]string{}, 1983 expected: false, 1984 }, 1985 { 1986 name: "b unset doesn't match", 1987 request: &[]string{}, 1988 expected: false, 1989 }, 1990 { 1991 name: "matching slices work", 1992 state: &[]string{"a", "b", "c"}, 1993 request: &[]string{"a", "b", "c"}, 1994 expected: true, 1995 }, 1996 { 1997 name: "ordering doesn't matter", 1998 state: &[]string{"a", "c", "b"}, 1999 request: &[]string{"a", "b", "c"}, 2000 expected: true, 2001 }, 2002 { 2003 name: "unequal slices don't match", 2004 state: &[]string{"a", "b"}, 2005 request: &[]string{"a", "b", "c"}, 2006 expected: false, 2007 }, 2008 { 2009 name: "disoint slices don't match", 2010 state: &[]string{"e", "f", "g"}, 2011 request: &[]string{"a", "b", "c"}, 2012 expected: false, 2013 }, 2014 } 2015 2016 for _, testCase := range testCases { 2017 if actual, expected := equalStringSlices(testCase.state, testCase.request), testCase.expected; actual != expected { 2018 t.Errorf("%s: didn't compute equality correctly, expected %v got %v", testCase.name, expected, actual) 2019 } 2020 } 2021 } 2022 2023 func TestEqualAdminEnforcement(t *testing.T) { 2024 yes, no := true, false 2025 var testCases = []struct { 2026 name string 2027 state github.EnforceAdmins 2028 request *bool 2029 expected bool 2030 }{ 2031 { 2032 name: "unset request matches no enforcement", 2033 state: github.EnforceAdmins{Enabled: false}, 2034 expected: true, 2035 }, 2036 { 2037 name: "set request matches enforcement", 2038 state: github.EnforceAdmins{Enabled: false}, 2039 request: &no, 2040 expected: true, 2041 }, 2042 { 2043 name: "set request doesn't match enforcement", 2044 state: github.EnforceAdmins{Enabled: false}, 2045 request: &yes, 2046 expected: false, 2047 }, 2048 } 2049 2050 for _, testCase := range testCases { 2051 if actual, expected := equalAdminEnforcement(testCase.state, testCase.request), testCase.expected; actual != expected { 2052 t.Errorf("%s: didn't compute equality correctly, expected %v got %v", testCase.name, expected, actual) 2053 } 2054 } 2055 } 2056 2057 func TestEqualRequiredPullRequestReviews(t *testing.T) { 2058 var testCases = []struct { 2059 name string 2060 state *github.RequiredPullRequestReviews 2061 request *github.RequiredPullRequestReviewsRequest 2062 expected bool 2063 }{ 2064 { 2065 name: "neither set matches", 2066 expected: true, 2067 }, 2068 { 2069 name: "request unset doesn't match", 2070 state: &github.RequiredPullRequestReviews{}, 2071 expected: false, 2072 }, 2073 { 2074 name: "state unset doesn't match", 2075 request: &github.RequiredPullRequestReviewsRequest{}, 2076 expected: false, 2077 }, 2078 { 2079 name: "matching requests work", 2080 state: &github.RequiredPullRequestReviews{ 2081 DismissStaleReviews: true, 2082 RequireCodeOwnerReviews: true, 2083 RequiredApprovingReviewCount: 1, 2084 DismissalRestrictions: &github.DismissalRestrictions{ 2085 Users: []github.User{{Login: "user"}}, 2086 Teams: []github.Team{{Slug: "team"}}, 2087 }, 2088 BypassRestrictions: &github.BypassRestrictions{ 2089 Users: []github.User{{Login: "user"}}, 2090 Teams: []github.Team{{Slug: "team"}}, 2091 }, 2092 }, 2093 request: &github.RequiredPullRequestReviewsRequest{ 2094 DismissStaleReviews: true, 2095 RequireCodeOwnerReviews: true, 2096 RequiredApprovingReviewCount: 1, 2097 DismissalRestrictions: github.DismissalRestrictionsRequest{ 2098 Users: &[]string{"user"}, 2099 Teams: &[]string{"team"}, 2100 }, 2101 BypassRestrictions: github.BypassRestrictionsRequest{ 2102 Users: &[]string{"user"}, 2103 Teams: &[]string{"team"}, 2104 }, 2105 }, 2106 expected: true, 2107 }, 2108 { 2109 name: "not matching on dismissal", 2110 state: &github.RequiredPullRequestReviews{ 2111 DismissStaleReviews: true, 2112 RequireCodeOwnerReviews: true, 2113 RequiredApprovingReviewCount: 1, 2114 }, 2115 request: &github.RequiredPullRequestReviewsRequest{ 2116 DismissStaleReviews: false, 2117 RequireCodeOwnerReviews: true, 2118 RequiredApprovingReviewCount: 1, 2119 }, 2120 expected: false, 2121 }, 2122 { 2123 name: "not matching on reviews", 2124 state: &github.RequiredPullRequestReviews{ 2125 DismissStaleReviews: true, 2126 RequireCodeOwnerReviews: true, 2127 RequiredApprovingReviewCount: 1, 2128 }, 2129 request: &github.RequiredPullRequestReviewsRequest{ 2130 DismissStaleReviews: true, 2131 RequireCodeOwnerReviews: false, 2132 RequiredApprovingReviewCount: 1, 2133 }, 2134 expected: false, 2135 }, 2136 { 2137 name: "not matching on count", 2138 state: &github.RequiredPullRequestReviews{ 2139 DismissStaleReviews: true, 2140 RequireCodeOwnerReviews: true, 2141 RequiredApprovingReviewCount: 1, 2142 }, 2143 request: &github.RequiredPullRequestReviewsRequest{ 2144 DismissStaleReviews: true, 2145 RequireCodeOwnerReviews: true, 2146 RequiredApprovingReviewCount: 2, 2147 }, 2148 expected: false, 2149 }, 2150 { 2151 name: "not matching on restrictions", 2152 state: &github.RequiredPullRequestReviews{ 2153 DismissStaleReviews: true, 2154 RequireCodeOwnerReviews: true, 2155 RequiredApprovingReviewCount: 1, 2156 DismissalRestrictions: &github.DismissalRestrictions{ 2157 Users: []github.User{{Login: "user"}}, 2158 Teams: []github.Team{{Slug: "team"}}, 2159 }, 2160 BypassRestrictions: &github.BypassRestrictions{ 2161 Users: []github.User{{Login: "user"}}, 2162 Teams: []github.Team{{Slug: "team"}}, 2163 }, 2164 }, 2165 request: &github.RequiredPullRequestReviewsRequest{ 2166 DismissStaleReviews: true, 2167 RequireCodeOwnerReviews: true, 2168 RequiredApprovingReviewCount: 1, 2169 DismissalRestrictions: github.DismissalRestrictionsRequest{ 2170 Users: &[]string{"other"}, 2171 Teams: &[]string{"team"}, 2172 }, 2173 BypassRestrictions: github.BypassRestrictionsRequest{ 2174 Users: &[]string{"other"}, 2175 Teams: &[]string{"team"}, 2176 }, 2177 }, 2178 expected: false, 2179 }, 2180 } 2181 2182 for _, testCase := range testCases { 2183 if actual, expected := equalRequiredPullRequestReviews(testCase.state, testCase.request), testCase.expected; actual != expected { 2184 t.Errorf("%s: didn't compute equality correctly, expected %v got %v", testCase.name, expected, actual) 2185 } 2186 } 2187 } 2188 2189 func TestEqualDismissalRestrictions(t *testing.T) { 2190 var testCases = []struct { 2191 name string 2192 state *github.DismissalRestrictions 2193 request *github.DismissalRestrictionsRequest 2194 expected bool 2195 }{ 2196 { 2197 name: "neither set matches", 2198 expected: true, 2199 }, 2200 { 2201 name: "request unset doesn't match", 2202 state: &github.DismissalRestrictions{}, 2203 expected: false, 2204 }, 2205 { 2206 name: "matching requests work", 2207 state: &github.DismissalRestrictions{ 2208 Users: []github.User{{Login: "user"}}, 2209 Teams: []github.Team{{Slug: "team"}}, 2210 }, 2211 request: &github.DismissalRestrictionsRequest{ 2212 Users: &[]string{"user"}, 2213 Teams: &[]string{"team"}, 2214 }, 2215 expected: true, 2216 }, 2217 { 2218 name: "user login casing is ignored", 2219 state: &github.DismissalRestrictions{ 2220 Users: []github.User{{Login: "User"}, {Login: "OTHer"}}, 2221 Teams: []github.Team{{Slug: "team"}}, 2222 }, 2223 request: &github.DismissalRestrictionsRequest{ 2224 Users: &[]string{"uSer", "oThER"}, 2225 Teams: &[]string{"team"}, 2226 }, 2227 expected: true, 2228 }, 2229 { 2230 name: "not matching on users", 2231 state: &github.DismissalRestrictions{ 2232 Users: []github.User{{Login: "user"}}, 2233 Teams: []github.Team{{Slug: "team"}}, 2234 }, 2235 request: &github.DismissalRestrictionsRequest{ 2236 Users: &[]string{"other"}, 2237 Teams: &[]string{"team"}, 2238 }, 2239 expected: false, 2240 }, 2241 { 2242 name: "not matching on team", 2243 state: &github.DismissalRestrictions{ 2244 Users: []github.User{{Login: "user"}}, 2245 Teams: []github.Team{{Slug: "team"}}, 2246 }, 2247 request: &github.DismissalRestrictionsRequest{ 2248 Users: &[]string{"user"}, 2249 Teams: &[]string{"other"}, 2250 }, 2251 expected: false, 2252 }, 2253 { 2254 name: "both unset", 2255 request: &github.DismissalRestrictionsRequest{}, 2256 expected: true, 2257 }, 2258 { 2259 name: "partially unset", 2260 request: &github.DismissalRestrictionsRequest{ 2261 Teams: &[]string{"team"}, 2262 }, 2263 expected: false, 2264 }, 2265 } 2266 2267 for _, testCase := range testCases { 2268 if actual, expected := equalDismissalRestrictions(testCase.state, testCase.request), testCase.expected; actual != expected { 2269 t.Errorf("%s: didn't compute equality correctly, expected %v got %v", testCase.name, expected, actual) 2270 } 2271 } 2272 } 2273 2274 func TestEqualBypassRestrictions(t *testing.T) { 2275 var testCases = []struct { 2276 name string 2277 state *github.BypassRestrictions 2278 request *github.BypassRestrictionsRequest 2279 expected bool 2280 }{ 2281 { 2282 name: "neither set matches", 2283 expected: true, 2284 }, 2285 { 2286 name: "request unset doesn't match", 2287 state: &github.BypassRestrictions{}, 2288 expected: false, 2289 }, 2290 { 2291 name: "matching requests work", 2292 state: &github.BypassRestrictions{ 2293 Users: []github.User{{Login: "user"}}, 2294 Teams: []github.Team{{Slug: "team"}}, 2295 }, 2296 request: &github.BypassRestrictionsRequest{ 2297 Users: &[]string{"user"}, 2298 Teams: &[]string{"team"}, 2299 }, 2300 expected: true, 2301 }, 2302 { 2303 name: "user login casing is ignored", 2304 state: &github.BypassRestrictions{ 2305 Users: []github.User{{Login: "User"}, {Login: "OTHer"}}, 2306 Teams: []github.Team{{Slug: "team"}}, 2307 }, 2308 request: &github.BypassRestrictionsRequest{ 2309 Users: &[]string{"uSer", "oThER"}, 2310 Teams: &[]string{"team"}, 2311 }, 2312 expected: true, 2313 }, 2314 { 2315 name: "not matching on users", 2316 state: &github.BypassRestrictions{ 2317 Users: []github.User{{Login: "user"}}, 2318 Teams: []github.Team{{Slug: "team"}}, 2319 }, 2320 request: &github.BypassRestrictionsRequest{ 2321 Users: &[]string{"other"}, 2322 Teams: &[]string{"team"}, 2323 }, 2324 expected: false, 2325 }, 2326 { 2327 name: "not matching on team", 2328 state: &github.BypassRestrictions{ 2329 Users: []github.User{{Login: "user"}}, 2330 Teams: []github.Team{{Slug: "team"}}, 2331 }, 2332 request: &github.BypassRestrictionsRequest{ 2333 Users: &[]string{"user"}, 2334 Teams: &[]string{"other"}, 2335 }, 2336 expected: false, 2337 }, 2338 { 2339 name: "both unset", 2340 request: &github.BypassRestrictionsRequest{}, 2341 expected: true, 2342 }, 2343 { 2344 name: "partially unset", 2345 request: &github.BypassRestrictionsRequest{ 2346 Teams: &[]string{"team"}, 2347 }, 2348 expected: false, 2349 }, 2350 } 2351 2352 for _, testCase := range testCases { 2353 if actual, expected := equalBypassRestrictions(testCase.state, testCase.request), testCase.expected; actual != expected { 2354 t.Errorf("%s: didn't compute equality correctly, expected %v got %v", testCase.name, expected, actual) 2355 } 2356 } 2357 } 2358 2359 func TestEqualRestrictions(t *testing.T) { 2360 var testCases = []struct { 2361 name string 2362 state *github.Restrictions 2363 request *github.RestrictionsRequest 2364 expected bool 2365 }{ 2366 { 2367 name: "neither set matches", 2368 expected: true, 2369 }, 2370 { 2371 name: "request unset doesn't match", 2372 state: &github.Restrictions{}, 2373 expected: false, 2374 }, 2375 { 2376 name: "matching requests work", 2377 state: &github.Restrictions{ 2378 Apps: []github.App{{Slug: "app"}}, 2379 Users: []github.User{{Login: "user"}}, 2380 Teams: []github.Team{{Slug: "team"}}, 2381 }, 2382 request: &github.RestrictionsRequest{ 2383 Apps: &[]string{"app"}, 2384 Users: &[]string{"user"}, 2385 Teams: &[]string{"team"}, 2386 }, 2387 expected: true, 2388 }, 2389 { 2390 name: "user login casing is ignored", 2391 state: &github.Restrictions{ 2392 Users: []github.User{{Login: "User"}, {Login: "OTHer"}}, 2393 Teams: []github.Team{{Slug: "team"}}, 2394 }, 2395 request: &github.RestrictionsRequest{ 2396 Users: &[]string{"uSer", "oThER"}, 2397 Teams: &[]string{"team"}, 2398 }, 2399 expected: true, 2400 }, 2401 { 2402 name: "not matching on users", 2403 state: &github.Restrictions{ 2404 Users: []github.User{{Login: "user"}}, 2405 Teams: []github.Team{{Slug: "team"}}, 2406 }, 2407 request: &github.RestrictionsRequest{ 2408 Users: &[]string{"other"}, 2409 Teams: &[]string{"team"}, 2410 }, 2411 expected: false, 2412 }, 2413 { 2414 name: "not matching on team", 2415 state: &github.Restrictions{ 2416 Users: []github.User{{Login: "user"}}, 2417 Teams: []github.Team{{Slug: "team"}}, 2418 }, 2419 request: &github.RestrictionsRequest{ 2420 Users: &[]string{"user"}, 2421 Teams: &[]string{"other"}, 2422 }, 2423 expected: false, 2424 }, 2425 { 2426 name: "not matching on app", 2427 state: &github.Restrictions{ 2428 Apps: []github.App{{Slug: "app"}}, 2429 Users: []github.User{{Login: "user"}}, 2430 Teams: []github.Team{{Slug: "team"}}, 2431 }, 2432 request: &github.RestrictionsRequest{ 2433 Apps: &[]string{"other"}, 2434 Users: &[]string{"user"}, 2435 Teams: &[]string{"team"}, 2436 }, 2437 expected: false, 2438 }, 2439 { 2440 name: "both unset", 2441 request: &github.RestrictionsRequest{}, 2442 expected: true, 2443 }, 2444 { 2445 name: "partially unset", 2446 request: &github.RestrictionsRequest{ 2447 Teams: &[]string{"team"}, 2448 }, 2449 expected: false, 2450 }, 2451 // TODO: consider harmonizing apps handling with teams and users 2452 { 2453 name: "app request unset", 2454 state: &github.Restrictions{ 2455 Apps: []github.App{{Slug: "app"}}, 2456 Users: []github.User{{Login: "user"}}, 2457 Teams: []github.Team{{Slug: "team"}}, 2458 }, 2459 request: &github.RestrictionsRequest{ 2460 Users: &[]string{"user"}, 2461 Teams: &[]string{"team"}, 2462 }, 2463 expected: true, 2464 }, 2465 { 2466 name: "app request is empty list", 2467 state: &github.Restrictions{ 2468 Apps: []github.App{{Slug: "app"}}, 2469 Users: []github.User{{Login: "user"}}, 2470 Teams: []github.Team{{Slug: "team"}}, 2471 }, 2472 request: &github.RestrictionsRequest{ 2473 Apps: &[]string{}, 2474 Users: &[]string{"user"}, 2475 Teams: &[]string{"team"}, 2476 }, 2477 expected: false, 2478 }, 2479 } 2480 2481 for _, testCase := range testCases { 2482 if actual, expected := equalRestrictions(testCase.state, testCase.request), testCase.expected; actual != expected { 2483 t.Errorf("%s: didn't compute equality correctly, expected %v got %v", testCase.name, expected, actual) 2484 } 2485 } 2486 } 2487 2488 func TestValidateRequest(t *testing.T) { 2489 var testCases = []struct { 2490 name string 2491 request *github.BranchProtectionRequest 2492 appInstallations []string 2493 collaborators []string 2494 teams []string 2495 errs []error 2496 }{ 2497 { 2498 name: "restrict to unauthorized apps results in error", 2499 request: &github.BranchProtectionRequest{ 2500 Restrictions: &github.RestrictionsRequest{ 2501 Apps: &[]string{"bar"}, 2502 }, 2503 }, 2504 errs: []error{fmt.Errorf("the following apps are not authorized for %s/%s: [%s]", "org", "repo", "bar")}, 2505 }, 2506 { 2507 name: "restrict to unathorized collaborator results in error", 2508 request: &github.BranchProtectionRequest{ 2509 Restrictions: &github.RestrictionsRequest{ 2510 Users: &[]string{"foo"}, 2511 }, 2512 }, 2513 errs: []error{fmt.Errorf("the following collaborators are not authorized for %s/%s: [%s]", "org", "repo", "foo")}, 2514 }, 2515 { 2516 name: "restrict to unauthorized team results in error", 2517 request: &github.BranchProtectionRequest{ 2518 Restrictions: &github.RestrictionsRequest{ 2519 Teams: &[]string{"bar"}, 2520 }, 2521 }, 2522 errs: []error{fmt.Errorf("the following teams are not authorized for %s/%s: [%s]", "org", "repo", "bar")}, 2523 }, 2524 { 2525 name: "authorized app, user and team result in no errors", 2526 request: &github.BranchProtectionRequest{ 2527 Restrictions: &github.RestrictionsRequest{ 2528 Apps: &[]string{"foobar"}, 2529 Users: &[]string{"foo"}, 2530 Teams: &[]string{"bar"}, 2531 }, 2532 }, 2533 appInstallations: []string{"foobar"}, 2534 collaborators: []string{"foo"}, 2535 teams: []string{"bar"}, 2536 }, 2537 } 2538 2539 for _, tc := range testCases { 2540 t.Run(tc.name, func(t *testing.T) { 2541 errs := validateRestrictions("org", "repo", tc.request, tc.appInstallations, tc.collaborators, tc.teams) 2542 if !reflect.DeepEqual(errs, tc.errs) { 2543 t.Errorf("%s: errors %v != expected %v", tc.name, errs, tc.errs) 2544 } 2545 }) 2546 } 2547 } 2548 2549 func TestAuthorizedApps(t *testing.T) { 2550 var testCases = []struct { 2551 name string 2552 appInstallations []github.AppInstallation 2553 expected []string 2554 }{ 2555 { 2556 name: "AppInstallations with content read is not included", 2557 appInstallations: []github.AppInstallation{ 2558 { 2559 AppSlug: "foo", 2560 Permissions: github.InstallationPermissions{ 2561 Contents: string(github.Read), 2562 }, 2563 }, 2564 }, 2565 }, 2566 { 2567 name: "AppInstallations with content read is included", 2568 appInstallations: []github.AppInstallation{ 2569 { 2570 AppSlug: "foo", 2571 Permissions: github.InstallationPermissions{ 2572 Contents: string(github.Write), 2573 }, 2574 }, 2575 }, 2576 expected: []string{"foo"}, 2577 }, 2578 } 2579 2580 for _, tc := range testCases { 2581 t.Run(tc.name, func(t *testing.T) { 2582 fc := fakeClient{appInstallations: tc.appInstallations} 2583 p := protector{ 2584 client: &fc, 2585 errors: Errors{}, 2586 } 2587 2588 apps, err := p.authorizedApps("org") 2589 if err != nil { 2590 t.Errorf("Unexpected error: %v", err) 2591 } 2592 sort.Strings(tc.expected) 2593 sort.Strings(apps) 2594 if !reflect.DeepEqual(tc.expected, apps) { 2595 t.Errorf("expected: %v, got: %v", tc.expected, apps) 2596 } 2597 }) 2598 } 2599 } 2600 2601 func TestAuthorizedCollaborators(t *testing.T) { 2602 var testCases = []struct { 2603 name string 2604 collaborators []github.User 2605 expected []string 2606 }{ 2607 { 2608 name: "Collaborator with pull permission is not included", 2609 collaborators: []github.User{ 2610 { 2611 Login: "foo", 2612 Permissions: github.RepoPermissions{ 2613 Pull: true, 2614 }, 2615 }, 2616 }, 2617 }, 2618 { 2619 name: "Collaborators with Push or Admin permission are included", 2620 collaborators: []github.User{ 2621 { 2622 Login: "foo", 2623 Permissions: github.RepoPermissions{ 2624 Push: true, 2625 }, 2626 }, 2627 { 2628 Login: "bar", 2629 Permissions: github.RepoPermissions{ 2630 Admin: true, 2631 }, 2632 }, 2633 }, 2634 expected: []string{"foo", "bar"}, 2635 }, 2636 } 2637 2638 for _, tc := range testCases { 2639 t.Run(tc.name, func(t *testing.T) { 2640 fc := fakeClient{collaborators: tc.collaborators} 2641 p := protector{ 2642 client: &fc, 2643 errors: Errors{}, 2644 } 2645 2646 collaborators, err := p.authorizedCollaborators("org", "repo") 2647 if err != nil { 2648 t.Errorf("Unexpected error: %v", err) 2649 } 2650 sort.Strings(tc.expected) 2651 sort.Strings(collaborators) 2652 if !reflect.DeepEqual(tc.expected, collaborators) { 2653 t.Errorf("expected: %v, got: %v", tc.expected, collaborators) 2654 } 2655 }) 2656 } 2657 } 2658 2659 func TestAuthorizedTeams(t *testing.T) { 2660 var testCases = []struct { 2661 name string 2662 teams []github.Team 2663 expected []string 2664 }{ 2665 { 2666 name: "Team with pull permission is not included", 2667 teams: []github.Team{ 2668 { 2669 Slug: "foo", 2670 Permission: github.RepoPull, 2671 }, 2672 }, 2673 }, 2674 { 2675 name: "Teams with Push or Admin permission are included", 2676 teams: []github.Team{ 2677 { 2678 Slug: "foo", 2679 Permission: github.RepoPush, 2680 }, 2681 { 2682 Slug: "bar", 2683 Permission: github.RepoAdmin, 2684 }, 2685 }, 2686 expected: []string{"foo", "bar"}, 2687 }, 2688 } 2689 2690 for _, tc := range testCases { 2691 t.Run(tc.name, func(t *testing.T) { 2692 fc := fakeClient{teams: tc.teams} 2693 p := protector{ 2694 client: &fc, 2695 errors: Errors{}, 2696 } 2697 2698 teams, err := p.authorizedTeams("org", "repo") 2699 if err != nil { 2700 t.Errorf("Unexpected error: %v", err) 2701 } 2702 sort.Strings(tc.expected) 2703 sort.Strings(teams) 2704 if !reflect.DeepEqual(tc.expected, teams) { 2705 t.Errorf("expected: %v, got: %v", tc.expected, teams) 2706 } 2707 }) 2708 } 2709 }