github.com/jenkins-x/test-infra@v0.0.7/prow/config/branch_protection_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 config 18 19 import ( 20 "reflect" 21 "sort" 22 "testing" 23 24 "k8s.io/apimachinery/pkg/util/diff" 25 ) 26 27 var ( 28 y = true 29 n = false 30 yes = &y 31 no = &n 32 ) 33 34 func normalize(policy *Policy) { 35 if policy == nil || policy.RequiredStatusChecks == nil { 36 return 37 } 38 sort.Strings(policy.RequiredStatusChecks.Contexts) 39 } 40 41 func TestSelectBool(t *testing.T) { 42 cases := []struct { 43 name string 44 parent *bool 45 child *bool 46 expected *bool 47 }{ 48 { 49 name: "default is nil", 50 }, 51 { 52 name: "use child if set", 53 child: yes, 54 expected: yes, 55 }, 56 { 57 name: "child overrides parent", 58 child: yes, 59 parent: no, 60 expected: yes, 61 }, 62 { 63 name: "use parent if child unset", 64 parent: no, 65 expected: no, 66 }, 67 } 68 for _, tc := range cases { 69 t.Run(tc.name, func(t *testing.T) { 70 actual := selectBool(tc.parent, tc.child) 71 if !reflect.DeepEqual(actual, tc.expected) { 72 t.Errorf("actual %v != expected %v", actual, tc.expected) 73 } 74 }) 75 } 76 } 77 78 func TestSelectInt(t *testing.T) { 79 one := 1 80 two := 2 81 cases := []struct { 82 name string 83 parent *int 84 child *int 85 expected *int 86 }{ 87 { 88 name: "default is nil", 89 }, 90 { 91 name: "use child if set", 92 child: &one, 93 expected: &one, 94 }, 95 { 96 name: "child overrides parent", 97 child: &one, 98 parent: &two, 99 expected: &one, 100 }, 101 { 102 name: "use parent if child unset", 103 parent: &two, 104 expected: &two, 105 }, 106 } 107 for _, tc := range cases { 108 t.Run(tc.name, func(t *testing.T) { 109 actual := selectInt(tc.parent, tc.child) 110 if !reflect.DeepEqual(actual, tc.expected) { 111 t.Errorf("actual %v != expected %v", actual, tc.expected) 112 } 113 }) 114 } 115 } 116 117 func TestUnionStrings(t *testing.T) { 118 cases := []struct { 119 name string 120 parent []string 121 child []string 122 expected []string 123 }{ 124 { 125 name: "empty list", 126 }, 127 { 128 name: "all parent items", 129 parent: []string{"hi", "there"}, 130 expected: []string{"hi", "there"}, 131 }, 132 { 133 name: "all child items", 134 child: []string{"hi", "there"}, 135 expected: []string{"hi", "there"}, 136 }, 137 { 138 name: "both child and parent items, no duplicates", 139 child: []string{"hi", "world"}, 140 parent: []string{"hi", "there"}, 141 expected: []string{"hi", "there", "world"}, 142 }, 143 } 144 for _, tc := range cases { 145 t.Run(tc.name, func(t *testing.T) { 146 actual := unionStrings(tc.parent, tc.child) 147 sort.Strings(actual) 148 sort.Strings(tc.expected) 149 if !reflect.DeepEqual(actual, tc.expected) { 150 t.Errorf("actual %v != expected %v", actual, tc.expected) 151 } 152 }) 153 } 154 } 155 156 func TestApply(test *testing.T) { 157 t := true 158 f := false 159 basic := Policy{ 160 Protect: &t, 161 } 162 ebasic := Policy{ 163 Protect: &t, 164 } 165 cases := []struct { 166 name string 167 parent Policy 168 child Policy 169 expected Policy 170 }{ 171 { 172 name: "nil child", 173 parent: basic, 174 expected: ebasic, 175 }, 176 { 177 name: "merge parent and child", 178 parent: Policy{ 179 Protect: &t, 180 }, 181 child: Policy{ 182 Admins: &f, 183 }, 184 expected: Policy{ 185 Protect: &t, 186 Admins: &f, 187 }, 188 }, 189 { 190 name: "child overrides parent", 191 parent: Policy{ 192 Protect: &t, 193 }, 194 child: Policy{ 195 Protect: &f, 196 }, 197 expected: Policy{ 198 Protect: &f, 199 }, 200 }, 201 { 202 name: "append strings", 203 parent: Policy{ 204 RequiredStatusChecks: &ContextPolicy{ 205 Contexts: []string{"hello", "world"}, 206 }, 207 }, 208 child: Policy{ 209 RequiredStatusChecks: &ContextPolicy{ 210 Contexts: []string{"world", "of", "thrones"}, 211 }, 212 }, 213 expected: Policy{ 214 RequiredStatusChecks: &ContextPolicy{ 215 Contexts: []string{"hello", "of", "thrones", "world"}, 216 }, 217 }, 218 }, 219 { 220 name: "merge struct", 221 parent: Policy{ 222 RequiredStatusChecks: &ContextPolicy{ 223 Contexts: []string{"hi"}, 224 }, 225 }, 226 child: Policy{ 227 RequiredStatusChecks: &ContextPolicy{ 228 Strict: &t, 229 }, 230 }, 231 expected: Policy{ 232 RequiredStatusChecks: &ContextPolicy{ 233 Contexts: []string{"hi"}, 234 Strict: &t, 235 }, 236 }, 237 }, 238 { 239 name: "nil child struct", 240 parent: Policy{ 241 RequiredStatusChecks: &ContextPolicy{ 242 Strict: &f, 243 }, 244 }, 245 child: Policy{ 246 Protect: &t, 247 }, 248 expected: Policy{ 249 RequiredStatusChecks: &ContextPolicy{ 250 Strict: &f, 251 }, 252 Protect: &t, 253 }, 254 }, 255 { 256 name: "nil parent struct", 257 child: Policy{ 258 RequiredStatusChecks: &ContextPolicy{ 259 Strict: &f, 260 }, 261 }, 262 parent: Policy{ 263 Protect: &t, 264 }, 265 expected: Policy{ 266 RequiredStatusChecks: &ContextPolicy{ 267 Strict: &f, 268 }, 269 Protect: &t, 270 }, 271 }, 272 } 273 274 for _, tc := range cases { 275 test.Run(tc.name, func(test *testing.T) { 276 defer func() { 277 if r := recover(); r != nil { 278 test.Errorf("unexpected panic: %s", r) 279 } 280 }() 281 actual, err := tc.parent.Apply(tc.child) 282 if err != nil { 283 test.Fatalf("unexpected error: %v", err) 284 } 285 normalize(&actual) 286 normalize(&tc.expected) 287 if !reflect.DeepEqual(actual, tc.expected) { 288 test.Errorf("bad merged policy:\n%s", diff.ObjectReflectDiff(tc.expected, actual)) 289 } 290 }) 291 } 292 } 293 294 func TestJobRequirements(t *testing.T) { 295 cases := []struct { 296 name string 297 config []Presubmit 298 masterExpected, otherExpected []string 299 masterOptional, otherOptional []string 300 }{ 301 { 302 name: "basic", 303 config: []Presubmit{ 304 { 305 Context: "always-run", 306 AlwaysRun: true, 307 SkipReport: false, 308 }, 309 { 310 Context: "run-if-changed", 311 RegexpChangeMatcher: RegexpChangeMatcher{ 312 RunIfChanged: "foo", 313 }, 314 AlwaysRun: false, 315 SkipReport: false, 316 }, 317 { 318 Context: "not-always", 319 AlwaysRun: false, 320 SkipReport: false, 321 }, 322 { 323 Context: "skip-report", 324 AlwaysRun: true, 325 SkipReport: true, 326 Brancher: Brancher{ 327 SkipBranches: []string{"master"}, 328 }, 329 }, 330 { 331 Context: "optional", 332 AlwaysRun: true, 333 SkipReport: false, 334 Optional: true, 335 }, 336 }, 337 masterExpected: []string{"always-run", "run-if-changed"}, 338 masterOptional: []string{"optional"}, 339 otherExpected: []string{"always-run", "run-if-changed"}, 340 otherOptional: []string{"skip-report", "optional"}, 341 }, 342 { 343 name: "children", 344 config: []Presubmit{ 345 { 346 Context: "always-run", 347 AlwaysRun: true, 348 SkipReport: false, 349 RunAfterSuccess: []Presubmit{ 350 { 351 Context: "include-me", 352 }, 353 }, 354 }, 355 { 356 Context: "run-if-changed", 357 RegexpChangeMatcher: RegexpChangeMatcher{ 358 RunIfChanged: "foo", 359 }, 360 SkipReport: true, 361 AlwaysRun: false, 362 RunAfterSuccess: []Presubmit{ 363 { 364 Context: "me2", 365 }, 366 }, 367 }, 368 { 369 Context: "run-and-skip", 370 AlwaysRun: true, 371 SkipReport: true, 372 RunAfterSuccess: []Presubmit{ 373 { 374 Context: "also-me-3", 375 }, 376 }, 377 }, 378 { 379 Context: "optional", 380 AlwaysRun: false, 381 SkipReport: false, 382 RunAfterSuccess: []Presubmit{ 383 { 384 Context: "no thanks", 385 }, 386 }, 387 }, 388 { 389 Context: "hidden-grandpa", 390 AlwaysRun: true, 391 SkipReport: true, 392 RunAfterSuccess: []Presubmit{ 393 { 394 Context: "hidden-parent", 395 Optional: true, 396 AlwaysRun: false, 397 Brancher: Brancher{ 398 Branches: []string{"master"}, 399 }, 400 RunAfterSuccess: []Presubmit{ 401 { 402 Context: "visible-kid", 403 Brancher: Brancher{ 404 Branches: []string{"master"}, 405 }, 406 }, 407 }, 408 }, 409 }, 410 }, 411 }, 412 masterExpected: []string{ 413 "always-run", "include-me", 414 "me2", 415 "also-me-3", 416 "visible-kid", 417 }, 418 masterOptional: []string{ 419 "run-if-changed", 420 "run-and-skip", 421 "hidden-grandpa", 422 "hidden-parent"}, 423 otherExpected: []string{ 424 "always-run", "include-me", 425 "me2", 426 "also-me-3", 427 }, 428 otherOptional: []string{ 429 "run-if-changed", 430 "run-and-skip", 431 "hidden-grandpa"}, 432 }, 433 } 434 435 for _, tc := range cases { 436 if err := SetPresubmitRegexes(tc.config); err != nil { 437 t.Fatalf("could not set regexes: %v", err) 438 } 439 masterActual, masterOptional := jobRequirements(tc.config, "master", false) 440 if !reflect.DeepEqual(masterActual, tc.masterExpected) { 441 t.Errorf("branch: master - %s: actual %v != expected %v", tc.name, masterActual, tc.masterExpected) 442 } 443 if !reflect.DeepEqual(masterOptional, tc.masterOptional) { 444 t.Errorf("branch: master - optional - %s: actual %v != expected %v", tc.name, masterOptional, tc.masterOptional) 445 } 446 otherActual, otherOptional := jobRequirements(tc.config, "other", false) 447 if !reflect.DeepEqual(masterActual, tc.masterExpected) { 448 t.Errorf("branch: other - %s: actual %v != expected %v", tc.name, otherActual, tc.otherExpected) 449 } 450 if !reflect.DeepEqual(otherOptional, tc.otherOptional) { 451 t.Errorf("branch: other - optional - %s: actual %v != expected %v", tc.name, otherOptional, tc.otherOptional) 452 } 453 } 454 } 455 456 func TestConfig_GetBranchProtection(t *testing.T) { 457 testCases := []struct { 458 name string 459 config Config 460 org, repo, branch string 461 err bool 462 expected *Policy 463 }{ 464 { 465 name: "unprotected by default", 466 }, 467 { 468 name: "undefined org not protected", 469 config: Config{ 470 ProwConfig: ProwConfig{ 471 BranchProtection: BranchProtection{ 472 Policy: Policy{ 473 Protect: yes, 474 }, 475 Orgs: map[string]Org{ 476 "unknown": {}, 477 }, 478 }, 479 }, 480 }, 481 }, 482 { 483 name: "protect via config default", 484 config: Config{ 485 ProwConfig: ProwConfig{ 486 BranchProtection: BranchProtection{ 487 Policy: Policy{ 488 Protect: yes, 489 }, 490 Orgs: map[string]Org{ 491 "org": {}, 492 }, 493 }, 494 }, 495 }, 496 expected: &Policy{Protect: yes}, 497 }, 498 { 499 name: "protect via org default", 500 config: Config{ 501 ProwConfig: ProwConfig{ 502 BranchProtection: BranchProtection{ 503 Orgs: map[string]Org{ 504 "org": { 505 Policy: Policy{ 506 Protect: yes, 507 }, 508 }, 509 }, 510 }, 511 }, 512 }, 513 expected: &Policy{Protect: yes}, 514 }, 515 { 516 name: "protect via repo default", 517 config: Config{ 518 ProwConfig: ProwConfig{ 519 BranchProtection: BranchProtection{ 520 Orgs: map[string]Org{ 521 "org": { 522 Repos: map[string]Repo{ 523 "repo": { 524 Policy: Policy{ 525 Protect: yes, 526 }, 527 }, 528 }, 529 }, 530 }, 531 }, 532 }, 533 }, 534 expected: &Policy{Protect: yes}, 535 }, 536 { 537 name: "protect specific branch", 538 config: Config{ 539 ProwConfig: ProwConfig{ 540 BranchProtection: BranchProtection{ 541 Orgs: map[string]Org{ 542 "org": { 543 Repos: map[string]Repo{ 544 "repo": { 545 Branches: map[string]Branch{ 546 "branch": { 547 Policy: Policy{ 548 Protect: yes, 549 }, 550 }, 551 }, 552 }, 553 }, 554 }, 555 }, 556 }, 557 }, 558 }, 559 expected: &Policy{Protect: yes}, 560 }, 561 { 562 name: "ignore other org settings", 563 config: Config{ 564 ProwConfig: ProwConfig{ 565 BranchProtection: BranchProtection{ 566 Policy: Policy{ 567 Protect: no, 568 }, 569 Orgs: map[string]Org{ 570 "other": { 571 Policy: Policy{Protect: yes}, 572 }, 573 "org": {}, 574 }, 575 }, 576 }, 577 }, 578 expected: &Policy{Protect: no}, 579 }, 580 { 581 name: "defined branches must make a protection decision", 582 config: Config{ 583 ProwConfig: ProwConfig{ 584 BranchProtection: BranchProtection{ 585 Orgs: map[string]Org{ 586 "org": { 587 Repos: map[string]Repo{ 588 "repo": { 589 Branches: map[string]Branch{ 590 "branch": {}, 591 }, 592 }, 593 }, 594 }, 595 }, 596 }, 597 }, 598 }, 599 err: true, 600 }, 601 { 602 name: "pushers require protection", 603 config: Config{ 604 ProwConfig: ProwConfig{ 605 BranchProtection: BranchProtection{ 606 Policy: Policy{ 607 Protect: no, 608 Restrictions: &Restrictions{ 609 Teams: []string{"oncall"}, 610 }, 611 }, 612 Orgs: map[string]Org{ 613 "org": {}, 614 }, 615 }, 616 }, 617 }, 618 err: true, 619 }, 620 { 621 name: "required contexts require protection", 622 config: Config{ 623 ProwConfig: ProwConfig{ 624 BranchProtection: BranchProtection{ 625 Policy: Policy{ 626 Protect: no, 627 RequiredStatusChecks: &ContextPolicy{ 628 Contexts: []string{"test-foo"}, 629 }, 630 }, 631 Orgs: map[string]Org{ 632 "org": {}, 633 }, 634 }, 635 }, 636 }, 637 err: true, 638 }, 639 { 640 name: "child policy with defined parent can disable protection", 641 config: Config{ 642 ProwConfig: ProwConfig{ 643 BranchProtection: BranchProtection{ 644 AllowDisabledPolicies: true, 645 Policy: Policy{ 646 Protect: yes, 647 Restrictions: &Restrictions{ 648 Teams: []string{"oncall"}, 649 }, 650 }, 651 Orgs: map[string]Org{ 652 "org": { 653 Policy: Policy{ 654 Protect: no, 655 }, 656 }, 657 }, 658 }, 659 }, 660 }, 661 expected: &Policy{ 662 Protect: no, 663 }, 664 }, 665 { 666 name: "Make required presubmits required", 667 config: Config{ 668 ProwConfig: ProwConfig{ 669 BranchProtection: BranchProtection{ 670 Policy: Policy{ 671 Protect: yes, 672 RequiredStatusChecks: &ContextPolicy{ 673 Contexts: []string{"cla"}, 674 }, 675 }, 676 Orgs: map[string]Org{ 677 "org": {}, 678 }, 679 }, 680 }, 681 JobConfig: JobConfig{ 682 Presubmits: map[string][]Presubmit{ 683 "org/repo": { 684 { 685 JobBase: JobBase{ 686 Name: "required presubmit", 687 }, 688 Context: "required presubmit", 689 AlwaysRun: true, 690 }, 691 }, 692 }, 693 }, 694 }, 695 expected: &Policy{ 696 Protect: yes, 697 RequiredStatusChecks: &ContextPolicy{ 698 Contexts: []string{"required presubmit", "cla"}, 699 }, 700 }, 701 }, 702 { 703 name: "ProtectTested opts into protection", 704 config: Config{ 705 ProwConfig: ProwConfig{ 706 BranchProtection: BranchProtection{ 707 ProtectTested: true, 708 Orgs: map[string]Org{ 709 "org": {}, 710 }, 711 }, 712 }, 713 JobConfig: JobConfig{ 714 Presubmits: map[string][]Presubmit{ 715 "org/repo": { 716 { 717 JobBase: JobBase{ 718 Name: "required presubmit", 719 }, 720 Context: "required presubmit", 721 AlwaysRun: true, 722 }, 723 }, 724 }, 725 }, 726 }, 727 expected: &Policy{ 728 Protect: yes, 729 RequiredStatusChecks: &ContextPolicy{ 730 Contexts: []string{"required presubmit"}, 731 }, 732 }, 733 }, 734 { 735 name: "required presubmits require protection", 736 config: Config{ 737 ProwConfig: ProwConfig{ 738 BranchProtection: BranchProtection{ 739 Policy: Policy{ 740 Protect: no, 741 }, 742 Orgs: map[string]Org{ 743 "org": {}, 744 }, 745 }, 746 }, 747 JobConfig: JobConfig{ 748 Presubmits: map[string][]Presubmit{ 749 "org/repo": { 750 { 751 JobBase: JobBase{ 752 Name: "required presubmit", 753 }, 754 Context: "required presubmit", 755 AlwaysRun: true, 756 }, 757 }, 758 }, 759 }, 760 }, 761 err: true, 762 }, 763 { 764 name: "Optional presubmits do not force protection", 765 config: Config{ 766 ProwConfig: ProwConfig{ 767 BranchProtection: BranchProtection{ 768 ProtectTested: true, 769 Orgs: map[string]Org{ 770 "org": {}, 771 }, 772 }, 773 }, 774 JobConfig: JobConfig{ 775 Presubmits: map[string][]Presubmit{ 776 "org/repo": { 777 { 778 JobBase: JobBase{ 779 Name: "optional presubmit", 780 }, 781 Context: "optional presubmit", 782 AlwaysRun: true, 783 Optional: true, 784 }, 785 }, 786 }, 787 }, 788 }, 789 }, 790 { 791 name: "Explicit configuration takes precedence over ProtectTested", 792 config: Config{ 793 ProwConfig: ProwConfig{ 794 BranchProtection: BranchProtection{ 795 ProtectTested: true, 796 Orgs: map[string]Org{ 797 "org": { 798 Policy: Policy{ 799 Protect: yes, 800 }, 801 }, 802 }, 803 }, 804 }, 805 JobConfig: JobConfig{ 806 Presubmits: map[string][]Presubmit{ 807 "org/repo": { 808 { 809 JobBase: JobBase{ 810 Name: "optional presubmit", 811 }, 812 Context: "optional presubmit", 813 AlwaysRun: true, 814 Optional: true, 815 }, 816 }, 817 }, 818 }, 819 }, 820 expected: &Policy{Protect: yes}, 821 }, 822 } 823 824 for _, tc := range testCases { 825 t.Run(tc.name, func(t *testing.T) { 826 actual, err := tc.config.GetBranchProtection("org", "repo", "branch") 827 switch { 828 case err != nil: 829 if !tc.err { 830 t.Errorf("unexpected error: %v", err) 831 } 832 case err == nil && tc.err: 833 t.Errorf("failed to receive an error") 834 default: 835 normalize(actual) 836 normalize(tc.expected) 837 if !reflect.DeepEqual(actual, tc.expected) { 838 t.Errorf("actual %+v != expected %+v", actual, tc.expected) 839 } 840 } 841 }) 842 } 843 }