sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/trigger/trigger_test.go (about) 1 /* 2 Copyright 2019 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 trigger 18 19 import ( 20 "context" 21 "errors" 22 "testing" 23 "time" 24 25 "github.com/sirupsen/logrus" 26 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/runtime" 29 "k8s.io/apimachinery/pkg/util/sets" 30 clienttesting "k8s.io/client-go/testing" 31 32 utilpointer "k8s.io/utils/pointer" 33 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 34 "sigs.k8s.io/prow/pkg/client/clientset/versioned/fake" 35 "sigs.k8s.io/prow/pkg/config" 36 "sigs.k8s.io/prow/pkg/git/v2" 37 "sigs.k8s.io/prow/pkg/github" 38 "sigs.k8s.io/prow/pkg/github/fakegithub" 39 "sigs.k8s.io/prow/pkg/plugins" 40 ) 41 42 func TestHelpProvider(t *testing.T) { 43 enabledRepos := []config.OrgRepo{ 44 {Org: "org1", Repo: "repo"}, 45 {Org: "org2", Repo: "repo"}, 46 } 47 cases := []struct { 48 name string 49 config *plugins.Configuration 50 enabledRepos []config.OrgRepo 51 err bool 52 }{ 53 { 54 name: "Empty config", 55 config: &plugins.Configuration{}, 56 enabledRepos: enabledRepos, 57 }, 58 { 59 name: "All configs enabled", 60 config: &plugins.Configuration{ 61 Triggers: []plugins.Trigger{ 62 { 63 Repos: []string{"org2/repo"}, 64 TrustedOrg: "org2", 65 JoinOrgURL: "https://join.me", 66 OnlyOrgMembers: true, 67 IgnoreOkToTest: true, 68 }, 69 }, 70 }, 71 enabledRepos: enabledRepos, 72 }, 73 } 74 for _, c := range cases { 75 t.Run(c.name, func(t *testing.T) { 76 _, err := helpProvider(c.config, c.enabledRepos) 77 if err != nil && !c.err { 78 t.Fatalf("helpProvider error: %v", err) 79 } 80 }) 81 } 82 } 83 84 func TestRunRequested(t *testing.T) { 85 var testCases = []struct { 86 name string 87 88 pr *github.PullRequest 89 90 requestedJobs []config.Presubmit 91 jobCreationErrs sets.Set[string] // job names which fail creation 92 93 expectedJobs sets.Set[string] // by name 94 expectedErr bool 95 }{ 96 { 97 name: "nothing requested means nothing done", 98 pr: &github.PullRequest{}, 99 }, 100 { 101 name: "disjoint sets of jobs get triggered", 102 pr: &github.PullRequest{ 103 Base: github.PullRequestBranch{ 104 Repo: github.Repo{ 105 Owner: github.User{ 106 Login: "org", 107 }, 108 Name: "repo", 109 }, 110 Ref: "branch", 111 }, 112 Head: github.PullRequestBranch{ 113 SHA: "foobar1", 114 }, 115 }, 116 requestedJobs: []config.Presubmit{{ 117 JobBase: config.JobBase{ 118 Name: "first", 119 }, 120 Reporter: config.Reporter{Context: "first-context"}, 121 }, { 122 JobBase: config.JobBase{ 123 Name: "second", 124 }, 125 Reporter: config.Reporter{Context: "second-context"}, 126 }}, 127 expectedJobs: sets.New[string]("first", "second"), 128 }, 129 { 130 name: "all requested jobs get run", 131 pr: &github.PullRequest{ 132 Base: github.PullRequestBranch{ 133 Repo: github.Repo{ 134 Owner: github.User{ 135 Login: "org", 136 }, 137 Name: "repo", 138 }, 139 Ref: "branch", 140 }, 141 Head: github.PullRequestBranch{ 142 SHA: "foobar1", 143 }, 144 }, 145 requestedJobs: []config.Presubmit{{ 146 JobBase: config.JobBase{ 147 Name: "first", 148 }, 149 Reporter: config.Reporter{Context: "first-context"}, 150 }, { 151 JobBase: config.JobBase{ 152 Name: "second", 153 }, 154 Reporter: config.Reporter{Context: "second-context"}, 155 }}, 156 expectedJobs: sets.New[string]("first", "second"), 157 }, 158 { 159 name: "failure on job creation bubbles up but doesn't stop others from starting", 160 pr: &github.PullRequest{ 161 Base: github.PullRequestBranch{ 162 Repo: github.Repo{ 163 Owner: github.User{ 164 Login: "org", 165 }, 166 Name: "repo", 167 }, 168 Ref: "branch", 169 }, 170 Head: github.PullRequestBranch{ 171 SHA: "foobar1", 172 }, 173 }, 174 requestedJobs: []config.Presubmit{{ 175 JobBase: config.JobBase{ 176 Name: "first", 177 }, 178 Reporter: config.Reporter{Context: "first-context"}, 179 }, { 180 JobBase: config.JobBase{ 181 Name: "second", 182 }, 183 Reporter: config.Reporter{Context: "second-context"}, 184 }}, 185 jobCreationErrs: sets.New[string]("first"), 186 expectedJobs: sets.New[string]("second"), 187 expectedErr: true, 188 }, 189 { 190 name: "no errors and unmergable PR means we should see no trigger", 191 pr: &github.PullRequest{ 192 Base: github.PullRequestBranch{ 193 Repo: github.Repo{ 194 Owner: github.User{ 195 Login: "org", 196 }, 197 Name: "repo", 198 }, 199 Ref: "branch", 200 }, 201 Head: github.PullRequestBranch{ 202 SHA: "foobar1", 203 }, 204 Mergable: utilpointer.Bool(false), 205 }, 206 requestedJobs: []config.Presubmit{{ 207 JobBase: config.JobBase{ 208 Name: "first", 209 }, 210 Reporter: config.Reporter{Context: "first-context"}, 211 }, { 212 JobBase: config.JobBase{ 213 Name: "second", 214 }, 215 Reporter: config.Reporter{Context: "second-context"}, 216 }}, 217 expectedJobs: sets.New[string](), 218 }, 219 } 220 221 for _, testCase := range testCases { 222 t.Run(testCase.name, func(t *testing.T) { 223 var fakeGitHubClient fakegithub.FakeClient 224 fakeProwJobClient := fake.NewSimpleClientset() 225 fakeProwJobClient.PrependReactor("*", "*", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { 226 switch action := action.(type) { 227 case clienttesting.CreateActionImpl: 228 prowJob, ok := action.Object.(*prowapi.ProwJob) 229 if !ok { 230 return false, nil, nil 231 } 232 if testCase.jobCreationErrs.Has(prowJob.Spec.Job) { 233 return true, action.Object, errors.New("failed to create job") 234 } 235 } 236 return false, nil, nil 237 }) 238 client := Client{ 239 Config: &config.Config{}, 240 GitHubClient: &fakeGitHubClient, 241 ProwJobClient: fakeProwJobClient.ProwV1().ProwJobs("prowjobs"), 242 Logger: logrus.WithField("testcase", testCase.name), 243 } 244 245 err := runRequested(client, testCase.pr, fakegithub.TestRef, testCase.requestedJobs, "event-guid", nil, time.Nanosecond) 246 if err == nil && testCase.expectedErr { 247 t.Error("failed to receive an error") 248 } 249 if err != nil && !testCase.expectedErr { 250 t.Errorf("unexpected error: %v", err) 251 } 252 253 observedCreatedProwJobs := sets.New[string]() 254 existingProwJobs, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").List(context.Background(), metav1.ListOptions{}) 255 if err != nil { 256 t.Errorf("could not list current state of prow jobs: %v", err) 257 return 258 } 259 for _, job := range existingProwJobs.Items { 260 observedCreatedProwJobs.Insert(job.Spec.Job) 261 } 262 263 if missing := testCase.expectedJobs.Difference(observedCreatedProwJobs); missing.Len() > 0 { 264 t.Errorf("didn't create all expected ProwJobs, missing: %s", sets.List(missing)) 265 } 266 if extra := observedCreatedProwJobs.Difference(testCase.expectedJobs); extra.Len() > 0 { 267 t.Errorf("created unexpected ProwJobs: %s", sets.List(extra)) 268 } 269 }) 270 } 271 } 272 273 func TestValidateContextOverlap(t *testing.T) { 274 var testCases = []struct { 275 name string 276 toRun, toSkip []config.Presubmit 277 expectedErr bool 278 }{ 279 { 280 name: "empty inputs mean no error", 281 toRun: []config.Presubmit{}, 282 toSkip: []config.Presubmit{}, 283 }, 284 { 285 name: "disjoint sets mean no error", 286 toRun: []config.Presubmit{{Reporter: config.Reporter{Context: "foo"}}}, 287 toSkip: []config.Presubmit{{Reporter: config.Reporter{Context: "bar"}}}, 288 }, 289 { 290 name: "complex disjoint sets mean no error", 291 toRun: []config.Presubmit{{Reporter: config.Reporter{Context: "foo"}}, {Reporter: config.Reporter{Context: "otherfoo"}}}, 292 toSkip: []config.Presubmit{{Reporter: config.Reporter{Context: "bar"}}, {Reporter: config.Reporter{Context: "otherbar"}}}, 293 }, 294 { 295 name: "overlapping sets error", 296 toRun: []config.Presubmit{{Reporter: config.Reporter{Context: "foo"}}, {Reporter: config.Reporter{Context: "otherfoo"}}}, 297 toSkip: []config.Presubmit{{Reporter: config.Reporter{Context: "bar"}}, {Reporter: config.Reporter{Context: "otherfoo"}}}, 298 expectedErr: true, 299 }, 300 { 301 name: "identical sets error", 302 toRun: []config.Presubmit{{Reporter: config.Reporter{Context: "foo"}}, {Reporter: config.Reporter{Context: "otherfoo"}}}, 303 toSkip: []config.Presubmit{{Reporter: config.Reporter{Context: "foo"}}, {Reporter: config.Reporter{Context: "otherfoo"}}}, 304 expectedErr: true, 305 }, 306 { 307 name: "superset callErrors", 308 toRun: []config.Presubmit{{Reporter: config.Reporter{Context: "foo"}}, {Reporter: config.Reporter{Context: "otherfoo"}}}, 309 toSkip: []config.Presubmit{{Reporter: config.Reporter{Context: "foo"}}, {Reporter: config.Reporter{Context: "otherfoo"}}, {Reporter: config.Reporter{Context: "thirdfoo"}}}, 310 expectedErr: true, 311 }, 312 } 313 314 for _, testCase := range testCases { 315 validateErr := validateContextOverlap(testCase.toRun, testCase.toSkip) 316 if validateErr == nil && testCase.expectedErr { 317 t.Errorf("%s: expected an error but got none", testCase.name) 318 } 319 if validateErr != nil && !testCase.expectedErr { 320 t.Errorf("%s: expected no error but got one: %v", testCase.name, validateErr) 321 } 322 } 323 } 324 325 func TestTrustedUser(t *testing.T) { 326 var testcases = []struct { 327 name string 328 329 onlyOrgMembers bool 330 trustedApps []string 331 trustedOrg string 332 333 user string 334 org string 335 repo string 336 337 expectedTrusted bool 338 expectedReason string 339 }{ 340 { 341 name: "user is member of trusted org", 342 onlyOrgMembers: false, 343 user: "test", 344 org: "kubernetes", 345 repo: "kubernetes", 346 expectedTrusted: true, 347 }, 348 { 349 name: "user is member of trusted org (only org members enabled)", 350 onlyOrgMembers: true, 351 user: "test", 352 org: "kubernetes", 353 repo: "kubernetes", 354 expectedTrusted: true, 355 }, 356 { 357 name: "user is collaborator", 358 onlyOrgMembers: false, 359 user: "test-collaborator", 360 org: "kubernetes", 361 repo: "kubernetes", 362 expectedTrusted: true, 363 }, 364 { 365 name: "user is collaborator (only org members enabled)", 366 onlyOrgMembers: true, 367 user: "test-collaborator", 368 org: "kubernetes", 369 repo: "kubernetes", 370 expectedTrusted: false, 371 expectedReason: (notMember).String(), 372 }, 373 { 374 name: "user is trusted org member", 375 onlyOrgMembers: false, 376 trustedOrg: "kubernetes", 377 user: "test", 378 org: "kubernetes-sigs", 379 repo: "test", 380 expectedTrusted: true, 381 }, 382 { 383 name: "user is not org member", 384 onlyOrgMembers: false, 385 user: "test-2", 386 org: "kubernetes", 387 repo: "kubernetes", 388 expectedTrusted: false, 389 expectedReason: (notMember | notCollaborator).String(), 390 }, 391 { 392 name: "user is not org member or trusted org member", 393 onlyOrgMembers: false, 394 trustedOrg: "kubernetes-sigs", 395 user: "test-2", 396 org: "kubernetes", 397 repo: "kubernetes", 398 expectedTrusted: false, 399 expectedReason: (notMember | notCollaborator | notSecondaryMember).String(), 400 }, 401 { 402 name: "user is not org member or trusted org member, onlyOrgMembers true", 403 onlyOrgMembers: true, 404 trustedOrg: "kubernetes-sigs", 405 user: "test-2", 406 org: "kubernetes", 407 repo: "kubernetes", 408 expectedTrusted: false, 409 expectedReason: (notMember | notSecondaryMember).String(), 410 }, 411 { 412 name: "Self as bot is trusted", 413 user: "k8s-ci-robot", 414 expectedTrusted: true, 415 }, 416 { 417 name: "Self as app is trusted", 418 user: "k8s-ci-robot[bot]", 419 expectedTrusted: true, 420 }, 421 { 422 name: "github-app[bot] is in trusted list", 423 user: "github-app[bot]", 424 trustedApps: []string{"github-app"}, 425 expectedTrusted: true, 426 }, 427 { 428 name: "github-app[bot] is not in trusted list", 429 user: "github-app[bot]", 430 trustedApps: []string{"other-app"}, 431 expectedTrusted: false, 432 expectedReason: (notMember | notCollaborator).String(), 433 }, 434 } 435 436 for _, tc := range testcases { 437 t.Run(tc.name, func(t *testing.T) { 438 fc := fakegithub.NewFakeClient() 439 fc.OrgMembers = map[string][]string{ 440 "kubernetes": {"test"}, 441 } 442 fc.Collaborators = []string{"test-collaborator"} 443 444 trustedResponse, err := TrustedUser(fc, tc.onlyOrgMembers, tc.trustedApps, tc.trustedOrg, tc.user, tc.org, tc.repo) 445 if err != nil { 446 t.Errorf("For case %s, didn't expect error from TrustedUser: %v", tc.name, err) 447 } 448 if trustedResponse.IsTrusted != tc.expectedTrusted { 449 t.Errorf("For case %s, expect trusted: %v, but got: %v", tc.name, tc.expectedTrusted, trustedResponse.IsTrusted) 450 } 451 if trustedResponse.Reason != tc.expectedReason { 452 t.Errorf("For case %s, expect trusted reason: %v, but got: %v", tc.name, tc.expectedReason, trustedResponse.Reason) 453 } 454 }) 455 } 456 } 457 458 func TestGetPresubmits(t *testing.T) { 459 const orgRepo = "my-org/my-repo" 460 461 testCases := []struct { 462 name string 463 cfg *config.Config 464 465 expectedPresubmits sets.Set[string] 466 }{ 467 { 468 name: "Result of GetPresubmits is used by default", 469 cfg: &config.Config{ 470 JobConfig: config.JobConfig{ 471 PresubmitsStatic: map[string][]config.Presubmit{ 472 orgRepo: {{ 473 JobBase: config.JobBase{Name: "my-static-presubmit"}, 474 }}, 475 }, 476 ProwYAMLGetterWithDefaults: func(_ *config.Config, _ git.ClientFactory, _, _, _ string, _ ...string) (*config.ProwYAML, error) { 477 return &config.ProwYAML{ 478 Presubmits: []config.Presubmit{{ 479 JobBase: config.JobBase{Name: "my-inrepoconfig-presubmit"}, 480 }}, 481 }, nil 482 }, 483 }, 484 ProwConfig: config.ProwConfig{ 485 InRepoConfig: config.InRepoConfig{Enabled: map[string]*bool{"*": utilpointer.Bool(true)}}, 486 }, 487 }, 488 489 expectedPresubmits: sets.New[string]("my-inrepoconfig-presubmit", "my-static-presubmit"), 490 }, 491 { 492 name: "Fallback to static presubmits", 493 cfg: &config.Config{ 494 JobConfig: config.JobConfig{ 495 PresubmitsStatic: map[string][]config.Presubmit{ 496 orgRepo: {{ 497 JobBase: config.JobBase{Name: "my-static-presubmit"}, 498 }}, 499 }, 500 ProwYAMLGetterWithDefaults: func(_ *config.Config, _ git.ClientFactory, _, _, _ string, _ ...string) (*config.ProwYAML, error) { 501 return &config.ProwYAML{ 502 Presubmits: []config.Presubmit{{ 503 JobBase: config.JobBase{Name: "my-inrepoconfig-presubmit"}, 504 }}, 505 }, errors.New("some error") 506 }, 507 }, 508 ProwConfig: config.ProwConfig{ 509 InRepoConfig: config.InRepoConfig{Enabled: map[string]*bool{"*": utilpointer.Bool(true)}}, 510 }, 511 }, 512 513 expectedPresubmits: sets.New[string]("my-static-presubmit"), 514 }, 515 } 516 517 shaGetter := func() (string, error) { 518 return "", nil 519 } 520 521 for _, tc := range testCases { 522 t.Run(tc.name, func(t *testing.T) { 523 presubmits := getPresubmits(logrus.NewEntry(logrus.New()), nil, tc.cfg, orgRepo, shaGetter, shaGetter) 524 actualPresubmits := sets.Set[string]{} 525 for _, presubmit := range presubmits { 526 actualPresubmits.Insert(presubmit.Name) 527 } 528 529 if !tc.expectedPresubmits.Equal(actualPresubmits) { 530 t.Errorf("got a different set of presubmits than expected, diff: %v", tc.expectedPresubmits.Difference(actualPresubmits)) 531 } 532 }) 533 } 534 } 535 536 func TestGetPostsubmits(t *testing.T) { 537 const orgRepo = "my-org/my-repo" 538 539 testCases := []struct { 540 name string 541 cfg *config.Config 542 543 expectedPostsubmits sets.Set[string] 544 }{ 545 { 546 name: "Result of GetPostsubmits is used by default", 547 cfg: &config.Config{ 548 JobConfig: config.JobConfig{ 549 PostsubmitsStatic: map[string][]config.Postsubmit{ 550 orgRepo: {{ 551 JobBase: config.JobBase{Name: "my-static-postsubmit"}, 552 }}, 553 }, 554 ProwYAMLGetterWithDefaults: func(_ *config.Config, _ git.ClientFactory, _, _, _ string, _ ...string) (*config.ProwYAML, error) { 555 return &config.ProwYAML{ 556 Postsubmits: []config.Postsubmit{{ 557 JobBase: config.JobBase{Name: "my-inrepoconfig-postsubmit"}, 558 }}, 559 }, nil 560 }, 561 }, 562 ProwConfig: config.ProwConfig{ 563 InRepoConfig: config.InRepoConfig{Enabled: map[string]*bool{"*": utilpointer.Bool(true)}}, 564 }, 565 }, 566 567 expectedPostsubmits: sets.New[string]("my-inrepoconfig-postsubmit", "my-static-postsubmit"), 568 }, 569 { 570 name: "Fallback to static postsubmits", 571 cfg: &config.Config{ 572 JobConfig: config.JobConfig{ 573 PostsubmitsStatic: map[string][]config.Postsubmit{ 574 orgRepo: {{ 575 JobBase: config.JobBase{Name: "my-static-postsubmit"}, 576 }}, 577 }, 578 ProwYAMLGetterWithDefaults: func(_ *config.Config, _ git.ClientFactory, _, _, _ string, _ ...string) (*config.ProwYAML, error) { 579 return &config.ProwYAML{ 580 Postsubmits: []config.Postsubmit{{ 581 JobBase: config.JobBase{Name: "my-inrepoconfig-postsubmit"}, 582 }}, 583 }, errors.New("some error") 584 }, 585 }, 586 ProwConfig: config.ProwConfig{ 587 InRepoConfig: config.InRepoConfig{Enabled: map[string]*bool{"*": utilpointer.Bool(true)}}, 588 }, 589 }, 590 591 expectedPostsubmits: sets.New[string]("my-static-postsubmit"), 592 }, 593 } 594 595 shaGetter := func() (string, error) { 596 return "", nil 597 } 598 599 for _, tc := range testCases { 600 t.Run(tc.name, func(t *testing.T) { 601 postsubmits := getPostsubmits(logrus.NewEntry(logrus.New()), nil, tc.cfg, orgRepo, shaGetter) 602 actualPostsubmits := sets.Set[string]{} 603 for _, postsubmit := range postsubmits { 604 actualPostsubmits.Insert(postsubmit.Name) 605 } 606 607 if !tc.expectedPostsubmits.Equal(actualPostsubmits) { 608 t.Errorf("got a different set of postsubmits than expected, diff: %v", tc.expectedPostsubmits.Difference(actualPostsubmits)) 609 } 610 }) 611 } 612 } 613 614 func TestCreateWithRetry(t *testing.T) { 615 testCases := []struct { 616 name string 617 numFailedCreate int 618 expectedErrMsg string 619 }{ 620 { 621 name: "Initial success", 622 }, 623 { 624 name: "Success after retry", 625 numFailedCreate: 7, 626 }, 627 { 628 name: "Failure", 629 numFailedCreate: 8, 630 expectedErrMsg: "need retrying", 631 }, 632 } 633 634 for _, tc := range testCases { 635 tc := tc 636 t.Run(tc.name, func(t *testing.T) { 637 638 fakeProwJobClient := fake.NewSimpleClientset() 639 fakeProwJobClient.PrependReactor("*", "*", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { 640 if _, ok := action.(clienttesting.CreateActionImpl); ok && tc.numFailedCreate > 0 { 641 tc.numFailedCreate-- 642 return true, nil, errors.New("need retrying") 643 } 644 return false, nil, nil 645 }) 646 647 pj := &prowapi.ProwJob{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} 648 649 var errMsg string 650 err := createWithRetry(context.TODO(), fakeProwJobClient.ProwV1().ProwJobs(""), pj, time.Nanosecond) 651 if err != nil { 652 errMsg = err.Error() 653 } 654 if errMsg != tc.expectedErrMsg { 655 t.Fatalf("expected error %s, got error %v", tc.expectedErrMsg, err) 656 } 657 if err != nil { 658 return 659 } 660 661 result, err := fakeProwJobClient.ProwV1().ProwJobs("").List(context.Background(), metav1.ListOptions{}) 662 if err != nil { 663 t.Fatalf("faile to list prowjobs: %v", err) 664 } 665 666 if len(result.Items) != 1 { 667 t.Errorf("expected to find exactly one prowjob, got %+v", result.Items) 668 } 669 }) 670 } 671 }