sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/project/project_test.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package project implements the `/project` command which allows members of the project
    18  // maintainers team to specify a project to be applied to an Issue or PR.
    19  package project
    20  
    21  import (
    22  	"fmt"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/sirupsen/logrus"
    27  	"sigs.k8s.io/prow/pkg/github"
    28  	"sigs.k8s.io/prow/pkg/github/fakegithub"
    29  	"sigs.k8s.io/prow/pkg/plugins"
    30  )
    31  
    32  func TestProjectCommand(t *testing.T) {
    33  	projectColumnsMap := map[string][]github.ProjectColumn{
    34  		"0.0.0": {
    35  			github.ProjectColumn{
    36  				Name: "To do",
    37  				ID:   00000,
    38  			},
    39  			github.ProjectColumn{
    40  				Name: "Backlog",
    41  				ID:   00001,
    42  			},
    43  		},
    44  		"0.1.0": {
    45  			github.ProjectColumn{
    46  				Name: "To do",
    47  				ID:   00002,
    48  			},
    49  			github.ProjectColumn{
    50  				Name: "Backlog",
    51  				ID:   00003,
    52  			},
    53  		},
    54  	}
    55  	repoProjects := map[string][]github.Project{
    56  		"kubernetes/*": {
    57  			github.Project{
    58  				Name: "0.0.0",
    59  				ID:   000,
    60  			},
    61  		},
    62  		"kubernetes/kubernetes": {
    63  			github.Project{
    64  				Name: "0.1.0",
    65  				ID:   010,
    66  			},
    67  		},
    68  	}
    69  	// Maps github project name to maps of column IDs to column string
    70  	columnIDMap := map[string]map[int]string{
    71  		"0.0.0": {
    72  			00000: "To do",
    73  			00001: "Backlog",
    74  		},
    75  		"0.1.0": {
    76  			00002: "To do",
    77  			00003: "Backlog",
    78  		},
    79  	}
    80  
    81  	projectConfig := plugins.ProjectConfig{
    82  		// The team ID is set to 0 (or 42) in order to match the teams returned by FakeClient's method ListTeamMembers
    83  		Orgs: map[string]plugins.ProjectOrgConfig{
    84  			"kubernetes": {
    85  				MaintainerTeamID: 0,
    86  				ProjectColumnMap: map[string]string{
    87  					"0.0.0": "Backlog",
    88  				},
    89  				Repos: map[string]plugins.ProjectRepoConfig{
    90  					"kubernetes": {
    91  						MaintainerTeamID: 42,
    92  						ProjectColumnMap: map[string]string{
    93  							"0.1.0": "To do",
    94  						},
    95  					},
    96  					"community": {
    97  						MaintainerTeamID: 0,
    98  						ProjectColumnMap: map[string]string{
    99  							"0.1.0": "does not exist column",
   100  						},
   101  					},
   102  				},
   103  			},
   104  		},
   105  	}
   106  
   107  	type testCase struct {
   108  		name            string
   109  		action          github.GenericCommentEventAction
   110  		noAction        bool
   111  		body            string
   112  		repo            string
   113  		org             string
   114  		commenter       string
   115  		previousProject string
   116  		previousColumn  string
   117  		expectedProject string
   118  		expectedColumn  string
   119  		expectedComment string
   120  	}
   121  
   122  	testcases := []testCase{
   123  		{
   124  			name:            "Setting project and column with valid values, but commenter does not belong to the project maintainer team",
   125  			action:          github.GenericCommentActionCreated,
   126  			body:            "/project 0.0.0 To do",
   127  			repo:            "kubernetes",
   128  			org:             "kubernetes",
   129  			commenter:       "random-user",
   130  			previousProject: "",
   131  			previousColumn:  "",
   132  			expectedProject: "",
   133  			expectedColumn:  "",
   134  			expectedComment: "@random-user: " + fmt.Sprintf(notATeamMemberMsg, "kubernetes", "kubernetes", "kubernetes", "kubernetes"),
   135  		},
   136  		{
   137  			name:            "Setting project and column with valid values; project card does not currently exist for this issue/PR in the project",
   138  			action:          github.GenericCommentActionCreated,
   139  			body:            "/project 0.0.0 To do",
   140  			repo:            "kubernetes",
   141  			org:             "kubernetes",
   142  			commenter:       "sig-lead",
   143  			previousProject: "",
   144  			previousColumn:  "",
   145  			expectedProject: "0.0.0",
   146  			expectedColumn:  "To do",
   147  		},
   148  		{
   149  			name:            "Setting project and column with valid values; project card already exist for this issue/PR in the project, but the project card is under a different column",
   150  			action:          github.GenericCommentActionCreated,
   151  			body:            "/project 0.0.0 To do",
   152  			repo:            "kubernetes",
   153  			org:             "kubernetes",
   154  			commenter:       "sig-lead",
   155  			previousProject: "",
   156  			previousColumn:  "",
   157  			expectedProject: "0.0.0",
   158  			expectedColumn:  "To do",
   159  		},
   160  		{
   161  			name:            "Setting project without column value; the project specified exists on the repo level; the default column is set on the project and it exists on the project",
   162  			action:          github.GenericCommentActionCreated,
   163  			body:            "/project 0.1.0",
   164  			repo:            "kubernetes",
   165  			org:             "kubernetes",
   166  			commenter:       "sig-lead",
   167  			previousProject: "0.0.0",
   168  			previousColumn:  "Backlog",
   169  			expectedProject: "0.1.0",
   170  			expectedColumn:  "To do",
   171  		},
   172  		{
   173  			name:            "Setting project without column value; the project specified exists on the org level; the default column is set on the project and it exists on the project",
   174  			action:          github.GenericCommentActionCreated,
   175  			body:            "/project 0.0.0",
   176  			repo:            "kubernetes",
   177  			org:             "kubernetes",
   178  			commenter:       "sig-lead",
   179  			previousProject: "",
   180  			previousColumn:  "",
   181  			expectedProject: "0.0.0",
   182  			expectedColumn:  "Backlog",
   183  		},
   184  		{
   185  			name:            "Setting project without column value; the default column is set on the project but it does not exist on the project",
   186  			action:          github.GenericCommentActionCreated,
   187  			body:            "/project 0.1.0",
   188  			repo:            "community",
   189  			org:             "kubernetes",
   190  			commenter:       "default-sig-lead",
   191  			previousProject: "0.0.0",
   192  			previousColumn:  "Backlog",
   193  			expectedProject: "0.0.0",
   194  			expectedColumn:  "Backlog",
   195  			expectedComment: "@default-sig-lead: " + fmt.Sprintf(invalidColumn, "0.1.0", []string{"To do", "Backlog"}),
   196  		},
   197  		{
   198  			name:            "Setting project with invalid column value; an error will be returned",
   199  			action:          github.GenericCommentActionCreated,
   200  			body:            "/project 0.1.0 Random 2",
   201  			repo:            "kubernetes",
   202  			org:             "kubernetes",
   203  			commenter:       "sig-lead",
   204  			previousProject: "",
   205  			previousColumn:  "",
   206  			expectedProject: "",
   207  			expectedColumn:  "",
   208  			expectedComment: "@sig-lead: " + fmt.Sprintf(invalidColumn, "0.1.0", []string{"To do", "Backlog"}),
   209  		},
   210  		{
   211  			name:            "Clearing project for a issue/PR; the project name provided is valid",
   212  			action:          github.GenericCommentActionCreated,
   213  			body:            "/project clear 0.0.0",
   214  			repo:            "kubernetes",
   215  			org:             "kubernetes",
   216  			commenter:       "sig-lead",
   217  			previousProject: "0.0.0",
   218  			previousColumn:  "Backlog",
   219  			expectedProject: "0.0.0",
   220  			expectedColumn:  "Backlog",
   221  		},
   222  		{
   223  			name:            "Setting project with invalid project name",
   224  			action:          github.GenericCommentActionCreated,
   225  			body:            "/project invalidprojectname",
   226  			repo:            "community",
   227  			org:             "kubernetes",
   228  			commenter:       "default-sig-lead",
   229  			previousProject: "",
   230  			previousColumn:  "",
   231  			expectedProject: "",
   232  			expectedColumn:  "",
   233  			expectedComment: "@default-sig-lead: " + fmt.Sprintf(invalidProject, "`0.0.0`, `0.1.0`"),
   234  		},
   235  		{
   236  			name:            "Clearing project for a issue/PR; the project name provided is invalid",
   237  			action:          github.GenericCommentActionCreated,
   238  			body:            "/project clear invalidprojectname",
   239  			repo:            "kubernetes",
   240  			org:             "kubernetes",
   241  			commenter:       "sig-lead",
   242  			previousProject: "0.1.0",
   243  			previousColumn:  "To do",
   244  			expectedProject: "0.1.0",
   245  			expectedColumn:  "To do",
   246  			expectedComment: "@sig-lead: " + fmt.Sprintf(invalidProject, "`0.0.0`, `0.1.0`"),
   247  		},
   248  		{
   249  			name:            "Clearing project for a issue/PR; the project does not contain the card",
   250  			action:          github.GenericCommentActionCreated,
   251  			body:            "/project clear 0.1.0",
   252  			repo:            "kubernetes",
   253  			org:             "kubernetes",
   254  			commenter:       "sig-lead",
   255  			previousProject: "0.1.0",
   256  			previousColumn:  "To do",
   257  			expectedProject: "0.1.0",
   258  			expectedColumn:  "To do",
   259  			expectedComment: "@sig-lead: " + fmt.Sprintf(failedClearingProjectMsg, "0.1.0", "1"),
   260  		},
   261  		{
   262  			name:            "No action on events that are not new comments",
   263  			action:          github.GenericCommentActionEdited,
   264  			body:            "/project 0.0.0 To do",
   265  			repo:            "kubernetes",
   266  			org:             "kubernetes",
   267  			commenter:       "sig-lead",
   268  			previousProject: "",
   269  			previousColumn:  "",
   270  			expectedProject: "",
   271  			expectedColumn:  "",
   272  			noAction:        true,
   273  		},
   274  		{
   275  			name:            "No action on bot comments",
   276  			action:          github.GenericCommentActionCreated,
   277  			body:            "/project 0.0.0 To do",
   278  			repo:            "kubernetes",
   279  			org:             "kubernetes",
   280  			commenter:       fakegithub.Bot,
   281  			previousProject: "",
   282  			previousColumn:  "",
   283  			expectedProject: "",
   284  			expectedColumn:  "",
   285  			noAction:        true,
   286  		},
   287  		{
   288  			name:            "No action on non-matching comments",
   289  			action:          github.GenericCommentActionCreated,
   290  			body:            "random comment",
   291  			repo:            "kubernetes",
   292  			org:             "kubernetes",
   293  			commenter:       "sig-lead",
   294  			previousProject: "",
   295  			previousColumn:  "",
   296  			expectedProject: "",
   297  			expectedColumn:  "",
   298  			noAction:        true,
   299  		},
   300  	}
   301  
   302  	fakeClient := fakegithub.NewFakeClient()
   303  	fakeClient.RepoProjects = repoProjects
   304  	fakeClient.ProjectColumnsMap = projectColumnsMap
   305  	fakeClient.ColumnIDMap = columnIDMap
   306  
   307  	prevCommentCount := 0
   308  	for _, tc := range testcases {
   309  		tc := tc
   310  		fakeClient.Project = tc.previousProject
   311  		fakeClient.Column = tc.previousColumn
   312  		fakeClient.ColumnCardsMap = map[int][]github.ProjectCard{}
   313  
   314  		e := &github.GenericCommentEvent{
   315  			Action:       tc.action,
   316  			Body:         tc.body,
   317  			Number:       1,
   318  			IssueHTMLURL: "1",
   319  			Repo:         github.Repo{Owner: github.User{Login: tc.org}, Name: tc.repo},
   320  			User:         github.User{Login: tc.commenter},
   321  		}
   322  		if err := handle(fakeClient, logrus.WithField("plugin", pluginName), e, projectConfig); err != nil {
   323  			t.Errorf("(%s): Unexpected error from handle: %v.", tc.name, err)
   324  			continue
   325  		}
   326  		if fakeClient.Project != tc.expectedProject {
   327  			t.Errorf("(%s): Unexpected project %s but got %s", tc.name, tc.expectedProject, fakeClient.Project)
   328  		}
   329  		if fakeClient.Column != tc.expectedColumn {
   330  			t.Errorf("(%s): Unexpected column %s but got %s", tc.name, tc.expectedColumn, fakeClient.Column)
   331  		}
   332  		issueComments := fakeClient.IssueComments[e.Number]
   333  		if tc.expectedComment != "" {
   334  			actualComment := issueComments[len(issueComments)-1].Body
   335  			// Only check for substring because the actual comment contains a lot of extra stuff
   336  			if !strings.Contains(actualComment, tc.expectedComment) {
   337  				t.Errorf("(%s): Unexpected comment\n%s\nbut got\n%s", tc.name, tc.expectedComment, actualComment)
   338  			}
   339  		}
   340  		if tc.noAction {
   341  			if len(issueComments) != prevCommentCount {
   342  				t.Errorf("(%s): No new comment should be created", tc.name)
   343  			}
   344  		}
   345  		prevCommentCount = len(issueComments)
   346  	}
   347  }
   348  
   349  func TestParseCommand(t *testing.T) {
   350  	var testcases = []struct {
   351  		hasMatches      bool
   352  		command         string
   353  		proposedProject string
   354  		proposedColumn  string
   355  		shouldClear     bool
   356  	}{
   357  		{
   358  			hasMatches:      true,
   359  			command:         "/project 0.0.0 To do",
   360  			proposedProject: "0.0.0",
   361  			proposedColumn:  "To do",
   362  			shouldClear:     false,
   363  		},
   364  		{
   365  			hasMatches:      true,
   366  			command:         "/project 0.0.0 Backlog",
   367  			proposedProject: "0.0.0",
   368  			proposedColumn:  "Backlog",
   369  			shouldClear:     false,
   370  		},
   371  		{
   372  			hasMatches:      true,
   373  			command:         "/project clear 0.0.0",
   374  			proposedProject: "0.0.0",
   375  			proposedColumn:  "",
   376  			shouldClear:     true,
   377  		},
   378  		{
   379  			hasMatches:      true,
   380  			command:         "/project clear 0.0.0 Backlog",
   381  			proposedProject: "0.0.0",
   382  			proposedColumn:  "Backlog",
   383  			shouldClear:     true,
   384  		},
   385  		{
   386  			hasMatches:      true,
   387  			command:         "/project clear",
   388  			proposedProject: "",
   389  			proposedColumn:  "",
   390  			shouldClear:     false,
   391  		},
   392  		{
   393  			hasMatches:      true,
   394  			command:         "/project 0.0.0",
   395  			proposedProject: "0.0.0",
   396  			proposedColumn:  "",
   397  			shouldClear:     false,
   398  		},
   399  		{
   400  			hasMatches:      true,
   401  			command:         "/project '0.0.0'",
   402  			proposedProject: "0.0.0",
   403  			proposedColumn:  "",
   404  			shouldClear:     false,
   405  		},
   406  		{
   407  			hasMatches:      true,
   408  			command:         "/project \"0.0.0\"",
   409  			proposedProject: "0.0.0",
   410  			proposedColumn:  "",
   411  			shouldClear:     false,
   412  		},
   413  		{
   414  			hasMatches:      true,
   415  			command:         "/project '0.0.0' To do",
   416  			proposedProject: "0.0.0",
   417  			proposedColumn:  "To do",
   418  			shouldClear:     false,
   419  		},
   420  		{
   421  			hasMatches:      true,
   422  			command:         "/project '0.0.0' \"To do\"",
   423  			proposedProject: "0.0.0",
   424  			proposedColumn:  "To do",
   425  			shouldClear:     false,
   426  		},
   427  		{
   428  			hasMatches:      true,
   429  			command:         "/project 'something 0.0.0' \"To do\"",
   430  			proposedProject: "something 0.0.0",
   431  			proposedColumn:  "To do",
   432  			shouldClear:     false,
   433  		},
   434  		{
   435  			hasMatches:      true,
   436  			command:         "/project clear '0.0.0' \"To do\"",
   437  			proposedProject: "0.0.0",
   438  			proposedColumn:  "To do",
   439  			shouldClear:     true,
   440  		},
   441  		{
   442  			hasMatches:      true,
   443  			command:         "/project clear 'something 0.0.0' \"To do\"",
   444  			proposedProject: "something 0.0.0",
   445  			proposedColumn:  "To do",
   446  			shouldClear:     true,
   447  		},
   448  		{
   449  			hasMatches:      false,
   450  			command:         "/project",
   451  			proposedProject: "",
   452  			proposedColumn:  "",
   453  			shouldClear:     false,
   454  		},
   455  		{
   456  			hasMatches: false,
   457  			command:    "random comment",
   458  		},
   459  	}
   460  
   461  	for _, test := range testcases {
   462  		matches := projectRegex.FindStringSubmatch(test.command)
   463  		if !test.hasMatches {
   464  			if len(matches) > 0 {
   465  				t.Errorf("For command %s, project command regex should not match", test.command)
   466  			}
   467  			continue
   468  		}
   469  		proposedProject, proposedColumn, shouldClear, _ := processCommand(matches[1])
   470  		if proposedProject != test.proposedProject ||
   471  			proposedColumn != test.proposedColumn ||
   472  			shouldClear != test.shouldClear {
   473  			t.Errorf("\nFor command %s, expected\n  proposedProject = %s\n  proposedColumn = %s\n  shouldClear = %t\nbut got\n  proposedProject = %s\n  proposedColumn = %s\n  shouldClear = %t\n", test.command, test.proposedProject, test.proposedColumn, test.shouldClear, proposedProject, proposedColumn, shouldClear)
   474  		}
   475  	}
   476  }
   477  
   478  func TestGetProjectConfigs(t *testing.T) {
   479  	var testcases = []struct {
   480  		org                      string
   481  		repo                     string
   482  		expectedMaintainerTeamID int
   483  	}{
   484  		{
   485  			org:                      "kubernetes",
   486  			repo:                     "kubernetes",
   487  			expectedMaintainerTeamID: 42,
   488  		},
   489  		{
   490  			org:                      "kubernetes",
   491  			repo:                     "community",
   492  			expectedMaintainerTeamID: 11,
   493  		},
   494  		{
   495  			org:                      "kubernetes-sigs",
   496  			repo:                     "kubespray",
   497  			expectedMaintainerTeamID: 10,
   498  		},
   499  		{
   500  			org:                      "kubernetes-sigs",
   501  			repo:                     "kind",
   502  			expectedMaintainerTeamID: 0,
   503  		},
   504  	}
   505  	projectConfig := plugins.ProjectConfig{
   506  		Orgs: map[string]plugins.ProjectOrgConfig{
   507  			"kubernetes": {
   508  				MaintainerTeamID: 11,
   509  				Repos: map[string]plugins.ProjectRepoConfig{
   510  					"kubernetes": {
   511  						MaintainerTeamID: 42,
   512  					},
   513  				},
   514  			},
   515  			"kubeflow": {
   516  				MaintainerTeamID: 20,
   517  			},
   518  			"kubernetes-sigs": {
   519  				Repos: map[string]plugins.ProjectRepoConfig{
   520  					"kubespray": {
   521  						MaintainerTeamID: 10,
   522  					},
   523  					"kind": {},
   524  				},
   525  			},
   526  		},
   527  	}
   528  
   529  	for _, tc := range testcases {
   530  		maintainerTeamID := projectConfig.GetMaintainerTeam(tc.org, tc.repo)
   531  		if maintainerTeamID != tc.expectedMaintainerTeamID {
   532  			t.Errorf("\nFor %s/%s, expected maintainer team ID %d but got ID %d", tc.org, tc.repo, tc.expectedMaintainerTeamID, maintainerTeamID)
   533  		}
   534  	}
   535  }