github.com/abayer/test-infra@v0.0.5/prow/plugins/override/override_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 override
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"reflect"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/sirupsen/logrus"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  
    29  	"k8s.io/test-infra/prow/config"
    30  	"k8s.io/test-infra/prow/github"
    31  	"k8s.io/test-infra/prow/kube"
    32  )
    33  
    34  const (
    35  	fakeOrg     = "fake-org"
    36  	fakeRepo    = "fake-repo"
    37  	fakePR      = 33
    38  	fakeSHA     = "deadbeef"
    39  	fakeBaseSHA = "fffffff"
    40  	adminUser   = "admin-user"
    41  )
    42  
    43  type fakeClient struct {
    44  	comments   []string
    45  	statuses   map[string]github.Status
    46  	presubmits map[string]config.Presubmit
    47  	jobs       sets.String
    48  }
    49  
    50  func (c *fakeClient) CreateComment(org, repo string, number int, comment string) error {
    51  	switch {
    52  	case org != fakeOrg:
    53  		return fmt.Errorf("bad org: %s", org)
    54  	case repo != fakeRepo:
    55  		return fmt.Errorf("bad repo: %s", repo)
    56  	case number != fakePR:
    57  		return fmt.Errorf("bad number: %d", number)
    58  	case strings.Contains(comment, "fail-comment"):
    59  		return errors.New("injected CreateComment failure")
    60  	}
    61  	c.comments = append(c.comments, comment)
    62  	return nil
    63  }
    64  
    65  func (c *fakeClient) CreateStatus(org, repo, ref string, s github.Status) error {
    66  	switch {
    67  	case s.Context == "fail-create":
    68  		return errors.New("injected CreateStatus failure")
    69  	case org != fakeOrg:
    70  		return fmt.Errorf("bad org: %s", org)
    71  	case repo != fakeRepo:
    72  		return fmt.Errorf("bad repo: %s", repo)
    73  	case ref != fakeSHA:
    74  		return fmt.Errorf("bad ref: %s", ref)
    75  	}
    76  	c.statuses[s.Context] = s
    77  	return nil
    78  }
    79  
    80  func (c *fakeClient) GetPullRequest(org, repo string, number int) (*github.PullRequest, error) {
    81  	switch {
    82  	case number < 0:
    83  		return nil, errors.New("injected CreateStatus failure")
    84  	case org != fakeOrg:
    85  		return nil, fmt.Errorf("bad org: %s", org)
    86  	case repo != fakeRepo:
    87  		return nil, fmt.Errorf("bad repo: %s", repo)
    88  	case number != fakePR:
    89  		return nil, fmt.Errorf("bad number: %d", number)
    90  	}
    91  	var pr github.PullRequest
    92  	pr.Head.SHA = fakeSHA
    93  	return &pr, nil
    94  }
    95  
    96  func (c *fakeClient) ListStatuses(org, repo, ref string) ([]github.Status, error) {
    97  	switch {
    98  	case org != fakeOrg:
    99  		return nil, fmt.Errorf("bad org: %s", org)
   100  	case repo != fakeRepo:
   101  		return nil, fmt.Errorf("bad repo: %s", repo)
   102  	case ref != fakeSHA:
   103  		return nil, fmt.Errorf("bad ref: %s", ref)
   104  	}
   105  	var out []github.Status
   106  	for _, s := range c.statuses {
   107  		if s.Context == "fail-list" {
   108  			return nil, errors.New("injected ListStatuses failure")
   109  		}
   110  		out = append(out, s)
   111  	}
   112  	return out, nil
   113  }
   114  
   115  func (c *fakeClient) HasPermission(org, repo, user string, roles ...string) (bool, error) {
   116  	switch {
   117  	case org != fakeOrg:
   118  		return false, fmt.Errorf("bad org: %s", org)
   119  	case repo != fakeRepo:
   120  		return false, fmt.Errorf("bad repo: %s", repo)
   121  	case roles[0] != github.RoleAdmin:
   122  		return false, fmt.Errorf("bad roles: %s", roles)
   123  	case user == "fail":
   124  		return true, errors.New("injected HasRole error")
   125  	}
   126  	return user == adminUser, nil
   127  }
   128  
   129  func (c *fakeClient) GetRef(org, repo, ref string) (string, error) {
   130  	if repo == "fail-ref" {
   131  		return "", errors.New("injected GetRef error")
   132  	}
   133  	return fakeBaseSHA, nil
   134  }
   135  
   136  func (c *fakeClient) CreateProwJob(pj kube.ProwJob) (kube.ProwJob, error) {
   137  	if s := pj.Status.State; s != kube.SuccessState {
   138  		return pj, fmt.Errorf("bad status state: %s", s)
   139  	}
   140  	if pj.Spec.Context == "fail-create" {
   141  		return pj, errors.New("injected CreateProwJob error")
   142  	}
   143  	c.jobs.Insert(pj.Spec.Context)
   144  	return pj, nil
   145  }
   146  
   147  func (c *fakeClient) presubmitForContext(org, repo, context string) *config.Presubmit {
   148  	if p, ok := c.presubmits[context]; !ok {
   149  		return nil
   150  	} else {
   151  		return &p
   152  	}
   153  }
   154  
   155  func TestAuthorized(t *testing.T) {
   156  	cases := []struct {
   157  		name     string
   158  		user     string
   159  		expected bool
   160  	}{
   161  		{
   162  			name: "fail closed",
   163  			user: "fail",
   164  		},
   165  		{
   166  			name: "reject rando",
   167  			user: "random",
   168  		},
   169  		{
   170  			name:     "accept admin",
   171  			user:     adminUser,
   172  			expected: true,
   173  		},
   174  	}
   175  
   176  	log := logrus.WithField("plugin", pluginName)
   177  	for _, tc := range cases {
   178  		t.Run(tc.name, func(t *testing.T) {
   179  			if actual := authorized(&fakeClient{}, log, fakeOrg, fakeRepo, tc.user); actual != tc.expected {
   180  				t.Errorf("actual %t != expected %t", actual, tc.expected)
   181  			}
   182  		})
   183  	}
   184  }
   185  
   186  func TestHandle(t *testing.T) {
   187  	cases := []struct {
   188  		name          string
   189  		action        github.GenericCommentEventAction
   190  		issue         bool
   191  		state         string
   192  		comment       string
   193  		contexts      map[string]github.Status
   194  		presubmits    map[string]config.Presubmit
   195  		user          string
   196  		number        int
   197  		expected      map[string]github.Status
   198  		jobs          sets.String
   199  		checkComments []string
   200  		err           bool
   201  	}{
   202  		{
   203  			name:    "successfully override failure",
   204  			comment: "/override broken-test",
   205  			contexts: map[string]github.Status{
   206  				"broken-test": {
   207  					Context: "broken-test",
   208  					State:   github.StatusFailure,
   209  				},
   210  			},
   211  			expected: map[string]github.Status{
   212  				"broken-test": {
   213  					Context:     "broken-test",
   214  					Description: description(adminUser),
   215  					State:       github.StatusSuccess,
   216  				},
   217  			},
   218  			checkComments: []string{"on behalf of " + adminUser},
   219  		},
   220  		{
   221  			name:    "successfully override pending",
   222  			comment: "/override hung-test",
   223  			contexts: map[string]github.Status{
   224  				"hung-test": {
   225  					Context: "hung-test",
   226  					State:   github.StatusPending,
   227  				},
   228  			},
   229  			expected: map[string]github.Status{
   230  				"hung-test": {
   231  					Context:     "hung-test",
   232  					Description: description(adminUser),
   233  					State:       github.StatusSuccess,
   234  				},
   235  			},
   236  		},
   237  		{
   238  			name:    "refuse override from non-admin",
   239  			comment: "/override broken-test",
   240  			contexts: map[string]github.Status{
   241  				"broken-test": {
   242  					Context: "broken-test",
   243  					State:   github.StatusPending,
   244  				},
   245  			},
   246  			user:          "rando",
   247  			checkComments: []string{"unauthorized"},
   248  			expected: map[string]github.Status{
   249  				"broken-test": {
   250  					Context: "broken-test",
   251  					State:   github.StatusPending,
   252  				},
   253  			},
   254  		},
   255  		{
   256  			name:    "override multiple",
   257  			comment: "/override broken-test\n/override hung-test",
   258  			contexts: map[string]github.Status{
   259  				"broken-test": {
   260  					Context: "broken-test",
   261  					State:   github.StatusFailure,
   262  				},
   263  				"hung-test": {
   264  					Context: "hung-test",
   265  					State:   github.StatusPending,
   266  				},
   267  			},
   268  			expected: map[string]github.Status{
   269  				"hung-test": {
   270  					Context:     "hung-test",
   271  					Description: description(adminUser),
   272  					State:       github.StatusSuccess,
   273  				},
   274  				"broken-test": {
   275  					Context:     "broken-test",
   276  					Description: description(adminUser),
   277  					State:       github.StatusSuccess,
   278  				},
   279  			},
   280  			checkComments: []string{fmt.Sprintf("%s: broken-test, hung-test", adminUser)},
   281  		},
   282  		{
   283  			name:    "ignore non-PRs",
   284  			issue:   true,
   285  			comment: "/override broken-test",
   286  			contexts: map[string]github.Status{
   287  				"broken-test": {
   288  					Context: "broken-test",
   289  					State:   github.StatusPending,
   290  				},
   291  			},
   292  			expected: map[string]github.Status{
   293  				"broken-test": {
   294  					Context: "broken-test",
   295  					State:   github.StatusPending,
   296  				},
   297  			},
   298  		},
   299  		{
   300  			name:    "ignore closed issues",
   301  			state:   "closed",
   302  			comment: "/override broken-test",
   303  			contexts: map[string]github.Status{
   304  				"broken-test": {
   305  					Context: "broken-test",
   306  					State:   github.StatusPending,
   307  				},
   308  			},
   309  			expected: map[string]github.Status{
   310  				"broken-test": {
   311  					Context: "broken-test",
   312  					State:   github.StatusPending,
   313  				},
   314  			},
   315  		},
   316  		{
   317  			name:    "ignore edits",
   318  			action:  github.GenericCommentActionEdited,
   319  			comment: "/override broken-test",
   320  			contexts: map[string]github.Status{
   321  				"broken-test": {
   322  					Context: "broken-test",
   323  					State:   github.StatusPending,
   324  				},
   325  			},
   326  			expected: map[string]github.Status{
   327  				"broken-test": {
   328  					Context: "broken-test",
   329  					State:   github.StatusPending,
   330  				},
   331  			},
   332  		},
   333  		{
   334  			name:    "ignore random text",
   335  			comment: "/test broken-test",
   336  			contexts: map[string]github.Status{
   337  				"broken-test": {
   338  					Context: "broken-test",
   339  					State:   github.StatusPending,
   340  				},
   341  			},
   342  			expected: map[string]github.Status{
   343  				"broken-test": {
   344  					Context: "broken-test",
   345  					State:   github.StatusPending,
   346  				},
   347  			},
   348  		},
   349  		{
   350  			name:    "comment on get pr failure",
   351  			number:  fakePR * 2,
   352  			comment: "/override broken-test",
   353  			contexts: map[string]github.Status{
   354  				"broken-test": {
   355  					Context: "broken-test",
   356  					State:   github.StatusFailure,
   357  				},
   358  			},
   359  			expected: map[string]github.Status{
   360  				"broken-test": {
   361  					Context:     "broken-test",
   362  					Description: description(adminUser),
   363  					State:       github.StatusSuccess,
   364  				},
   365  			},
   366  			checkComments: []string{"Cannot get PR"},
   367  		},
   368  		{
   369  			name:    "comment on list statuses failure",
   370  			comment: "/override fail-list",
   371  			contexts: map[string]github.Status{
   372  				"fail-list": {
   373  					Context: "fail-list",
   374  					State:   github.StatusFailure,
   375  				},
   376  			},
   377  			expected: map[string]github.Status{
   378  				"fail-list": {
   379  					Context: "fail-list",
   380  					State:   github.StatusFailure,
   381  				},
   382  			},
   383  			checkComments: []string{"Cannot get commit statuses"},
   384  		},
   385  		{
   386  			name:    "do not override passing contexts",
   387  			comment: "/override passing-test",
   388  			contexts: map[string]github.Status{
   389  				"passing-test": {
   390  					Context:     "passing-test",
   391  					Description: "preserve description",
   392  					State:       github.StatusSuccess,
   393  				},
   394  			},
   395  			expected: map[string]github.Status{
   396  				"passing-test": {
   397  					Context:     "passing-test",
   398  					State:       github.StatusSuccess,
   399  					Description: "preserve description",
   400  				},
   401  			},
   402  		},
   403  		{
   404  			name:    "create successful prow job",
   405  			comment: "/override prow-job",
   406  			contexts: map[string]github.Status{
   407  				"prow-job": {
   408  					Context:     "prow-job",
   409  					Description: "failed",
   410  					State:       github.StatusFailure,
   411  				},
   412  			},
   413  			presubmits: map[string]config.Presubmit{
   414  				"prow-job": {
   415  					Context: "prow-job",
   416  				},
   417  			},
   418  			jobs: sets.NewString("prow-job"),
   419  			expected: map[string]github.Status{
   420  				"prow-job": {
   421  					Context:     "prow-job",
   422  					State:       github.StatusSuccess,
   423  					Description: description(adminUser),
   424  				},
   425  			},
   426  		},
   427  		{
   428  			name:    "override with explanation works",
   429  			comment: "/override job\r\nobnoxious flake", // github ends lines with \r\n
   430  			contexts: map[string]github.Status{
   431  				"job": {
   432  					Context:     "job",
   433  					Description: "failed",
   434  					State:       github.StatusFailure,
   435  				},
   436  			},
   437  			expected: map[string]github.Status{
   438  				"job": {
   439  					Context:     "job",
   440  					Description: description(adminUser),
   441  					State:       github.StatusSuccess,
   442  				},
   443  			},
   444  		},
   445  	}
   446  
   447  	log := logrus.WithField("plugin", pluginName)
   448  	for _, tc := range cases {
   449  		t.Run(tc.name, func(t *testing.T) {
   450  			var event github.GenericCommentEvent
   451  			event.Repo.Owner.Login = fakeOrg
   452  			event.Repo.Name = fakeRepo
   453  			event.Body = tc.comment
   454  			event.Number = fakePR
   455  			event.IsPR = !tc.issue
   456  			if tc.user == "" {
   457  				tc.user = adminUser
   458  			}
   459  			event.User.Login = tc.user
   460  			if tc.state == "" {
   461  				tc.state = "open"
   462  			}
   463  			event.IssueState = tc.state
   464  			if tc.action == "" {
   465  				tc.action = github.GenericCommentActionCreated
   466  			}
   467  			event.Action = tc.action
   468  			if tc.contexts == nil {
   469  				tc.contexts = map[string]github.Status{}
   470  			}
   471  			fc := fakeClient{
   472  				statuses:   tc.contexts,
   473  				presubmits: tc.presubmits,
   474  				jobs:       sets.String{},
   475  			}
   476  
   477  			if tc.jobs == nil {
   478  				tc.jobs = sets.String{}
   479  			}
   480  
   481  			err := handle(&fc, log, &event)
   482  			switch {
   483  			case err != nil:
   484  				if !tc.err {
   485  					t.Errorf("unexpected error: %v", err)
   486  				}
   487  			case tc.err:
   488  				t.Error("failed to receive an error")
   489  			case !reflect.DeepEqual(fc.statuses, tc.expected):
   490  				t.Errorf("bad statuses: actual %#v != expected %#v", fc.statuses, tc.expected)
   491  			case !reflect.DeepEqual(fc.jobs, tc.jobs):
   492  				t.Errorf("bad jobs: actual %#v != expected %#v", fc.jobs, tc.jobs)
   493  			}
   494  		})
   495  	}
   496  }