sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/trigger/pull-request_test.go (about)

     1  /*
     2  Copyright 2016 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  	"encoding/json"
    22  	"strconv"
    23  	"testing"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/sirupsen/logrus"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	clienttesting "k8s.io/client-go/testing"
    29  
    30  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    31  	"sigs.k8s.io/prow/pkg/client/clientset/versioned/fake"
    32  	"sigs.k8s.io/prow/pkg/config"
    33  	"sigs.k8s.io/prow/pkg/github"
    34  	"sigs.k8s.io/prow/pkg/github/fakegithub"
    35  	"sigs.k8s.io/prow/pkg/kube"
    36  	"sigs.k8s.io/prow/pkg/labels"
    37  	"sigs.k8s.io/prow/pkg/plugins"
    38  )
    39  
    40  func TestTrusted(t *testing.T) {
    41  	const rando = "random-person"
    42  	const member = "org-member"
    43  	const sister = "trusted-org-member"
    44  	const friend = "repo-collaborator"
    45  
    46  	var testcases = []struct {
    47  		name     string
    48  		author   string
    49  		labels   []string
    50  		onlyOrg  bool
    51  		expected bool
    52  	}{
    53  		{
    54  			name:     "trust org member",
    55  			author:   member,
    56  			labels:   []string{},
    57  			expected: true,
    58  		},
    59  		{
    60  			name:     "trust member of other trusted org",
    61  			author:   sister,
    62  			labels:   []string{},
    63  			expected: true,
    64  		},
    65  		{
    66  			name:     "accept random PR with ok-to-test",
    67  			author:   rando,
    68  			labels:   []string{labels.OkToTest},
    69  			expected: true,
    70  		},
    71  		{
    72  			name:     "accept random PR with both labels",
    73  			author:   rando,
    74  			labels:   []string{labels.OkToTest, labels.NeedsOkToTest},
    75  			expected: true,
    76  		},
    77  		{
    78  			name:     "reject random PR with needs-ok-to-test",
    79  			author:   rando,
    80  			labels:   []string{labels.NeedsOkToTest},
    81  			expected: false,
    82  		},
    83  		{
    84  			name:     "reject random PR with no label",
    85  			author:   rando,
    86  			labels:   []string{},
    87  			expected: false,
    88  		},
    89  	}
    90  	for _, tc := range testcases {
    91  		t.Run(tc.name, func(t *testing.T) {
    92  			g := fakegithub.NewFakeClient()
    93  			g.OrgMembers = map[string][]string{"kubernetes": {sister}, "kubernetes-sigs": {member, fakegithub.Bot}}
    94  			g.Collaborators = []string{friend}
    95  			g.IssueComments = map[int][]github.IssueComment{}
    96  			trigger := plugins.Trigger{
    97  				TrustedOrg:     "kubernetes",
    98  				OnlyOrgMembers: tc.onlyOrg,
    99  			}
   100  			var labels []github.Label
   101  			for _, label := range tc.labels {
   102  				labels = append(labels, github.Label{
   103  					Name: label,
   104  				})
   105  			}
   106  			_, actual, err := TrustedPullRequest(g, trigger, tc.author, "kubernetes-sigs", "random-repo", 1, labels)
   107  			if err != nil {
   108  				t.Fatalf("Didn't expect error: %s", err)
   109  			}
   110  			if actual != tc.expected {
   111  				t.Errorf("actual result %t != expected %t", actual, tc.expected)
   112  			}
   113  		})
   114  	}
   115  }
   116  
   117  func TestHandlePullRequest(t *testing.T) {
   118  	jobToAbort := &prowapi.ProwJob{
   119  		ObjectMeta: metav1.ObjectMeta{
   120  			Name:      "job-to-abort",
   121  			Namespace: "namespace",
   122  			Labels: map[string]string{
   123  				kube.OrgLabel:         "org",
   124  				kube.RepoLabel:        "repo",
   125  				kube.PullLabel:        "0",
   126  				kube.ProwJobTypeLabel: string(prowapi.PresubmitJob),
   127  			},
   128  		},
   129  	}
   130  
   131  	var testcases = []struct {
   132  		name string
   133  
   134  		Author           string
   135  		ShouldBuild      bool
   136  		ShouldComment    bool
   137  		HasOkToTest      bool
   138  		prLabel          string
   139  		prChanges        bool
   140  		prAction         github.PullRequestEventAction
   141  		prIsDraft        bool
   142  		eventSender      string
   143  		jobToAbort       *prowapi.ProwJob
   144  		issueLabelsAdded []string
   145  	}{
   146  		{
   147  			name: "Trusted user open PR should build",
   148  
   149  			Author:      "t",
   150  			ShouldBuild: true,
   151  			prAction:    github.PullRequestActionOpened,
   152  		},
   153  		{
   154  			name: "Trusted user open draft PR should not build and should comment",
   155  
   156  			Author:        "t",
   157  			ShouldBuild:   false,
   158  			prAction:      github.PullRequestActionOpened,
   159  			prIsDraft:     true,
   160  			ShouldComment: true,
   161  		},
   162  		{
   163  			name: "Untrusted user open PR with ok-to-test should not add needs-ok-to-test",
   164  
   165  			Author:        "u",
   166  			ShouldComment: true,
   167  			prAction:      github.PullRequestActionOpened,
   168  			HasOkToTest:   true,
   169  		},
   170  		{
   171  			name: "Untrusted user open PR without ok-to-test should add needs-ok-to-test",
   172  
   173  			Author:           "u",
   174  			ShouldComment:    true,
   175  			prAction:         github.PullRequestActionOpened,
   176  			issueLabelsAdded: []string{"org/repo#0:needs-ok-to-test"},
   177  		},
   178  		{
   179  			name: "Untrusted user open PR should not build and should comment",
   180  
   181  			Author:           "u",
   182  			ShouldBuild:      false,
   183  			ShouldComment:    true,
   184  			prAction:         github.PullRequestActionOpened,
   185  			issueLabelsAdded: []string{"org/repo#0:needs-ok-to-test"},
   186  		},
   187  		{
   188  			name: "Untrusted user open draft PR should not build and should comment",
   189  
   190  			Author:           "u",
   191  			ShouldBuild:      false,
   192  			ShouldComment:    true,
   193  			prAction:         github.PullRequestActionOpened,
   194  			prIsDraft:        true,
   195  			issueLabelsAdded: []string{"org/repo#0:needs-ok-to-test"},
   196  		},
   197  		{
   198  			name: "Trusted user reopen PR should build",
   199  
   200  			Author:      "t",
   201  			ShouldBuild: true,
   202  			prAction:    github.PullRequestActionReopened,
   203  		},
   204  		{
   205  			name: "Trusted user reopen draft PR should not build",
   206  
   207  			Author:      "t",
   208  			ShouldBuild: false,
   209  			prAction:    github.PullRequestActionReopened,
   210  			prIsDraft:   true,
   211  		},
   212  		{
   213  			name: "Trusted user switch PR from draft to normal shoud build",
   214  
   215  			Author:      "t",
   216  			ShouldBuild: true,
   217  			prAction:    github.PullRequestActionReadyForReview,
   218  		},
   219  		{
   220  			name: "Untrusted user switch PR from draft to normal should not build",
   221  
   222  			Author:      "u",
   223  			ShouldBuild: false,
   224  			prAction:    github.PullRequestActionReadyForReview,
   225  		},
   226  		{
   227  			name: "Untrusted user switch PR from draft to normal with ok-to-test should build",
   228  
   229  			Author:      "u",
   230  			HasOkToTest: true,
   231  			ShouldBuild: true,
   232  			prAction:    github.PullRequestActionReadyForReview,
   233  		},
   234  		{
   235  			name: "Untrusted user reopen PR with ok-to-test should build",
   236  
   237  			Author:      "u",
   238  			ShouldBuild: true,
   239  			HasOkToTest: true,
   240  			prAction:    github.PullRequestActionReopened,
   241  		},
   242  		{
   243  			name: "Untrusted user reopen PR without ok-to-test should not build",
   244  
   245  			Author:      "u",
   246  			ShouldBuild: false,
   247  			prAction:    github.PullRequestActionReopened,
   248  		},
   249  		{
   250  			name: "Untrusted user reopen draft PR should not build",
   251  
   252  			Author:      "u",
   253  			ShouldBuild: false,
   254  			prAction:    github.PullRequestActionReopened,
   255  			prIsDraft:   true,
   256  		},
   257  		{
   258  			name: "Trusted user edit PR with changes should build",
   259  
   260  			Author:      "t",
   261  			ShouldBuild: true,
   262  			prChanges:   true,
   263  			prAction:    github.PullRequestActionEdited,
   264  		},
   265  		{
   266  			name: "Trusted user edit draft PR with changes should not build",
   267  
   268  			Author:      "t",
   269  			ShouldBuild: false,
   270  			prChanges:   true,
   271  			prAction:    github.PullRequestActionEdited,
   272  			prIsDraft:   true,
   273  		},
   274  		{
   275  			name: "Trusted user edit PR without changes should not build",
   276  
   277  			Author:      "t",
   278  			ShouldBuild: false,
   279  			prAction:    github.PullRequestActionEdited,
   280  		},
   281  		{
   282  			name: "Untrusted user edit PR without changes and without ok-to-test should not build",
   283  
   284  			Author:      "u",
   285  			ShouldBuild: false,
   286  			prAction:    github.PullRequestActionEdited,
   287  		},
   288  		{
   289  			name: "Untrusted user edit PR with changes and without ok-to-test should not build",
   290  
   291  			Author:      "u",
   292  			ShouldBuild: false,
   293  			prChanges:   true,
   294  			prAction:    github.PullRequestActionEdited,
   295  		},
   296  		{
   297  			name: "Untrusted user edit PR without changes and with ok-to-test should not build",
   298  
   299  			Author:      "u",
   300  			ShouldBuild: false,
   301  			HasOkToTest: true,
   302  			prAction:    github.PullRequestActionEdited,
   303  		},
   304  		{
   305  			name: "Untrusted user edit PR with changes and with ok-to-test should build",
   306  
   307  			Author:      "u",
   308  			ShouldBuild: true,
   309  			HasOkToTest: true,
   310  			prChanges:   true,
   311  			prAction:    github.PullRequestActionEdited,
   312  		},
   313  		{
   314  			name: "Trusted user sync PR should build",
   315  
   316  			Author:      "t",
   317  			ShouldBuild: true,
   318  			prAction:    github.PullRequestActionSynchronize,
   319  		},
   320  		{
   321  			name: "Untrusted user sync PR without ok-to-test should not build",
   322  
   323  			Author:      "u",
   324  			ShouldBuild: false,
   325  			prAction:    github.PullRequestActionSynchronize,
   326  		},
   327  		{
   328  			name: "Untrusted user sync PR with ok-to-test should build",
   329  
   330  			Author:      "u",
   331  			ShouldBuild: true,
   332  			HasOkToTest: true,
   333  			prAction:    github.PullRequestActionSynchronize,
   334  		},
   335  		{
   336  			name: "Trusted user labeled PR with lgtm should not build",
   337  
   338  			Author:      "t",
   339  			ShouldBuild: false,
   340  			prAction:    github.PullRequestActionLabeled,
   341  			prLabel:     labels.LGTM,
   342  		},
   343  		{
   344  			name: "Untrusted user labeled PR with lgtm should build",
   345  
   346  			Author:      "u",
   347  			ShouldBuild: true,
   348  			prAction:    github.PullRequestActionLabeled,
   349  			prLabel:     labels.LGTM,
   350  		},
   351  		{
   352  			name: "Untrusted user labeled PR without lgtm should not build",
   353  
   354  			Author:      "u",
   355  			ShouldBuild: false,
   356  			prAction:    github.PullRequestActionLabeled,
   357  			prLabel:     "test",
   358  		},
   359  		{
   360  			name: "Trusted user closed PR should not build",
   361  
   362  			Author:      "t",
   363  			ShouldBuild: false,
   364  			prAction:    github.PullRequestActionClosed,
   365  		},
   366  		{
   367  			name: "Trusted user labeled PR with ok-to-test should build",
   368  
   369  			Author:      "t",
   370  			ShouldBuild: true,
   371  			eventSender: "not-k8s-ci-robot",
   372  			prAction:    github.PullRequestActionLabeled,
   373  			prLabel:     labels.OkToTest,
   374  		},
   375  		{
   376  			name: "Untrusted user labeled PR with ok-to-test should build",
   377  
   378  			Author:      "u",
   379  			ShouldBuild: true,
   380  			eventSender: "not-k8s-ci-robot",
   381  			prAction:    github.PullRequestActionLabeled,
   382  			prLabel:     labels.OkToTest,
   383  		},
   384  		{
   385  			name: "Label added by a bot. Build should not be triggered in this case.",
   386  
   387  			Author:      "u",
   388  			eventSender: "k8s-ci-robot",
   389  			prLabel:     labels.OkToTest,
   390  			prAction:    github.PullRequestActionLabeled,
   391  			ShouldBuild: false,
   392  		},
   393  		{
   394  			name: "Abort jobs if PR is closed",
   395  
   396  			Author:      "t",
   397  			HasOkToTest: true,
   398  			prAction:    github.PullRequestActionClosed,
   399  			ShouldBuild: false,
   400  			jobToAbort:  jobToAbort,
   401  		},
   402  		{
   403  			name: "Abort jobs if PR is changed to draft",
   404  
   405  			Author:      "t",
   406  			HasOkToTest: true,
   407  			prAction:    github.PullRequestActionConvertedToDraft,
   408  			ShouldBuild: false,
   409  			jobToAbort:  jobToAbort,
   410  		},
   411  		{
   412  			name: "Abort old jobs and build on push",
   413  
   414  			Author:      "t",
   415  			HasOkToTest: true,
   416  			prAction:    github.PullRequestActionSynchronize,
   417  			ShouldBuild: true,
   418  			jobToAbort:  jobToAbort,
   419  		},
   420  	}
   421  	for _, tc := range testcases {
   422  		t.Logf("running scenario %q", tc.name)
   423  		t.Run(tc.name, func(t *testing.T) {
   424  			g := fakegithub.NewFakeClient()
   425  			g.IssueComments = map[int][]github.IssueComment{}
   426  			g.OrgMembers = map[string][]string{"org": {"t"}}
   427  			g.PullRequests = map[int]*github.PullRequest{
   428  				0: {
   429  					Number: 0,
   430  					User:   github.User{Login: tc.Author},
   431  					Base: github.PullRequestBranch{
   432  						Ref: "master",
   433  						Repo: github.Repo{
   434  							Owner: github.User{Login: "org"},
   435  							Name:  "repo",
   436  						},
   437  					},
   438  					Draft: tc.prIsDraft,
   439  				},
   440  			}
   441  			fakeProwJobClient := fake.NewSimpleClientset(jobToAbort)
   442  			c := Client{
   443  				GitHubClient:  g,
   444  				ProwJobClient: fakeProwJobClient.ProwV1().ProwJobs("namespace"),
   445  				Config:        &config.Config{},
   446  				Logger:        logrus.WithField("plugin", PluginName),
   447  				GitClient:     nil,
   448  			}
   449  
   450  			presubmits := map[string][]config.Presubmit{
   451  				"org/repo": {
   452  					{
   453  						JobBase: config.JobBase{
   454  							Name: "jib",
   455  						},
   456  						AlwaysRun: true,
   457  					},
   458  				},
   459  			}
   460  			if err := c.Config.SetPresubmits(presubmits); err != nil {
   461  				t.Fatalf("failed to set presubmits: %v", err)
   462  			}
   463  
   464  			if tc.HasOkToTest {
   465  				g.IssueLabelsExisting = append(g.IssueLabelsExisting, issueLabels(labels.OkToTest)...)
   466  			}
   467  			if tc.eventSender == "" {
   468  				tc.eventSender = tc.Author
   469  			}
   470  			pr := github.PullRequestEvent{
   471  				Action: tc.prAction,
   472  				Label:  github.Label{Name: tc.prLabel},
   473  				PullRequest: github.PullRequest{
   474  					Number: 0,
   475  					User:   github.User{Login: tc.Author},
   476  					Base: github.PullRequestBranch{
   477  						Ref: "master",
   478  						Repo: github.Repo{
   479  							Owner:    github.User{Login: "org"},
   480  							Name:     "repo",
   481  							FullName: "org/repo",
   482  						},
   483  					},
   484  					Draft: tc.prIsDraft,
   485  				},
   486  				Sender: github.User{
   487  					Login: tc.eventSender,
   488  				},
   489  			}
   490  			if tc.prChanges {
   491  				data := []byte(`{"base":{"ref":{"from":"REF"}, "sha":{"from":"SHA"}}}`)
   492  				pr.Changes = (json.RawMessage)(data)
   493  			}
   494  			trigger := plugins.Trigger{
   495  				TrustedOrg:     "org",
   496  				OnlyOrgMembers: true,
   497  			}
   498  			trigger.SetDefaults()
   499  			if err := handlePR(c, trigger, pr); err != nil {
   500  				t.Fatalf("Didn't expect error: %s", err)
   501  			}
   502  			var numStarted int
   503  			for _, action := range fakeProwJobClient.Actions() {
   504  				switch action.(type) {
   505  				case clienttesting.CreateActionImpl:
   506  					numStarted++
   507  				}
   508  			}
   509  			if numStarted > 0 && !tc.ShouldBuild {
   510  				t.Errorf("Built but should not have: %+v", tc)
   511  			} else if numStarted == 0 && tc.ShouldBuild {
   512  				t.Errorf("Not built but should have: %+v", tc)
   513  			}
   514  			if tc.ShouldComment && len(g.IssueCommentsAdded) == 0 {
   515  				t.Error("Expected comment to github")
   516  			} else if !tc.ShouldComment && len(g.IssueCommentsAdded) > 0 {
   517  				t.Errorf("Expected no comments to github, but got %d", len(g.IssueCommentsAdded))
   518  			}
   519  			if tc.jobToAbort != nil {
   520  				pj, err := fakeProwJobClient.ProwV1().ProwJobs("namespace").Get(context.Background(), tc.jobToAbort.Name, metav1.GetOptions{})
   521  				if err != nil {
   522  					t.Fatalf("failed to get prowjob: %v", err)
   523  				}
   524  
   525  				if pj.Status.State != prowapi.AbortedState {
   526  					t.Errorf("exptected job %s to be aborted, found state: %v", tc.jobToAbort.Name, pj.Status.State)
   527  				}
   528  				if pj.Complete() {
   529  					t.Errorf("exptected job %s to not be set to complete.", tc.jobToAbort.Name)
   530  				}
   531  			}
   532  			if cmp.Diff(tc.issueLabelsAdded, g.IssueLabelsAdded) != "" {
   533  				t.Errorf("exptected added issue labels %v to match %v", tc.issueLabelsAdded, g.IssueLabelsAdded)
   534  			}
   535  		})
   536  	}
   537  }
   538  
   539  func TestAbortAllJobs(t *testing.T) {
   540  	t.Parallel()
   541  	const org, repo, number = "org", "repo", 1
   542  	pj := func(modifier ...func(*prowapi.ProwJob)) *prowapi.ProwJob {
   543  		job := &prowapi.ProwJob{
   544  			ObjectMeta: metav1.ObjectMeta{
   545  				Name: "my-pj",
   546  				Labels: map[string]string{
   547  					kube.OrgLabel:         org,
   548  					kube.RepoLabel:        repo,
   549  					kube.PullLabel:        strconv.Itoa(number),
   550  					kube.ProwJobTypeLabel: string(prowapi.PresubmitJob),
   551  				},
   552  			},
   553  		}
   554  
   555  		for _, m := range modifier {
   556  			m(job)
   557  		}
   558  
   559  		return job
   560  	}
   561  	testCases := []struct {
   562  		name                   string
   563  		pj                     *prowapi.ProwJob
   564  		expectedAbortedProwJob bool
   565  	}{
   566  		{
   567  			name:                   "Job gets aborted",
   568  			pj:                     pj(),
   569  			expectedAbortedProwJob: true,
   570  		},
   571  		{
   572  			name:                   "Wrong org, ignored",
   573  			pj:                     pj(func(pj *prowapi.ProwJob) { pj.Labels[kube.OrgLabel] = "wrong" }),
   574  			expectedAbortedProwJob: false,
   575  		},
   576  		{
   577  			name:                   "Wrong repo, ignored",
   578  			pj:                     pj(func(pj *prowapi.ProwJob) { pj.Labels[kube.RepoLabel] = "wrong" }),
   579  			expectedAbortedProwJob: false,
   580  		},
   581  		{
   582  			name:                   "Wrong pr, ignored",
   583  			pj:                     pj(func(pj *prowapi.ProwJob) { pj.Labels[kube.PullLabel] = "99" }),
   584  			expectedAbortedProwJob: false,
   585  		},
   586  		{
   587  			name:                   "Wrong type, ignored",
   588  			pj:                     pj(func(pj *prowapi.ProwJob) { pj.Labels[kube.ProwJobTypeLabel] = "wrong" }),
   589  			expectedAbortedProwJob: false,
   590  		},
   591  		{
   592  			name: "Job completed, ignored",
   593  			pj: pj(func(pj *prowapi.ProwJob) {
   594  				pj.Status.CompletionTime = &[]metav1.Time{metav1.Now()}[0]
   595  			}),
   596  			expectedAbortedProwJob: false,
   597  		},
   598  	}
   599  
   600  	for _, tc := range testCases {
   601  		t.Run(tc.name, func(t *testing.T) {
   602  			pjClient := fake.NewSimpleClientset(tc.pj)
   603  			client := Client{
   604  				ProwJobClient: pjClient.ProwV1().ProwJobs(""),
   605  				Logger:        logrus.NewEntry(logrus.New()),
   606  			}
   607  			pr := &github.PullRequest{
   608  				Base: github.PullRequestBranch{
   609  					Repo: github.Repo{
   610  						Owner: github.User{
   611  							Login: org,
   612  						},
   613  						Name: repo,
   614  					},
   615  				},
   616  				Number: number,
   617  			}
   618  
   619  			if err := abortAllJobs(client, pr); err != nil {
   620  				t.Fatalf("error caling abortAllJobs: %v", err)
   621  			}
   622  
   623  			pj, err := pjClient.ProwV1().ProwJobs("").Get(context.Background(), pj().Name, metav1.GetOptions{})
   624  			if err != nil {
   625  				t.Fatalf("failed to get prowjob: %v", err)
   626  			}
   627  
   628  			if isAborted := (pj.Status.State == prowapi.AbortedState && pj.Status.Description == abortedDescription); isAborted != tc.expectedAbortedProwJob {
   629  				t.Errorf("IsAborted: %t, but expected aborted: %t", isAborted, tc.expectedAbortedProwJob)
   630  			}
   631  		})
   632  	}
   633  }