...

Source file src/edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/reviewer/reviewer_test.go

Documentation: edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/reviewer

     1  // Copyright 2019 Palantir Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package reviewer
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"math/rand"
    21  	"testing"
    22  
    23  	"github.com/stretchr/testify/assert"
    24  	"github.com/stretchr/testify/require"
    25  
    26  	"edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/pull"
    27  	"edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/pull/pulltest"
    28  
    29  	"edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/common"
    30  )
    31  
    32  func TestFindLeafResults(t *testing.T) {
    33  	result := &common.Result{
    34  		Name:   "Parent",
    35  		Status: common.StatusPending,
    36  		Children: []*common.Result{
    37  			{
    38  				Name:              "One",
    39  				Status:            common.StatusPending,
    40  				ReviewRequestRule: &common.ReviewRequestRule{},
    41  			},
    42  			{
    43  				Name:              "Skipped",
    44  				Status:            common.StatusSkipped,
    45  				ReviewRequestRule: &common.ReviewRequestRule{},
    46  			},
    47  			{
    48  				Name:              "Error",
    49  				Status:            common.StatusPending,
    50  				Error:             errors.New("failed"),
    51  				ReviewRequestRule: &common.ReviewRequestRule{},
    52  			},
    53  			{
    54  				Name:              "Disapproved",
    55  				Status:            common.StatusDisapproved,
    56  				ReviewRequestRule: &common.ReviewRequestRule{},
    57  			},
    58  			{
    59  				Name:   "Two",
    60  				Status: common.StatusPending,
    61  				Children: []*common.Result{
    62  					{
    63  						Name:              "Three",
    64  						Status:            common.StatusPending,
    65  						ReviewRequestRule: &common.ReviewRequestRule{},
    66  					},
    67  				},
    68  			},
    69  		},
    70  	}
    71  	actualResults := FindRequests(result)
    72  	require.Len(t, actualResults, 2, "incorrect number of leaf results")
    73  }
    74  
    75  func TestSelectionDifference(t *testing.T) {
    76  	tests := map[string]struct {
    77  		Input     Selection
    78  		Reviewers []*pull.Reviewer
    79  		Output    Selection
    80  	}{
    81  		"users": {
    82  			Input: Selection{
    83  				Users: []string{"a", "b", "c"},
    84  			},
    85  			Reviewers: []*pull.Reviewer{
    86  				{
    87  					Type: pull.ReviewerUser,
    88  					Name: "a",
    89  				},
    90  				{
    91  					Type: pull.ReviewerUser,
    92  					Name: "c",
    93  				},
    94  				{
    95  					Type: pull.ReviewerTeam,
    96  					Name: "team-a",
    97  				},
    98  			},
    99  			Output: Selection{
   100  				Users: []string{"b"},
   101  			},
   102  		},
   103  		"teams": {
   104  			Input: Selection{
   105  				Teams: []string{"team-a", "team-b", "team-c"},
   106  			},
   107  			Reviewers: []*pull.Reviewer{
   108  				{
   109  					Type: pull.ReviewerUser,
   110  					Name: "a",
   111  				},
   112  				{
   113  					Type: pull.ReviewerUser,
   114  					Name: "c",
   115  				},
   116  				{
   117  					Type: pull.ReviewerTeam,
   118  					Name: "team-a",
   119  				},
   120  			},
   121  			Output: Selection{
   122  				Teams: []string{"team-b", "team-c"},
   123  			},
   124  		},
   125  		"dismissedUsers": {
   126  			Input: Selection{
   127  				Users: []string{"a", "b", "c"},
   128  			},
   129  			Reviewers: []*pull.Reviewer{
   130  				{
   131  					Type:    pull.ReviewerUser,
   132  					Name:    "a",
   133  					Removed: true,
   134  				},
   135  				{
   136  					Type: pull.ReviewerUser,
   137  					Name: "c",
   138  				},
   139  			},
   140  			Output: Selection{
   141  				Users: []string{"b"},
   142  			},
   143  		},
   144  		"dismissedTeams": {
   145  			Input: Selection{
   146  				Teams: []string{"team-a", "team-b", "team-c"},
   147  			},
   148  			Reviewers: []*pull.Reviewer{
   149  				{
   150  					Type: pull.ReviewerTeam,
   151  					Name: "team-a",
   152  				},
   153  				{
   154  					Type:    pull.ReviewerTeam,
   155  					Name:    "team-c",
   156  					Removed: true,
   157  				},
   158  			},
   159  			Output: Selection{
   160  				Teams: []string{"team-b"},
   161  			},
   162  		},
   163  	}
   164  
   165  	for name, test := range tests {
   166  		t.Run(name, func(t *testing.T) {
   167  			out := test.Input.Difference(test.Reviewers)
   168  			assert.Equal(t, test.Output.Users, out.Users, "incorrect users in difference")
   169  			assert.Equal(t, test.Output.Teams, out.Teams, "incorrect users in difference")
   170  		})
   171  	}
   172  }
   173  
   174  func TestSelectRandomUsers(t *testing.T) {
   175  	r := rand.New(rand.NewSource(42))
   176  
   177  	require.Len(t, selectRandomUsers(0, []string{"a"}, r), 0, "0 selection should return nothing")
   178  	require.Len(t, selectRandomUsers(1, []string{}, r), 0, "empty list should return nothing")
   179  
   180  	assert.Equal(t, []string{"a"}, selectRandomUsers(1, []string{"a"}, r))
   181  	assert.Equal(t, []string{"a", "b"}, selectRandomUsers(3, []string{"a", "b"}, r))
   182  
   183  	pseudoRandom := selectRandomUsers(1, []string{"a", "b", "c"}, r)
   184  	assert.Equal(t, []string{"c"}, pseudoRandom)
   185  
   186  	multiplePseudoRandom := selectRandomUsers(4, []string{"a", "b", "c", "d", "e", "f", "g"}, r)
   187  	assert.Equal(t, []string{"c", "e", "b", "f"}, multiplePseudoRandom)
   188  }
   189  
   190  func TestSelectReviewers(t *testing.T) {
   191  	r := rand.New(rand.NewSource(42))
   192  	results := []*common.Result{
   193  		{
   194  			Name:   "users",
   195  			Status: common.StatusPending,
   196  			ReviewRequestRule: &common.ReviewRequestRule{
   197  				Users:         []string{"mhaypenny", "review-approver", "contributor-committer"},
   198  				RequiredCount: 2,
   199  				Mode:          common.RequestModeRandomUsers,
   200  			},
   201  		},
   202  		{
   203  			Name:   "admin-users",
   204  			Status: common.StatusPending,
   205  			ReviewRequestRule: &common.ReviewRequestRule{
   206  				Permissions:   []pull.Permission{pull.PermissionAdmin},
   207  				RequiredCount: 1,
   208  				Mode:          common.RequestModeRandomUsers,
   209  			},
   210  		},
   211  	}
   212  
   213  	prctx := makeContext()
   214  
   215  	selection, err := SelectReviewers(context.Background(), prctx, results, r)
   216  	require.NoError(t, err)
   217  	require.Len(t, selection.Users, 3, "policy should request three people")
   218  	require.Contains(t, selection.Users, "review-approver", "at least review-approver must be selected")
   219  	require.NotContains(t, selection.Users, "mhaypenny", "the author cannot be requested")
   220  	require.NotContains(t, selection.Users, "not-a-collaborator", "a non collaborator cannot be requested")
   221  	require.NotContains(t, selection.Users, "org-owner", "org-owner should not be requested")
   222  }
   223  
   224  func TestSelectReviewers_UserPermission(t *testing.T) {
   225  	r := rand.New(rand.NewSource(42))
   226  	results := []*common.Result{
   227  		{
   228  			Name:   "user-permissions",
   229  			Status: common.StatusPending,
   230  			ReviewRequestRule: &common.ReviewRequestRule{
   231  				Permissions:   []pull.Permission{pull.PermissionTriage, pull.PermissionMaintain},
   232  				RequiredCount: 2,
   233  				Mode:          common.RequestModeAllUsers,
   234  			},
   235  		},
   236  	}
   237  
   238  	prctx := makeContext()
   239  
   240  	selection, err := SelectReviewers(context.Background(), prctx, results, r)
   241  	require.NoError(t, err)
   242  	require.Len(t, selection.Teams, 0, "policy should request no teams")
   243  
   244  	require.Len(t, selection.Users, 4, "policy should request two users")
   245  	require.Contains(t, selection.Users, "maintainer", "maintainer selected")
   246  	require.Contains(t, selection.Users, "triager", "triager selected")
   247  	require.Contains(t, selection.Users, "org-owner-team-maintainer", "triager selected")
   248  	require.Contains(t, selection.Users, "direct-write-team-maintainer", "triager selected")
   249  }
   250  
   251  func TestSelectReviewers_TeamPermission(t *testing.T) {
   252  	r := rand.New(rand.NewSource(42))
   253  	results := []*common.Result{
   254  		{
   255  			Name:   "team-permissions",
   256  			Status: common.StatusPending,
   257  			ReviewRequestRule: &common.ReviewRequestRule{
   258  				Permissions:   []pull.Permission{pull.PermissionAdmin, pull.PermissionMaintain},
   259  				RequiredCount: 1,
   260  				Mode:          common.RequestModeTeams,
   261  			},
   262  		},
   263  	}
   264  
   265  	prctx := makeContext()
   266  
   267  	selection, err := SelectReviewers(context.Background(), prctx, results, r)
   268  	require.NoError(t, err)
   269  	require.Len(t, selection.Teams, 2, "policy should request two teams")
   270  	require.Contains(t, selection.Teams, "team-admin", "admin team seleted")
   271  	require.Contains(t, selection.Teams, "team-maintain", "maintainer team seleted")
   272  
   273  	require.Len(t, selection.Users, 0, "policy should request no people")
   274  }
   275  
   276  func TestSelectReviewers_TeamMembers(t *testing.T) {
   277  	r := rand.New(rand.NewSource(42))
   278  	results := []*common.Result{
   279  		{
   280  			Name:   "team-users",
   281  			Status: common.StatusPending,
   282  			ReviewRequestRule: &common.ReviewRequestRule{
   283  				Teams:         []string{"everyone/team-write"},
   284  				RequiredCount: 1,
   285  				Mode:          common.RequestModeRandomUsers,
   286  			},
   287  		},
   288  	}
   289  
   290  	prctx := makeContext()
   291  	selection, err := SelectReviewers(context.Background(), prctx, results, r)
   292  	require.NoError(t, err)
   293  	require.Empty(t, selection.Teams, "no teams should be returned")
   294  	require.Len(t, selection.Users, 1, "policy should request one reviewer")
   295  	require.Contains(t, selection.Users, "user-team-write", "user-team-write must be selected")
   296  }
   297  
   298  func TestSelectReviewers_Team(t *testing.T) {
   299  	r := rand.New(rand.NewSource(42))
   300  	results := []*common.Result{
   301  		{
   302  			Name:   "Team",
   303  			Status: common.StatusPending,
   304  			ReviewRequestRule: &common.ReviewRequestRule{
   305  				Teams:         []string{"everyone/team-write", "everyone/team-not-collaborators"},
   306  				Users:         []string{"user-team-write"},
   307  				RequiredCount: 1,
   308  				Mode:          common.RequestModeTeams,
   309  			},
   310  		},
   311  	}
   312  
   313  	prctx := makeContext()
   314  	selection, err := SelectReviewers(context.Background(), prctx, results, r)
   315  	require.NoError(t, err)
   316  	require.Len(t, selection.Teams, 1, "one team should be returned")
   317  	require.Contains(t, selection.Teams, "team-write", "team-write should be selected")
   318  	require.Len(t, selection.Users, 0, "policy should request 0 users")
   319  }
   320  
   321  func TestSelectReviewers_TeamNotCollaborator(t *testing.T) {
   322  	r := rand.New(rand.NewSource(42))
   323  	results := []*common.Result{
   324  		{
   325  			Name:   "not-collaborators",
   326  			Status: common.StatusPending,
   327  			ReviewRequestRule: &common.ReviewRequestRule{
   328  				Teams:         []string{"everyone/team-not-collaborators"},
   329  				Users:         []string{"user-team-write"},
   330  				RequiredCount: 1,
   331  				Mode:          common.RequestModeTeams,
   332  			},
   333  		},
   334  	}
   335  
   336  	prctx := makeContext()
   337  	selection, err := SelectReviewers(context.Background(), prctx, results, r)
   338  	require.NoError(t, err)
   339  	require.Empty(t, selection.Teams, "no team should be returned")
   340  	require.Len(t, selection.Users, 0, "policy should request no people")
   341  }
   342  
   343  func TestSelectReviewers_Org(t *testing.T) {
   344  	r := rand.New(rand.NewSource(42))
   345  	results := []*common.Result{
   346  		{
   347  			Name:   "org",
   348  			Status: common.StatusPending,
   349  			ReviewRequestRule: &common.ReviewRequestRule{
   350  				Organizations: []string{"everyone"},
   351  				RequiredCount: 1,
   352  				Mode:          common.RequestModeRandomUsers,
   353  			},
   354  		},
   355  	}
   356  
   357  	prctx := makeContext()
   358  	selection, err := SelectReviewers(context.Background(), prctx, results, r)
   359  	require.NoError(t, err)
   360  	require.Len(t, selection.Users, 1, "policy should request one person")
   361  	require.Contains(t, selection.Users, "review-approver", "review-approver must be selected")
   362  }
   363  
   364  func makeContext() pull.Context {
   365  	return &pulltest.Context{
   366  		OwnerValue: "everyone",
   367  
   368  		AuthorValue:   "mhaypenny",
   369  		CommentsValue: []*pull.Comment{},
   370  		ReviewsValue:  []*pull.Review{},
   371  
   372  		OrgMemberships: map[string][]string{
   373  			"mhaypenny":             {"everyone"},
   374  			"contributor-author":    {"everyone"},
   375  			"contributor-committer": {"everyone"},
   376  			"comment-approver":      {"everyone", "cool-org"},
   377  			"review-approver":       {"everyone", "even-cooler-org"},
   378  		},
   379  		CollaboratorsValue: []*pull.Collaborator{
   380  			{
   381  				Name: "mhaypenny",
   382  				Permissions: []pull.CollaboratorPermission{
   383  					{Permission: pull.PermissionAdmin},
   384  				},
   385  			},
   386  			{
   387  				Name: "org-owner",
   388  				Permissions: []pull.CollaboratorPermission{
   389  					{Permission: pull.PermissionAdmin},
   390  				},
   391  			},
   392  			{
   393  				Name: "user-team-admin",
   394  				Permissions: []pull.CollaboratorPermission{
   395  					{Permission: pull.PermissionAdmin, ViaRepo: true},
   396  				},
   397  			},
   398  			{
   399  				Name: "user-direct-admin",
   400  				Permissions: []pull.CollaboratorPermission{
   401  					{Permission: pull.PermissionAdmin, ViaRepo: true},
   402  				},
   403  			},
   404  			{
   405  				Name: "user-team-write",
   406  				Permissions: []pull.CollaboratorPermission{
   407  					{Permission: pull.PermissionWrite, ViaRepo: true},
   408  				},
   409  			},
   410  			{
   411  				Name: "contributor-committer",
   412  				Permissions: []pull.CollaboratorPermission{
   413  					{Permission: pull.PermissionWrite},
   414  				},
   415  			},
   416  			{
   417  				Name: "contributor-author",
   418  				Permissions: []pull.CollaboratorPermission{
   419  					{Permission: pull.PermissionWrite},
   420  				},
   421  			},
   422  			{
   423  				Name: "review-approver",
   424  				Permissions: []pull.CollaboratorPermission{
   425  					{Permission: pull.PermissionWrite},
   426  				},
   427  			},
   428  			{
   429  				Name: "maintainer",
   430  				Permissions: []pull.CollaboratorPermission{
   431  					{Permission: pull.PermissionMaintain, ViaRepo: true},
   432  				},
   433  			},
   434  			{
   435  				// note: currently not possible in GitHub
   436  				Name: "indirect-maintainer",
   437  				Permissions: []pull.CollaboratorPermission{
   438  					{Permission: pull.PermissionMaintain},
   439  				},
   440  			},
   441  			{
   442  				Name: "triager",
   443  				Permissions: []pull.CollaboratorPermission{
   444  					{Permission: pull.PermissionTriage, ViaRepo: true},
   445  				},
   446  			},
   447  			{
   448  				// note: currently not possible in GitHub
   449  				Name: "indirect-triager",
   450  				Permissions: []pull.CollaboratorPermission{
   451  					{Permission: pull.PermissionTriage},
   452  				},
   453  			},
   454  			{
   455  				Name: "org-owner-team-maintainer",
   456  				Permissions: []pull.CollaboratorPermission{
   457  					{Permission: pull.PermissionAdmin, ViaRepo: false},
   458  					{Permission: pull.PermissionMaintain, ViaRepo: true},
   459  				},
   460  			},
   461  			{
   462  				Name: "direct-write-team-maintainer",
   463  				Permissions: []pull.CollaboratorPermission{
   464  					{Permission: pull.PermissionMaintain, ViaRepo: true},
   465  					{Permission: pull.PermissionWrite, ViaRepo: true},
   466  				},
   467  			},
   468  		},
   469  		TeamsValue: map[string]pull.Permission{
   470  			"team-write":    pull.PermissionWrite,
   471  			"team-admin":    pull.PermissionAdmin,
   472  			"team-maintain": pull.PermissionMaintain,
   473  		},
   474  		TeamMemberships: map[string][]string{
   475  			"user-team-admin":    {"everyone/team-admin"},
   476  			"user-team-write":    {"everyone/team-write"},
   477  			"maintainer":         {"everyone/team-maintain"},
   478  			"not-a-collaborator": {"everyone/team-not-collaborators"},
   479  		},
   480  	}
   481  }
   482  

View as plain text