github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/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  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"reflect"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  	"github.com/sirupsen/logrus"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  
    32  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    33  	"sigs.k8s.io/prow/pkg/config"
    34  	"sigs.k8s.io/prow/pkg/github"
    35  	"sigs.k8s.io/prow/pkg/layeredsets"
    36  	"sigs.k8s.io/prow/pkg/plugins"
    37  	"sigs.k8s.io/prow/pkg/plugins/ownersconfig"
    38  	"sigs.k8s.io/prow/pkg/repoowners"
    39  )
    40  
    41  const (
    42  	fakeOrg     = "fake-org"
    43  	fakeRepo    = "fake-repo"
    44  	fakePR      = 33
    45  	fakeSHA     = "deadbeef"
    46  	faseBaseRef = "fake-branch"
    47  	fakeBaseSHA = "fffffff"
    48  	adminUser   = "admin-user"
    49  )
    50  
    51  type fakeRepoownersClient struct {
    52  	foc *fakeOwnersClient
    53  }
    54  
    55  func (froc *fakeRepoownersClient) LoadRepoOwners(org, repo, base string) (repoowners.RepoOwner, error) {
    56  	return froc.foc, nil
    57  }
    58  
    59  type fakeOwnersClient struct {
    60  	topLevelApprovers sets.Set[string]
    61  }
    62  
    63  func (foc *fakeOwnersClient) AllApprovers() sets.Set[string] {
    64  	return sets.Set[string]{}
    65  }
    66  
    67  func (foc *fakeOwnersClient) AllOwners() sets.Set[string] {
    68  	return sets.Set[string]{}
    69  }
    70  
    71  func (foc *fakeOwnersClient) AllReviewers() sets.Set[string] {
    72  	return sets.Set[string]{}
    73  }
    74  
    75  func (foc *fakeOwnersClient) Filenames() ownersconfig.Filenames {
    76  	return ownersconfig.FakeFilenames
    77  }
    78  
    79  func (foc *fakeOwnersClient) TopLevelApprovers() sets.Set[string] {
    80  	return foc.topLevelApprovers
    81  }
    82  
    83  func (foc *fakeOwnersClient) Approvers(path string) layeredsets.String {
    84  	return layeredsets.String{}
    85  }
    86  
    87  func (foc *fakeOwnersClient) LeafApprovers(path string) sets.Set[string] {
    88  	return sets.Set[string]{}
    89  }
    90  
    91  func (foc *fakeOwnersClient) FindApproverOwnersForFile(path string) string {
    92  	return ""
    93  }
    94  
    95  func (foc *fakeOwnersClient) Reviewers(path string) layeredsets.String {
    96  	return layeredsets.String{}
    97  }
    98  
    99  func (foc *fakeOwnersClient) RequiredReviewers(path string) sets.Set[string] {
   100  	return sets.Set[string]{}
   101  }
   102  
   103  func (foc *fakeOwnersClient) LeafReviewers(path string) sets.Set[string] {
   104  	return sets.Set[string]{}
   105  }
   106  
   107  func (foc *fakeOwnersClient) FindReviewersOwnersForFile(path string) string {
   108  	return ""
   109  }
   110  
   111  func (foc *fakeOwnersClient) FindLabelsForFile(path string) sets.Set[string] {
   112  	return sets.Set[string]{}
   113  }
   114  
   115  func (foc *fakeOwnersClient) IsNoParentOwners(path string) bool {
   116  	return false
   117  }
   118  
   119  func (foc *fakeOwnersClient) IsAutoApproveUnownedSubfolders(path string) bool {
   120  	return false
   121  }
   122  
   123  func (foc *fakeOwnersClient) ParseSimpleConfig(path string) (repoowners.SimpleConfig, error) {
   124  	return repoowners.SimpleConfig{}, nil
   125  }
   126  
   127  func (foc *fakeOwnersClient) ParseFullConfig(path string) (repoowners.FullConfig, error) {
   128  	return repoowners.FullConfig{}, nil
   129  }
   130  
   131  type fakeClient struct {
   132  	comments         []string
   133  	statuses         []github.Status
   134  	branchProtection *github.BranchProtection
   135  	ps               []config.Presubmit
   136  	jobs             sets.Set[string]
   137  	owners           ownersClient
   138  	checkruns        *github.CheckRunList
   139  	usesAppsAuth     bool
   140  }
   141  
   142  func (c *fakeClient) presubmits(_, _ string, _ config.RefGetter, _ string) ([]config.Presubmit, error) {
   143  	var result []config.Presubmit
   144  	result = append(result, c.ps...)
   145  	return result, nil
   146  }
   147  
   148  func (c *fakeClient) CreateComment(org, repo string, number int, comment string) error {
   149  	c.comments = append(c.comments, comment)
   150  	return nil
   151  }
   152  
   153  func (c *fakeClient) CreateStatus(org, repo, ref string, s github.Status) error {
   154  	switch {
   155  	case s.Context == "fail-create":
   156  		return errors.New("injected CreateStatus failure")
   157  	case org != fakeOrg:
   158  		return fmt.Errorf("bad org: %s", org)
   159  	case repo != fakeRepo:
   160  		return fmt.Errorf("bad repo: %s", repo)
   161  	case ref != fakeSHA:
   162  		return fmt.Errorf("bad ref: %s", ref)
   163  	}
   164  	for i, status := range c.statuses {
   165  		if status.State != github.StatusSuccess && status.Context == s.Context {
   166  			c.statuses[i] = s
   167  			return nil
   168  		}
   169  	}
   170  	//handle branch protection case
   171  	if len(c.statuses) == 0 {
   172  		c.statuses = append(c.statuses, s)
   173  	}
   174  	return nil
   175  }
   176  
   177  func (c *fakeClient) GetPullRequest(org, repo string, number int) (*github.PullRequest, error) {
   178  	switch {
   179  	case number < 0:
   180  		return nil, errors.New("injected GetPullRequest failure")
   181  	case org != fakeOrg:
   182  		return nil, fmt.Errorf("bad org: %s", org)
   183  	case repo != fakeRepo:
   184  		return nil, fmt.Errorf("bad repo: %s", repo)
   185  	case number != fakePR:
   186  		return nil, fmt.Errorf("bad number: %d", number)
   187  	}
   188  	var pr github.PullRequest
   189  	pr.Head.SHA = fakeSHA
   190  	pr.Base.Ref = faseBaseRef
   191  	return &pr, nil
   192  }
   193  
   194  func (c *fakeClient) ListStatuses(org, repo, ref string) ([]github.Status, error) {
   195  	switch {
   196  	case org != fakeOrg:
   197  		return nil, fmt.Errorf("bad org: %s", org)
   198  	case repo != fakeRepo:
   199  		return nil, fmt.Errorf("bad repo: %s", repo)
   200  	case ref != fakeSHA:
   201  		return nil, fmt.Errorf("bad ref: %s", ref)
   202  	}
   203  	var out []github.Status
   204  	for _, s := range c.statuses {
   205  		if s.Context == "fail-list" {
   206  			return nil, errors.New("injected ListStatuses failure")
   207  		}
   208  		out = append(out, s)
   209  	}
   210  	return out, nil
   211  }
   212  
   213  func (c *fakeClient) ListCheckRuns(org, repo, ref string) (*github.CheckRunList, error) {
   214  	if c.checkruns != nil {
   215  		return c.checkruns, nil
   216  	}
   217  	return &github.CheckRunList{}, nil
   218  }
   219  
   220  func (c *fakeClient) CreateCheckRun(org, repo string, checkRun github.CheckRun) error {
   221  	for _, checkrun := range c.checkruns.CheckRuns {
   222  		if checkrun.CompletedAt == "" {
   223  			continue
   224  		} else if strings.ToUpper(checkrun.Conclusion) == "NEUTRAL" {
   225  			continue
   226  		} else if strings.ToUpper(checkrun.Conclusion) == "SUCCESS" {
   227  			continue
   228  		} else if checkrun.Name == checkRun.Name {
   229  			prowOverrideCR := github.CheckRun{
   230  				Name:        checkrun.Name,
   231  				HeadSHA:     checkrun.HeadSHA,
   232  				CompletedAt: checkrun.CompletedAt,
   233  				Status:      "completed",
   234  				Conclusion:  "success",
   235  				Output: github.CheckRunOutput{
   236  					Title:   fmt.Sprintf("Prow override - %s", checkrun.Name),
   237  					Summary: fmt.Sprintf("Prow has received override command for the %s checkrun.", checkrun.Name),
   238  				},
   239  			}
   240  			c.checkruns.CheckRuns = append(c.checkruns.CheckRuns, prowOverrideCR)
   241  		}
   242  	}
   243  	return nil
   244  }
   245  
   246  func (c *fakeClient) GetBranchProtection(org, repo, branch string) (*github.BranchProtection, error) {
   247  	switch {
   248  	case org != fakeOrg:
   249  		return nil, fmt.Errorf("bad org: %s", org)
   250  	case repo != fakeRepo:
   251  		return nil, fmt.Errorf("bad repo: %s", repo)
   252  	case branch != faseBaseRef:
   253  		return nil, fmt.Errorf("bad branch: %s", branch)
   254  	}
   255  
   256  	if c.branchProtection != nil && c.branchProtection.RequiredStatusChecks != nil &&
   257  		len(c.branchProtection.RequiredStatusChecks.Contexts) > 0 &&
   258  		c.branchProtection.RequiredStatusChecks.Contexts[0] == "fail-protection" {
   259  		return nil, errors.New("injected GetBranchProtection failure")
   260  	}
   261  
   262  	return c.branchProtection, nil
   263  }
   264  
   265  func (c *fakeClient) HasPermission(org, repo, user string, roles ...string) (bool, error) {
   266  	switch {
   267  	case org != fakeOrg:
   268  		return false, fmt.Errorf("bad org: %s", org)
   269  	case repo != fakeRepo:
   270  		return false, fmt.Errorf("bad repo: %s", repo)
   271  	case roles[0] != github.RoleAdmin:
   272  		return false, fmt.Errorf("bad roles: %s", roles)
   273  	case user == "fail":
   274  		return true, errors.New("injected HasPermission error")
   275  	}
   276  	return user == adminUser, nil
   277  }
   278  
   279  func (c *fakeClient) GetRef(org, repo, ref string) (string, error) {
   280  	if repo == "fail-ref" {
   281  		return "", errors.New("injected GetRef error")
   282  	}
   283  	return fakeBaseSHA, nil
   284  }
   285  
   286  func (c *fakeClient) ListTeams(org string) ([]github.Team, error) {
   287  	if org == fakeOrg {
   288  		return []github.Team{
   289  			{
   290  				ID:   1,
   291  				Name: "team foo",
   292  				Slug: "team-foo",
   293  			},
   294  		}, nil
   295  	}
   296  	return []github.Team{}, nil
   297  }
   298  
   299  func (c *fakeClient) ListTeamMembersBySlug(org, teamSlug, role string) ([]github.TeamMember, error) {
   300  	if teamSlug == "team-foo" {
   301  		return []github.TeamMember{
   302  			{Login: "user1"},
   303  			{Login: "user2"},
   304  		}, nil
   305  	}
   306  	return []github.TeamMember{}, nil
   307  }
   308  
   309  func (c *fakeClient) Create(_ context.Context, pj *prowapi.ProwJob, _ metav1.CreateOptions) (*prowapi.ProwJob, error) {
   310  	if s := pj.Status.State; s != prowapi.SuccessState {
   311  		return pj, fmt.Errorf("bad status state: %s", s)
   312  	}
   313  	if pj.Spec.Context == "fail-create" {
   314  		return pj, errors.New("injected CreateProwJob error")
   315  	}
   316  	c.jobs.Insert(pj.Spec.Context)
   317  	return pj, nil
   318  }
   319  
   320  func (c *fakeClient) LoadRepoOwners(org, repo, base string) (repoowners.RepoOwner, error) {
   321  	return c.owners.LoadRepoOwners(org, repo, base)
   322  }
   323  
   324  func (c *fakeClient) UsesAppAuth() bool {
   325  	return c.usesAppsAuth
   326  }
   327  
   328  func TestAuthorizedUser(t *testing.T) {
   329  	cases := []struct {
   330  		name     string
   331  		user     string
   332  		expected bool
   333  	}{
   334  		{
   335  			name: "fail closed",
   336  			user: "fail",
   337  		},
   338  		{
   339  			name: "reject rando",
   340  			user: "random",
   341  		},
   342  		{
   343  			name:     "accept admin",
   344  			user:     adminUser,
   345  			expected: true,
   346  		},
   347  	}
   348  
   349  	log := logrus.WithField("plugin", pluginName)
   350  	for _, tc := range cases {
   351  		t.Run(tc.name, func(t *testing.T) {
   352  			if actual := authorizedUser(&fakeClient{}, log, fakeOrg, fakeRepo, tc.user); actual != tc.expected {
   353  				t.Errorf("actual %t != expected %t", actual, tc.expected)
   354  			}
   355  		})
   356  	}
   357  }
   358  
   359  func TestHandle(t *testing.T) {
   360  	cases := []struct {
   361  		name              string
   362  		action            github.GenericCommentEventAction
   363  		issue             bool
   364  		state             string
   365  		comment           string
   366  		contexts          []github.Status
   367  		branchProtection  *github.BranchProtection
   368  		presubmits        []config.Presubmit
   369  		user              string
   370  		number            int
   371  		expected          []github.Status
   372  		expectedCheckRuns *github.CheckRunList
   373  		jobs              sets.Set[string]
   374  		checkComments     []string
   375  		options           plugins.Override
   376  		approvers         []string
   377  		err               bool
   378  		checkruns         *github.CheckRunList
   379  		usesAppsAuth      bool
   380  	}{
   381  		{
   382  			name:    "successfully override failure",
   383  			comment: "/override broken-test",
   384  			contexts: []github.Status{
   385  				{
   386  					Context: "broken-test",
   387  					State:   github.StatusFailure,
   388  				},
   389  			},
   390  			expected: []github.Status{
   391  				{
   392  					Context:     "broken-test",
   393  					Description: description(adminUser),
   394  					State:       github.StatusSuccess,
   395  				},
   396  			},
   397  			checkComments: []string{"on behalf of " + adminUser},
   398  		},
   399  		{
   400  			name:    "successfully override unknown context derived from checkruns",
   401  			comment: "/override failure-checkrun",
   402  			checkruns: &github.CheckRunList{
   403  				CheckRuns: []github.CheckRun{
   404  					{Name: "incomplete-checkrun"},
   405  					{Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"},
   406  				},
   407  			},
   408  			expected: []github.Status{},
   409  			expectedCheckRuns: &github.CheckRunList{
   410  				CheckRuns: []github.CheckRun{
   411  					{Name: "incomplete-checkrun"},
   412  					{Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"},
   413  					{Name: "failure-checkrun", CompletedAt: "1800 BC", Status: "completed", Conclusion: "success", Output: github.CheckRunOutput{
   414  						Title:   fmt.Sprintf("Prow override - %s", "failure-checkrun"),
   415  						Summary: fmt.Sprintf("Prow has received override command for the %s checkrun.", "failure-checkrun"),
   416  					}},
   417  				},
   418  			},
   419  			usesAppsAuth: true,
   420  		},
   421  		{
   422  			name:    "successfully override unknown context with special characters derived from checkruns",
   423  			comment: `/override "test / Unit Tests"`,
   424  			checkruns: &github.CheckRunList{
   425  				CheckRuns: []github.CheckRun{
   426  					{Name: "incomplete-checkrun"},
   427  					{Name: "test / Unit Tests", CompletedAt: "1800 BC", Conclusion: "failure"},
   428  				},
   429  			},
   430  			expected: []github.Status{},
   431  			expectedCheckRuns: &github.CheckRunList{
   432  				CheckRuns: []github.CheckRun{
   433  					{Name: "incomplete-checkrun"},
   434  					{Name: "test / Unit Tests", CompletedAt: "1800 BC", Conclusion: "failure"},
   435  					{Name: "test / Unit Tests", CompletedAt: "1800 BC", Status: "completed", Conclusion: "success", Output: github.CheckRunOutput{
   436  						Title:   fmt.Sprintf("Prow override - %s", "test / Unit Tests"),
   437  						Summary: fmt.Sprintf("Prow has received override command for the %s checkrun.", "test / Unit Tests"),
   438  					}},
   439  				},
   440  			},
   441  			usesAppsAuth: true,
   442  		},
   443  		{
   444  			name:    "successfully override a mix of checkruns and prowjobs",
   445  			comment: `/override broken-test "test / Unit Tests" hung-test`,
   446  			checkruns: &github.CheckRunList{
   447  				CheckRuns: []github.CheckRun{
   448  					{Name: "incomplete-checkrun"},
   449  					{Name: "test / Unit Tests", CompletedAt: "1800 BC", Conclusion: "failure"},
   450  				},
   451  			},
   452  			contexts: []github.Status{
   453  				{
   454  					Context: "broken-test",
   455  					State:   github.StatusFailure,
   456  				},
   457  				{
   458  					Context: "hung-test",
   459  					State:   github.StatusPending,
   460  				},
   461  			},
   462  			expected: []github.Status{
   463  				{
   464  					Context:     "broken-test",
   465  					Description: description(adminUser),
   466  					State:       github.StatusSuccess,
   467  				},
   468  				{
   469  					Context:     "hung-test",
   470  					Description: description(adminUser),
   471  					State:       github.StatusSuccess,
   472  				},
   473  			},
   474  			expectedCheckRuns: &github.CheckRunList{
   475  				CheckRuns: []github.CheckRun{
   476  					{Name: "incomplete-checkrun"},
   477  					{Name: "test / Unit Tests", CompletedAt: "1800 BC", Conclusion: "failure"},
   478  					{Name: "test / Unit Tests", CompletedAt: "1800 BC", Status: "completed", Conclusion: "success", Output: github.CheckRunOutput{
   479  						Title:   fmt.Sprintf("Prow override - %s", "test / Unit Tests"),
   480  						Summary: fmt.Sprintf("Prow has received override command for the %s checkrun.", "test / Unit Tests"),
   481  					}},
   482  				},
   483  			},
   484  			usesAppsAuth: true,
   485  		},
   486  		{
   487  			name:    "override a successful unknown context derived from checkruns",
   488  			comment: "/override success-checkrun",
   489  			checkruns: &github.CheckRunList{
   490  				CheckRuns: []github.CheckRun{
   491  					{Name: "incomplete-checkrun"},
   492  					{Name: "success-checkrun", CompletedAt: "1800 BC", Conclusion: "success"},
   493  				},
   494  			},
   495  			expected: []github.Status{},
   496  			expectedCheckRuns: &github.CheckRunList{
   497  				CheckRuns: []github.CheckRun{
   498  					{Name: "incomplete-checkrun"},
   499  					{Name: "success-checkrun", CompletedAt: "1800 BC", Conclusion: "success"},
   500  				},
   501  			},
   502  			usesAppsAuth: true,
   503  			checkComments: []string{
   504  				"The following unknown contexts/checkruns were given:", "`success-checkrun`",
   505  			},
   506  		},
   507  		{
   508  			name:    "override failure-checkrun checkrun, usesAppsAuth is false",
   509  			comment: "/override failure-checkrun",
   510  			checkruns: &github.CheckRunList{
   511  				CheckRuns: []github.CheckRun{
   512  					{Name: "incomplete-checkrun"},
   513  					{Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"},
   514  				},
   515  			},
   516  			expected: []github.Status{},
   517  			expectedCheckRuns: &github.CheckRunList{
   518  				CheckRuns: []github.CheckRun{
   519  					{Name: "incomplete-checkrun"},
   520  					{Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"},
   521  				},
   522  			},
   523  			usesAppsAuth: false,
   524  		},
   525  		{
   526  			name:    "override nonexistant checkrun",
   527  			comment: "/override foobar",
   528  			checkruns: &github.CheckRunList{
   529  				CheckRuns: []github.CheckRun{
   530  					{Name: "incomplete-checkrun"},
   531  					{Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"},
   532  				},
   533  			},
   534  			expected: []github.Status{},
   535  			expectedCheckRuns: &github.CheckRunList{
   536  				CheckRuns: []github.CheckRun{
   537  					{Name: "incomplete-checkrun"},
   538  					{Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"},
   539  				},
   540  			},
   541  			usesAppsAuth: true,
   542  		},
   543  
   544  		{
   545  			name:    "successfully override pending",
   546  			comment: "/override hung-test",
   547  			contexts: []github.Status{
   548  				{
   549  					Context: "hung-test",
   550  					State:   github.StatusPending,
   551  				},
   552  			},
   553  			expected: []github.Status{
   554  				{
   555  					Context:     "hung-test",
   556  					Description: description(adminUser),
   557  					State:       github.StatusSuccess,
   558  				},
   559  			},
   560  			usesAppsAuth: true,
   561  		},
   562  		{
   563  			name:    "comment for incorrect context",
   564  			comment: "/override whatever-you-want",
   565  			contexts: []github.Status{
   566  				{
   567  					Context: "hung-test",
   568  					State:   github.StatusPending,
   569  				},
   570  			},
   571  			presubmits: []config.Presubmit{
   572  				{
   573  					JobBase: config.JobBase{
   574  						Name: "hung-prow-job",
   575  					},
   576  					Reporter: config.Reporter{
   577  						Context: "hung-test",
   578  					},
   579  				},
   580  			},
   581  			expected: []github.Status{
   582  				{
   583  					Context: "hung-test",
   584  					State:   github.StatusPending,
   585  				},
   586  			},
   587  			checkComments: []string{
   588  				"The following unknown contexts/checkruns were given", "whatever-you-want",
   589  				"Only the following failed contexts/checkruns were expected", "hung-test", "hung-prow-job",
   590  			},
   591  		},
   592  		{
   593  			name:    "refuse override from non-admin",
   594  			comment: "/override broken-test",
   595  			contexts: []github.Status{
   596  				{
   597  					Context: "broken-test",
   598  					State:   github.StatusPending,
   599  				},
   600  			},
   601  			user:          "rando",
   602  			checkComments: []string{"unauthorized"},
   603  			expected: []github.Status{
   604  				{
   605  					Context: "broken-test",
   606  					State:   github.StatusPending,
   607  				},
   608  			},
   609  		},
   610  		{
   611  			name:    "comment for override with no target",
   612  			comment: "/override",
   613  			contexts: []github.Status{
   614  				{
   615  					Context: "broken-test",
   616  					State:   github.StatusPending,
   617  				},
   618  			},
   619  			user:          "rando",
   620  			checkComments: []string{"but none was given"},
   621  			expected: []github.Status{
   622  				{
   623  					Context: "broken-test",
   624  					State:   github.StatusPending,
   625  				},
   626  			},
   627  		},
   628  		{
   629  			name:    "override multiple",
   630  			comment: "/override broken-test\n/override hung-test",
   631  			contexts: []github.Status{
   632  				{
   633  					Context: "broken-test",
   634  					State:   github.StatusFailure,
   635  				},
   636  				{
   637  					Context: "hung-test",
   638  					State:   github.StatusPending,
   639  				},
   640  			},
   641  			expected: []github.Status{
   642  				{
   643  					Context:     "broken-test",
   644  					Description: description(adminUser),
   645  					State:       github.StatusSuccess,
   646  				},
   647  				{
   648  					Context:     "hung-test",
   649  					Description: description(adminUser),
   650  					State:       github.StatusSuccess,
   651  				},
   652  			},
   653  			checkComments: []string{fmt.Sprintf("%s: broken-test, hung-test", adminUser)},
   654  		},
   655  		{
   656  			name:    "override multiple contexts inline",
   657  			comment: "/override broken-test hung-test",
   658  			contexts: []github.Status{
   659  				{
   660  					Context: "broken-test",
   661  					State:   github.StatusFailure,
   662  				},
   663  				{
   664  					Context: "hung-test",
   665  					State:   github.StatusPending,
   666  				},
   667  			},
   668  			expected: []github.Status{
   669  				{
   670  					Context:     "broken-test",
   671  					Description: description(adminUser),
   672  					State:       github.StatusSuccess,
   673  				},
   674  				{
   675  					Context:     "hung-test",
   676  					Description: description(adminUser),
   677  					State:       github.StatusSuccess,
   678  				},
   679  			},
   680  			checkComments: []string{fmt.Sprintf("%s: broken-test, hung-test", adminUser)},
   681  		},
   682  		{
   683  			name: "override with extra whitespace",
   684  			// Note two spaces here to start, and trailing whitespace
   685  			comment: "/override  broken-test \r\n", // github ends lines with \r\n
   686  			contexts: []github.Status{
   687  				{
   688  					Context: "broken-test",
   689  					State:   github.StatusFailure,
   690  				},
   691  			},
   692  			expected: []github.Status{
   693  				{
   694  					Context:     "broken-test",
   695  					Description: description(adminUser),
   696  					State:       github.StatusSuccess,
   697  				},
   698  			},
   699  			checkComments: []string{fmt.Sprintf("%s: broken-test", adminUser)},
   700  		},
   701  		{
   702  			name:    "ignore non-PRs",
   703  			issue:   true,
   704  			comment: "/override broken-test",
   705  			contexts: []github.Status{
   706  				{
   707  					Context: "broken-test",
   708  					State:   github.StatusPending,
   709  				},
   710  			},
   711  			expected: []github.Status{
   712  				{
   713  					Context: "broken-test",
   714  					State:   github.StatusPending,
   715  				},
   716  			},
   717  		},
   718  		{
   719  			name:    "ignore closed issues",
   720  			state:   "closed",
   721  			comment: "/override broken-test",
   722  			contexts: []github.Status{
   723  				{
   724  					Context: "broken-test",
   725  					State:   github.StatusPending,
   726  				},
   727  			},
   728  			expected: []github.Status{
   729  				{
   730  					Context: "broken-test",
   731  					State:   github.StatusPending,
   732  				},
   733  			},
   734  		},
   735  		{
   736  			name:    "ignore edits",
   737  			action:  github.GenericCommentActionEdited,
   738  			comment: "/override broken-test",
   739  			contexts: []github.Status{
   740  				{
   741  					Context: "broken-test",
   742  					State:   github.StatusPending,
   743  				},
   744  			},
   745  			expected: []github.Status{
   746  				{
   747  					Context: "broken-test",
   748  					State:   github.StatusPending,
   749  				},
   750  			},
   751  		},
   752  		{
   753  			name:    "ignore random text",
   754  			comment: "/test broken-test",
   755  			contexts: []github.Status{
   756  				{
   757  					Context: "broken-test",
   758  					State:   github.StatusPending,
   759  				},
   760  			},
   761  			expected: []github.Status{
   762  				{
   763  					Context: "broken-test",
   764  					State:   github.StatusPending,
   765  				},
   766  			},
   767  		},
   768  		{
   769  			name:    "comment on get pr failure",
   770  			number:  fakePR * 2,
   771  			comment: "/override broken-test",
   772  			contexts: []github.Status{
   773  				{
   774  					Context: "broken-test",
   775  					State:   github.StatusFailure,
   776  				},
   777  			},
   778  			expected: []github.Status{
   779  				{
   780  					Context: "broken-test",
   781  					State:   github.StatusFailure,
   782  				},
   783  			},
   784  			checkComments: []string{"Cannot get PR"},
   785  		},
   786  		{
   787  			name:    "comment on list statuses failure",
   788  			comment: "/override fail-list",
   789  			contexts: []github.Status{
   790  				{
   791  					Context: "fail-list",
   792  					State:   github.StatusFailure,
   793  				},
   794  			},
   795  			expected: []github.Status{
   796  				{
   797  					Context: "fail-list",
   798  					State:   github.StatusFailure,
   799  				},
   800  			},
   801  			checkComments: []string{"Cannot get commit statuses"},
   802  		},
   803  		{
   804  			name:    "comment on get branch protection failure",
   805  			comment: "/override fail-list",
   806  			branchProtection: &github.BranchProtection{RequiredStatusChecks: &github.RequiredStatusChecks{
   807  				Contexts: []string{"fail-protection"},
   808  			}},
   809  			contexts: []github.Status{
   810  				{
   811  					Context: "broken-test",
   812  					State:   github.StatusFailure,
   813  				},
   814  			},
   815  			expected: []github.Status{
   816  				{
   817  					Context: "broken-test",
   818  					State:   github.StatusFailure,
   819  				},
   820  			},
   821  			checkComments: []string{"Cannot get branch protection"},
   822  		},
   823  		{
   824  			name:    "do not override passing contexts",
   825  			comment: "/override passing-test",
   826  			contexts: []github.Status{
   827  				{
   828  					Context:     "passing-test",
   829  					Description: "preserve description",
   830  					State:       github.StatusSuccess,
   831  				},
   832  			},
   833  			expected: []github.Status{
   834  				{
   835  					Context:     "passing-test",
   836  					State:       github.StatusSuccess,
   837  					Description: "preserve description",
   838  				},
   839  			},
   840  		},
   841  		{
   842  			name:    "create successful prow job",
   843  			comment: "/override prow-job",
   844  			contexts: []github.Status{
   845  				{
   846  					Context:     "prow-job",
   847  					Description: "failed",
   848  					State:       github.StatusFailure,
   849  				},
   850  			},
   851  			presubmits: []config.Presubmit{
   852  				{
   853  					JobBase: config.JobBase{
   854  						Name: "prow-job",
   855  					},
   856  					Reporter: config.Reporter{
   857  						Context: "prow-job",
   858  					},
   859  				},
   860  			},
   861  			jobs: sets.New[string]("prow-job"),
   862  			expected: []github.Status{
   863  				{
   864  					Context:     "prow-job",
   865  					State:       github.StatusSuccess,
   866  					Description: description(adminUser),
   867  				},
   868  			},
   869  		},
   870  		{
   871  			name:    "successfully override prow job name",
   872  			comment: "/override prow-job",
   873  			contexts: []github.Status{
   874  				{
   875  					Context:     "ci/prow/pkg-job",
   876  					Description: "failed",
   877  					State:       github.StatusFailure,
   878  				},
   879  			},
   880  			presubmits: []config.Presubmit{
   881  				{
   882  					JobBase: config.JobBase{
   883  						Name: "prow-job",
   884  					},
   885  					Reporter: config.Reporter{
   886  						Context: "ci/prow/pkg-job",
   887  					},
   888  				},
   889  			},
   890  			jobs: sets.New[string]("ci/prow/pkg-job"),
   891  			expected: []github.Status{
   892  				{
   893  					Context:     "ci/prow/pkg-job",
   894  					State:       github.StatusSuccess,
   895  					Description: description(adminUser),
   896  				},
   897  			},
   898  		},
   899  		{
   900  			name:    "override prow job and context",
   901  			comment: "/override prow-job\n/override ci/prow/context",
   902  			contexts: []github.Status{
   903  				{
   904  					Context:     "ci/prow/context",
   905  					Description: "failed",
   906  					State:       github.StatusFailure,
   907  				},
   908  				{
   909  					Context:     "ci/prow/pkg-job",
   910  					Description: "failed",
   911  					State:       github.StatusFailure,
   912  				},
   913  			},
   914  			presubmits: []config.Presubmit{
   915  				{
   916  					JobBase: config.JobBase{
   917  						Name: "prow-job",
   918  					},
   919  					Reporter: config.Reporter{
   920  						Context: "ci/prow/pkg-job",
   921  					},
   922  				},
   923  			},
   924  			jobs: sets.New[string]("ci/prow/pkg-job"),
   925  			expected: []github.Status{
   926  				{
   927  					Context:     "ci/prow/context",
   928  					State:       github.StatusSuccess,
   929  					Description: description(adminUser),
   930  				},
   931  				{
   932  					Context:     "ci/prow/pkg-job",
   933  					State:       github.StatusSuccess,
   934  					Description: description(adminUser),
   935  				},
   936  			},
   937  		},
   938  		{
   939  			name:    "override same context and prow job",
   940  			comment: "/override ci/prow/pkg-job\n/override prow-job",
   941  			contexts: []github.Status{
   942  				{
   943  					Context:     "ci/prow/pkg-job",
   944  					Description: "failed",
   945  					State:       github.StatusFailure,
   946  				},
   947  			},
   948  			presubmits: []config.Presubmit{
   949  				{
   950  					JobBase: config.JobBase{
   951  						Name: "prow-job",
   952  					},
   953  					Reporter: config.Reporter{
   954  						Context: "ci/prow/pkg-job",
   955  					},
   956  				},
   957  			},
   958  			jobs: sets.New[string]("ci/prow/pkg-job"),
   959  			expected: []github.Status{
   960  				{
   961  					Context:     "ci/prow/pkg-job",
   962  					State:       github.StatusSuccess,
   963  					Description: description(adminUser),
   964  				},
   965  			},
   966  		},
   967  		{
   968  			name:    "override with explanation works",
   969  			comment: "/override job\r\nobnoxious flake", // github ends lines with \r\n
   970  			contexts: []github.Status{
   971  				{
   972  					Context:     "job",
   973  					Description: "failed",
   974  					State:       github.StatusFailure,
   975  				},
   976  			},
   977  			expected: []github.Status{
   978  				{
   979  					Context:     "job",
   980  					Description: description(adminUser),
   981  					State:       github.StatusSuccess,
   982  				},
   983  			},
   984  		},
   985  		{
   986  			name:      "override with allow_top_level_owners works",
   987  			comment:   "/override job",
   988  			user:      "code_owner",
   989  			options:   plugins.Override{AllowTopLevelOwners: true},
   990  			approvers: []string{"code_owner"},
   991  			contexts: []github.Status{
   992  				{
   993  					Context:     "job",
   994  					Description: "failed",
   995  					State:       github.StatusFailure,
   996  				},
   997  			},
   998  			expected: []github.Status{
   999  				{
  1000  					Context:     "job",
  1001  					Description: description("code_owner"),
  1002  					State:       github.StatusSuccess,
  1003  				},
  1004  			},
  1005  		},
  1006  		{
  1007  			name:      "override with allow_top_level_owners works for uppercase user",
  1008  			comment:   "/override job",
  1009  			user:      "Code_owner",
  1010  			options:   plugins.Override{AllowTopLevelOwners: true},
  1011  			approvers: []string{"code_owner"},
  1012  			contexts: []github.Status{
  1013  				{
  1014  					Context:     "job",
  1015  					Description: "failed",
  1016  					State:       github.StatusFailure,
  1017  				},
  1018  			},
  1019  			expected: []github.Status{
  1020  				{
  1021  					Context:     "job",
  1022  					Description: description("Code_owner"),
  1023  					State:       github.StatusSuccess,
  1024  				},
  1025  			},
  1026  		},
  1027  		{
  1028  			name:    "override with allow_top_level_owners fails if user is not in OWNERS file",
  1029  			comment: "/override job",
  1030  			user:    "non_code_owner",
  1031  			options: plugins.Override{AllowTopLevelOwners: true},
  1032  			contexts: []github.Status{
  1033  				{
  1034  					Context:     "job",
  1035  					Description: "failed",
  1036  					State:       github.StatusFailure,
  1037  				},
  1038  			},
  1039  			expected: []github.Status{
  1040  				{
  1041  					Context:     "job",
  1042  					Description: "failed",
  1043  					State:       github.StatusFailure,
  1044  				},
  1045  			},
  1046  		},
  1047  		{
  1048  			name:    "override with allowed_github_team allowed if user is in specified github team",
  1049  			comment: "/override job",
  1050  			user:    "user1",
  1051  			options: plugins.Override{
  1052  				AllowedGitHubTeams: map[string][]string{
  1053  					fmt.Sprintf("%s/%s", fakeOrg, fakeRepo): {"team-foo"},
  1054  				},
  1055  			},
  1056  			contexts: []github.Status{
  1057  				{
  1058  					Context:     "job",
  1059  					Description: "failed",
  1060  					State:       github.StatusFailure,
  1061  				},
  1062  			},
  1063  			expected: []github.Status{
  1064  				{
  1065  					Context:     "job",
  1066  					Description: description("user1"),
  1067  					State:       github.StatusSuccess,
  1068  				},
  1069  			},
  1070  		},
  1071  		{
  1072  			name:    "override does not fail due to invalid github team slug",
  1073  			comment: "/override job",
  1074  			user:    "user1",
  1075  			options: plugins.Override{
  1076  				AllowedGitHubTeams: map[string][]string{
  1077  					fmt.Sprintf("%s/%s", fakeOrg, fakeRepo): {"team-foo", "invalid-team-slug"},
  1078  				},
  1079  			},
  1080  			contexts: []github.Status{
  1081  				{
  1082  					Context:     "job",
  1083  					Description: "failed",
  1084  					State:       github.StatusFailure,
  1085  				},
  1086  			},
  1087  			expected: []github.Status{
  1088  				{
  1089  					Context:     "job",
  1090  					Description: description("user1"),
  1091  					State:       github.StatusSuccess,
  1092  				},
  1093  			},
  1094  		},
  1095  		{
  1096  			name:             "override with empty branch protection",
  1097  			comment:          "/override job",
  1098  			branchProtection: &github.BranchProtection{},
  1099  			expected:         []github.Status{},
  1100  			checkComments:    []string{},
  1101  		},
  1102  		{
  1103  			name:             "override with branch protection empty status checks",
  1104  			comment:          "/override job",
  1105  			branchProtection: &github.BranchProtection{RequiredStatusChecks: &github.RequiredStatusChecks{}},
  1106  			expected:         []github.Status{},
  1107  			checkComments:    []string{},
  1108  		},
  1109  		{
  1110  			name:    "override with branch protection status checks",
  1111  			comment: "/override job",
  1112  			branchProtection: &github.BranchProtection{RequiredStatusChecks: &github.RequiredStatusChecks{
  1113  				Contexts: []string{"job"},
  1114  			}},
  1115  			expected: []github.Status{
  1116  				{
  1117  					Context:     "job",
  1118  					Description: description(adminUser),
  1119  					State:       github.StatusSuccess,
  1120  				},
  1121  			},
  1122  			checkComments: []string{"on behalf of " + adminUser},
  1123  		},
  1124  		{
  1125  			name:    "override with same branch protection status check and status",
  1126  			comment: "/override job",
  1127  			branchProtection: &github.BranchProtection{RequiredStatusChecks: &github.RequiredStatusChecks{
  1128  				Contexts: []string{"job"},
  1129  			}},
  1130  			contexts: []github.Status{
  1131  				{
  1132  					Context: "job",
  1133  					State:   github.StatusFailure,
  1134  				},
  1135  			},
  1136  			expected: []github.Status{
  1137  				{
  1138  					Context:     "job",
  1139  					Description: description(adminUser),
  1140  					State:       github.StatusSuccess,
  1141  				},
  1142  			},
  1143  			checkComments: []string{"on behalf of " + adminUser},
  1144  		},
  1145  		{
  1146  			name:    "handle only one status when multiple statuses have the same context",
  1147  			comment: "/override problematic-test",
  1148  			contexts: []github.Status{
  1149  				{
  1150  					Context: "problematic-test",
  1151  					State:   github.StatusPending,
  1152  				},
  1153  				{
  1154  					Context: "problematic-test",
  1155  					State:   github.StatusFailure,
  1156  				},
  1157  				{
  1158  					Context: "problematic-test",
  1159  					State:   github.StatusPending,
  1160  				},
  1161  			},
  1162  			presubmits: []config.Presubmit{
  1163  				{
  1164  					JobBase: config.JobBase{
  1165  						Name: "problematic-test",
  1166  					},
  1167  					Reporter: config.Reporter{
  1168  						Context: "problematic-test",
  1169  					},
  1170  				},
  1171  			},
  1172  			jobs: sets.New[string]("problematic-test"),
  1173  			expected: []github.Status{
  1174  				{
  1175  					Context:     "problematic-test",
  1176  					Description: description(adminUser),
  1177  					State:       github.StatusSuccess,
  1178  				},
  1179  				{
  1180  					Context: "problematic-test",
  1181  					State:   github.StatusFailure,
  1182  				},
  1183  				{
  1184  					Context: "problematic-test",
  1185  					State:   github.StatusPending,
  1186  				},
  1187  			},
  1188  		},
  1189  	}
  1190  
  1191  	log := logrus.WithField("plugin", pluginName)
  1192  	log.Logger.SetLevel(logrus.DebugLevel)
  1193  	for _, tc := range cases {
  1194  		t.Run(tc.name, func(t *testing.T) {
  1195  			if tc.number == 0 {
  1196  				tc.number = fakePR
  1197  			}
  1198  			if tc.user == "" {
  1199  				tc.user = adminUser
  1200  			}
  1201  			if tc.state == "" {
  1202  				tc.state = "open"
  1203  			}
  1204  			if tc.action == "" {
  1205  				tc.action = github.GenericCommentActionCreated
  1206  			}
  1207  			if tc.contexts == nil {
  1208  				tc.contexts = []github.Status{}
  1209  			}
  1210  
  1211  			event := github.GenericCommentEvent{
  1212  				Repo: github.Repo{
  1213  					Owner: github.User{
  1214  						Login: fakeOrg,
  1215  					},
  1216  					Name: fakeRepo,
  1217  				},
  1218  				User: github.User{
  1219  					Login: tc.user,
  1220  				},
  1221  				Body:       tc.comment,
  1222  				Number:     tc.number,
  1223  				IsPR:       !tc.issue,
  1224  				IssueState: tc.state,
  1225  				Action:     tc.action,
  1226  			}
  1227  
  1228  			froc := &fakeRepoownersClient{
  1229  				foc: &fakeOwnersClient{
  1230  					topLevelApprovers: sets.New[string](tc.approvers...),
  1231  				},
  1232  			}
  1233  			fc := fakeClient{
  1234  				statuses:         tc.contexts,
  1235  				branchProtection: tc.branchProtection,
  1236  				ps:               tc.presubmits,
  1237  				jobs:             sets.Set[string]{},
  1238  				owners:           froc,
  1239  				checkruns:        tc.checkruns,
  1240  				usesAppsAuth:     tc.usesAppsAuth,
  1241  			}
  1242  
  1243  			if tc.jobs == nil {
  1244  				tc.jobs = sets.Set[string]{}
  1245  			}
  1246  
  1247  			err := handle(&fc, log, &event, tc.options)
  1248  			switch {
  1249  			case err != nil:
  1250  				if !tc.err {
  1251  					t.Errorf("unexpected error: %v", err)
  1252  				}
  1253  			case tc.err:
  1254  				t.Error("failed to receive an error")
  1255  			case !reflect.DeepEqual(fc.statuses, tc.expected):
  1256  				t.Errorf("bad statuses: actual %#v != expected %#v", fc.statuses, tc.expected)
  1257  			case !reflect.DeepEqual(fc.jobs, tc.jobs):
  1258  				t.Errorf("bad jobs: actual %#v != expected %#v", fc.jobs, tc.jobs)
  1259  			case !reflect.DeepEqual(fc.checkruns, tc.expectedCheckRuns):
  1260  				t.Errorf("expected checkruns differs from actual: %s", cmp.Diff(fc.checkruns, tc.expectedCheckRuns))
  1261  
  1262  			}
  1263  			for _, expectedComment := range tc.checkComments {
  1264  				if !strings.Contains(strings.Join(fc.comments, "\n"), expectedComment) {
  1265  					t.Errorf("bad comments: expected %#v to be in %#v", expectedComment, fc.comments)
  1266  				}
  1267  			}
  1268  		})
  1269  	}
  1270  }
  1271  
  1272  func TestHelpProvider(t *testing.T) {
  1273  	cases := []struct {
  1274  		name        string
  1275  		config      plugins.Configuration
  1276  		org         string
  1277  		repo        string
  1278  		expectedWho string
  1279  	}{
  1280  		{
  1281  			name:        "WhoCanUse restricted to Repo administrators if no other options specified",
  1282  			config:      plugins.Configuration{},
  1283  			expectedWho: "Repo administrators.",
  1284  		},
  1285  		{
  1286  			name: "WhoCanUse includes top level code OWNERS if allow_top_level_owners is set",
  1287  			config: plugins.Configuration{
  1288  				Override: plugins.Override{
  1289  					AllowTopLevelOwners: true,
  1290  				},
  1291  			},
  1292  			expectedWho: "Repo administrators, approvers in top level OWNERS file.",
  1293  		},
  1294  		{
  1295  			name: "WhoCanUse includes specified github teams",
  1296  			config: plugins.Configuration{
  1297  				Override: plugins.Override{
  1298  					AllowedGitHubTeams: map[string][]string{
  1299  						"org1/repo1": {"team-foo", "team-bar"},
  1300  					},
  1301  				},
  1302  			},
  1303  			expectedWho: "Repo administrators, and the following github teams:" +
  1304  				"org1/repo1: team-foo team-bar.",
  1305  		},
  1306  	}
  1307  
  1308  	for _, tc := range cases {
  1309  		help, err := helpProvider(&tc.config, []config.OrgRepo{})
  1310  		if err != nil {
  1311  			t.Errorf("%s: unexpected error: %v", tc.name, err)
  1312  		}
  1313  		switch {
  1314  		case help == nil:
  1315  			t.Errorf("%s: expected a valid plugin help object, got nil", tc.name)
  1316  		case len(help.Commands) != 1:
  1317  			t.Errorf("%s: expected a single command from plugin help, got: %v", tc.name, help.Commands)
  1318  		case help.Commands[0].WhoCanUse != tc.expectedWho:
  1319  			t.Errorf("%s: expected a single command with WhoCanUse set to %s, got %s instead", tc.name, tc.expectedWho, help.Commands[0].WhoCanUse)
  1320  		}
  1321  	}
  1322  }
  1323  
  1324  func TestWhoCanUse(t *testing.T) {
  1325  	override := plugins.Override{
  1326  		AllowedGitHubTeams: map[string][]string{
  1327  			"org1/repo1": {"team-foo", "team-bar"},
  1328  			"org2/repo2": {"team-bar"},
  1329  			"org1":       {"team-foo-bar"},
  1330  		},
  1331  	}
  1332  	expectedWho := "Repo administrators, and the following github teams:" +
  1333  		"org1/repo1: team-foo team-bar, org1: team-foo-bar."
  1334  
  1335  	who := whoCanUse(override, "org1", "repo1")
  1336  	if who != expectedWho {
  1337  		t.Errorf("expected %q, got %q", expectedWho, who)
  1338  	}
  1339  }
  1340  
  1341  func TestAuthorizedGitHubTeamMember(t *testing.T) {
  1342  	repoRef := fmt.Sprintf("%s/%s", fakeOrg, fakeRepo)
  1343  	cases := []struct {
  1344  		name     string
  1345  		slugs    map[string][]string
  1346  		org      string
  1347  		repo     string
  1348  		user     string
  1349  		expected bool
  1350  	}{
  1351  		{
  1352  			name: "members of specified teams are authorized",
  1353  			slugs: map[string][]string{
  1354  				repoRef: {"team-foo"},
  1355  			},
  1356  			user:     "user1",
  1357  			expected: true,
  1358  		},
  1359  		{
  1360  			name: "non-members of specified teams are not authorized",
  1361  			slugs: map[string][]string{
  1362  				repoRef: {"team-foo"},
  1363  			},
  1364  			user: "non-member",
  1365  		},
  1366  		{
  1367  			name: "only teams corresponding to the org/repo are considered",
  1368  			slugs: map[string][]string{
  1369  				"org/repo": {"team-foo"},
  1370  			},
  1371  			user: "member",
  1372  		},
  1373  		{
  1374  			name: "members of specified teams are authorized to org",
  1375  			slugs: map[string][]string{
  1376  				fakeOrg: {"team-foo"},
  1377  			},
  1378  			user:     "user1",
  1379  			expected: true,
  1380  		},
  1381  	}
  1382  	log := logrus.WithField("plugin", pluginName)
  1383  	log.Logger.SetLevel(logrus.DebugLevel)
  1384  	for _, tc := range cases {
  1385  		authorized := authorizedGitHubTeamMember(&fakeClient{}, log, tc.slugs, fakeOrg, fakeRepo, tc.user)
  1386  		if authorized != tc.expected {
  1387  			t.Errorf("%s: actual: %v != expected %v", tc.name, authorized, tc.expected)
  1388  		}
  1389  	}
  1390  }
  1391  
  1392  func TestValidateGitHubTeamSlugs(t *testing.T) {
  1393  	githubTeams := []github.Team{
  1394  		{
  1395  			ID:   2,
  1396  			Slug: "team-bar",
  1397  		},
  1398  		{
  1399  			ID:   3,
  1400  			Slug: "team-baz",
  1401  		},
  1402  	}
  1403  
  1404  	repoRef := fmt.Sprintf("%s/%s", fakeOrg, fakeRepo)
  1405  	cases := []struct {
  1406  		name      string
  1407  		teamSlugs map[string][]string
  1408  		err       error
  1409  	}{
  1410  		{
  1411  			name: "validation failure for invalid team slug",
  1412  			teamSlugs: map[string][]string{
  1413  				repoRef: {"foo"},
  1414  			},
  1415  			err: fmt.Errorf("invalid team slug(s): foo"),
  1416  		},
  1417  		{
  1418  			name: "no errors for valid team slugs",
  1419  			teamSlugs: map[string][]string{
  1420  				repoRef: {"team-bar", "team-baz"},
  1421  			},
  1422  		},
  1423  	}
  1424  
  1425  	for _, tc := range cases {
  1426  		err := validateGitHubTeamSlugs(tc.teamSlugs, fakeOrg, fakeRepo, githubTeams)
  1427  		if !reflect.DeepEqual(err, tc.err) {
  1428  			t.Errorf("%s: actual: %v != expected %v", tc.name, err, tc.err)
  1429  		}
  1430  	}
  1431  }