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  }