sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/welcome/welcome_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 welcome
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"regexp"
    23  	"testing"
    24  
    25  	"github.com/sirupsen/logrus"
    26  
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	"sigs.k8s.io/prow/pkg/config"
    29  	"sigs.k8s.io/prow/pkg/github"
    30  	"sigs.k8s.io/prow/pkg/plugins"
    31  )
    32  
    33  const (
    34  	testWelcomeTemplate = "Welcome human! 🤖 {{.AuthorName}} {{.AuthorLogin}} {{.Repo}} {{.Org}}}"
    35  )
    36  
    37  type fakeClient struct {
    38  	commentsAdded map[int][]string
    39  	prs           map[string]sets.Set[int]
    40  
    41  	// orgMembers maps org name to a list of member names.
    42  	orgMembers map[string][]string
    43  
    44  	// collaborators is a list of collaborators names.
    45  	collaborators []string
    46  }
    47  
    48  func newFakeClient() *fakeClient {
    49  	return &fakeClient{
    50  		commentsAdded: make(map[int][]string),
    51  		prs:           make(map[string]sets.Set[int]),
    52  		orgMembers:    make(map[string][]string),
    53  	}
    54  }
    55  
    56  func (fc *fakeClient) BotUserChecker() (func(candidate string) bool, error) {
    57  	return func(_ string) bool { return false }, nil
    58  }
    59  
    60  // CreateComment adds and tracks a comment in the client
    61  func (fc *fakeClient) CreateComment(owner, repo string, number int, comment string) error {
    62  	fc.commentsAdded[number] = append(fc.commentsAdded[number], comment)
    63  	return nil
    64  }
    65  
    66  // ClearComments removes all comments in the client
    67  func (fc *fakeClient) ClearComments() {
    68  	fc.commentsAdded = map[int][]string{}
    69  }
    70  
    71  // NumComments counts the number of tracked comments
    72  func (fc *fakeClient) NumComments() int {
    73  	n := 0
    74  	for _, comments := range fc.commentsAdded {
    75  		n += len(comments)
    76  	}
    77  	return n
    78  }
    79  
    80  // IsMember returns true if user is in org.
    81  func (fc *fakeClient) IsMember(org, user string) (bool, error) {
    82  	for _, m := range fc.orgMembers[org] {
    83  		if m == user {
    84  			return true, nil
    85  		}
    86  	}
    87  	return false, nil
    88  }
    89  
    90  // IsCollaborator returns true if the user is a collaborator of the repo.
    91  func (fc *fakeClient) IsCollaborator(org, repo, login string) (bool, error) {
    92  	for _, collab := range fc.collaborators {
    93  		if collab == login {
    94  			return true, nil
    95  		}
    96  	}
    97  	return false, nil
    98  }
    99  
   100  func (fc *fakeClient) addOrgMember(org, user string) {
   101  	fc.orgMembers[org] = append(fc.orgMembers[org], user)
   102  }
   103  
   104  func (fc *fakeClient) addCollaborator(user string) {
   105  	fc.collaborators = append(fc.collaborators, user)
   106  }
   107  
   108  var (
   109  	expectedQueryRegex = regexp.MustCompile(`is:pr repo:(.+)/(.+) author:(.+)`)
   110  )
   111  
   112  // AddPR records an PR in the client
   113  func (fc *fakeClient) AddPR(owner, repo string, author github.User, number int) {
   114  	key := fmt.Sprintf("%s,%s,%s", github.NormLogin(owner), github.NormLogin(repo), github.NormLogin(author.Login))
   115  	if _, ok := fc.prs[key]; !ok {
   116  		fc.prs[key] = sets.Set[int]{}
   117  	}
   118  	fc.prs[key].Insert(number)
   119  }
   120  
   121  // ClearPRs removes all PRs from the client
   122  func (fc *fakeClient) ClearPRs() {
   123  	fc.prs = make(map[string]sets.Set[int])
   124  }
   125  
   126  // FindIssuesWithOrg fails if the query does not match the expected query regex and
   127  // looks up issues based on parsing the expected query format
   128  func (fc *fakeClient) FindIssuesWithOrg(org, query, sort string, asc bool) ([]github.Issue, error) {
   129  	if org == "" {
   130  		return nil, errors.New("passing an empty organization is highly discouraged, as it's incompatible with GitHub Apps")
   131  	}
   132  	fields := expectedQueryRegex.FindStringSubmatch(query)
   133  	if fields == nil || len(fields) != 4 {
   134  		return nil, fmt.Errorf("invalid query: `%s` does not match expected regex `%s`", query, expectedQueryRegex.String())
   135  	}
   136  	// "find" results
   137  	owner, repo, author := fields[1], fields[2], fields[3]
   138  	key := fmt.Sprintf("%s,%s,%s", github.NormLogin(owner), github.NormLogin(repo), github.NormLogin(author))
   139  
   140  	issues := []github.Issue{}
   141  	for _, number := range sets.List(fc.prs[key]) {
   142  		issues = append(issues, github.Issue{
   143  			Number: number,
   144  		})
   145  	}
   146  	return issues, nil
   147  }
   148  
   149  func makeFakePullRequestEvent(owner, repo string, user github.User, number int, action github.PullRequestEventAction) github.PullRequestEvent {
   150  	return github.PullRequestEvent{
   151  		Action: action,
   152  		Number: number,
   153  		PullRequest: github.PullRequest{
   154  			Base: github.PullRequestBranch{
   155  				Repo: github.Repo{
   156  					Owner: github.User{
   157  						Login: owner,
   158  					},
   159  					Name: repo,
   160  				},
   161  			},
   162  			User: user,
   163  		},
   164  	}
   165  }
   166  
   167  func TestHandlePR(t *testing.T) {
   168  	fc := newFakeClient()
   169  
   170  	newContributor := github.User{
   171  		Login: "newContributor",
   172  		Name:  "newContributor fullname",
   173  		Type:  github.UserTypeUser,
   174  	}
   175  	contributorA := github.User{
   176  		Login: "contributorA",
   177  		Name:  "contributorA fullname",
   178  		Type:  github.UserTypeUser,
   179  	}
   180  	contributorB := github.User{
   181  		Login: "contributorB",
   182  		Name:  "contributorB fullname",
   183  		Type:  github.UserTypeUser,
   184  	}
   185  	member := github.User{
   186  		Login: "member",
   187  		Name:  "Member Member",
   188  		Type:  github.UserTypeUser,
   189  	}
   190  	collaborator := github.User{
   191  		Login: "collab",
   192  		Name:  "Collab Collab",
   193  		Type:  github.UserTypeUser,
   194  	}
   195  	robot := github.User{
   196  		Login: "robot",
   197  		Name:  "robot fullname",
   198  		Type:  github.UserTypeBot,
   199  	}
   200  
   201  	// old PRs
   202  	fc.AddPR("kubernetes", "test-infra", contributorA, 1)
   203  	fc.AddPR("kubernetes", "test-infra", contributorB, 2)
   204  	fc.AddPR("kubernetes", "test-infra", contributorB, 3)
   205  
   206  	// members & collaborators
   207  	fc.addOrgMember("kubernetes", member.Login)
   208  	fc.addCollaborator(collaborator.Login)
   209  
   210  	testCases := []struct {
   211  		name           string
   212  		repoOwner      string
   213  		repoName       string
   214  		author         github.User
   215  		prNumber       int
   216  		prAction       github.PullRequestEventAction
   217  		addPR          bool
   218  		alwaysPost     bool
   219  		onlyOrgMembers bool
   220  		expectComment  bool
   221  	}{
   222  		{
   223  			name:           "existing contributorA",
   224  			repoOwner:      "kubernetes",
   225  			repoName:       "test-infra",
   226  			author:         contributorA,
   227  			prNumber:       20,
   228  			prAction:       github.PullRequestActionOpened,
   229  			alwaysPost:     false,
   230  			onlyOrgMembers: false,
   231  			expectComment:  false,
   232  		},
   233  		{
   234  			name:           "existing contributorB",
   235  			repoOwner:      "kubernetes",
   236  			repoName:       "test-infra",
   237  			author:         contributorB,
   238  			prNumber:       40,
   239  			prAction:       github.PullRequestActionOpened,
   240  			alwaysPost:     false,
   241  			onlyOrgMembers: false,
   242  			expectComment:  false,
   243  		},
   244  		{
   245  			name:           "existing contributor when it should greet everyone",
   246  			repoOwner:      "kubernetes",
   247  			repoName:       "test-infra",
   248  			author:         contributorB,
   249  			prNumber:       40,
   250  			prAction:       github.PullRequestActionOpened,
   251  			alwaysPost:     true,
   252  			onlyOrgMembers: false,
   253  			expectComment:  true,
   254  		},
   255  		{
   256  			name:           "new contributor",
   257  			repoOwner:      "kubernetes",
   258  			repoName:       "test-infra",
   259  			author:         newContributor,
   260  			prAction:       github.PullRequestActionOpened,
   261  			prNumber:       50,
   262  			alwaysPost:     false,
   263  			onlyOrgMembers: false,
   264  			expectComment:  true,
   265  		},
   266  		{
   267  			name:           "new contributor when it should greet everyone",
   268  			repoOwner:      "kubernetes",
   269  			repoName:       "test-infra",
   270  			author:         newContributor,
   271  			prAction:       github.PullRequestActionOpened,
   272  			prNumber:       50,
   273  			alwaysPost:     true,
   274  			onlyOrgMembers: false,
   275  			expectComment:  true,
   276  		},
   277  		{
   278  			name:           "new contributor and API recorded PR already",
   279  			repoOwner:      "kubernetes",
   280  			repoName:       "test-infra",
   281  			author:         newContributor,
   282  			prAction:       github.PullRequestActionOpened,
   283  			prNumber:       50,
   284  			expectComment:  true,
   285  			alwaysPost:     false,
   286  			onlyOrgMembers: false,
   287  			addPR:          true,
   288  		},
   289  		{
   290  			name:           "new contributor, not PR open event",
   291  			repoOwner:      "kubernetes",
   292  			repoName:       "test-infra",
   293  			author:         newContributor,
   294  			prAction:       github.PullRequestActionEdited,
   295  			prNumber:       50,
   296  			alwaysPost:     false,
   297  			onlyOrgMembers: false,
   298  			expectComment:  false,
   299  		},
   300  		{
   301  			name:           "new contributor, but is a bot",
   302  			repoOwner:      "kubernetes",
   303  			repoName:       "test-infra",
   304  			author:         robot,
   305  			prAction:       github.PullRequestActionOpened,
   306  			prNumber:       500,
   307  			alwaysPost:     false,
   308  			onlyOrgMembers: false,
   309  			expectComment:  false,
   310  		},
   311  		{
   312  			name:           "new contribution from the org member",
   313  			repoOwner:      "kubernetes",
   314  			repoName:       "test-infra",
   315  			author:         member,
   316  			prNumber:       101,
   317  			prAction:       github.PullRequestActionOpened,
   318  			alwaysPost:     false,
   319  			onlyOrgMembers: false,
   320  			expectComment:  false,
   321  		},
   322  		{
   323  			name:           "new contribution from collaborator",
   324  			repoOwner:      "kubernetes",
   325  			repoName:       "test-infra",
   326  			author:         collaborator,
   327  			prNumber:       102,
   328  			prAction:       github.PullRequestActionOpened,
   329  			alwaysPost:     false,
   330  			onlyOrgMembers: false,
   331  			expectComment:  false,
   332  		},
   333  		{
   334  			name:           "contribution from org member when it should greet everyone",
   335  			repoOwner:      "kubernetes",
   336  			repoName:       "test-infra",
   337  			author:         member,
   338  			prNumber:       40,
   339  			prAction:       github.PullRequestActionOpened,
   340  			alwaysPost:     true,
   341  			onlyOrgMembers: true,
   342  			expectComment:  true,
   343  		},
   344  	}
   345  
   346  	for _, tc := range testCases {
   347  		c := client{
   348  			GitHubClient: fc,
   349  			Logger:       logrus.WithField("testcase", tc.name),
   350  		}
   351  
   352  		// clear out comments from the last test case
   353  		fc.ClearComments()
   354  
   355  		event := makeFakePullRequestEvent(tc.repoOwner, tc.repoName, tc.author, tc.prNumber, tc.prAction)
   356  		if tc.addPR {
   357  			// make sure the PR in the event is recorded
   358  			fc.AddPR(tc.repoOwner, tc.repoName, tc.author, tc.prNumber)
   359  		}
   360  
   361  		tr := plugins.Trigger{
   362  			TrustedOrg:     "kubernetes",
   363  			OnlyOrgMembers: tc.onlyOrgMembers,
   364  		}
   365  
   366  		// try handling it
   367  		if err := handlePR(c, tr, event, testWelcomeTemplate, tc.alwaysPost); err != nil {
   368  			t.Fatalf("did not expect error handling PR for case '%s': %v", tc.name, err)
   369  		}
   370  
   371  		// verify that comments were made
   372  		numComments := fc.NumComments()
   373  		if numComments > 1 {
   374  			t.Fatalf("did not expect multiple comments for any test case and got %d comments", numComments)
   375  		}
   376  		if numComments == 0 && tc.expectComment {
   377  			t.Fatalf("expected a comment for case '%s' and got none", tc.name)
   378  		} else if numComments > 0 && !tc.expectComment {
   379  			t.Fatalf("did not expect comments for case '%s' and got %d comments", tc.name, numComments)
   380  		}
   381  	}
   382  }
   383  
   384  func TestWelcomeConfig(t *testing.T) {
   385  	var (
   386  		orgMessage  = "defined message for an org"
   387  		repoMessage = "defined message for a repo"
   388  	)
   389  
   390  	config := &plugins.Configuration{
   391  		Welcome: []plugins.Welcome{
   392  			{
   393  				Repos:           []string{"kubernetes/test-infra"},
   394  				MessageTemplate: repoMessage,
   395  			},
   396  			{
   397  				Repos:           []string{"kubernetes"},
   398  				MessageTemplate: orgMessage,
   399  			},
   400  			{
   401  				Repos:           []string{"kubernetes/repo-infra"},
   402  				MessageTemplate: repoMessage,
   403  			},
   404  		},
   405  	}
   406  
   407  	testCases := []struct {
   408  		name            string
   409  		repo            string
   410  		org             string
   411  		expectedMessage string
   412  	}{
   413  		{
   414  			name:            "default message",
   415  			org:             "kubernetes-sigs",
   416  			repo:            "kind",
   417  			expectedMessage: defaultWelcomeMessage,
   418  		},
   419  		{
   420  			name:            "org defined message",
   421  			org:             "kubernetes",
   422  			repo:            "community",
   423  			expectedMessage: orgMessage,
   424  		},
   425  		{
   426  			name:            "repo defined message, before an org",
   427  			org:             "kubernetes",
   428  			repo:            "test-infra",
   429  			expectedMessage: repoMessage,
   430  		},
   431  		{
   432  			name:            "repo defined message, after an org",
   433  			org:             "kubernetes",
   434  			repo:            "repo-infra",
   435  			expectedMessage: repoMessage,
   436  		},
   437  	}
   438  
   439  	for _, tc := range testCases {
   440  		receivedMessage := welcomeMessageForRepo(optionsForRepo(config, tc.org, tc.repo))
   441  		if receivedMessage != tc.expectedMessage {
   442  			t.Fatalf("%s: expected to get '%s' and received '%s'", tc.name, tc.expectedMessage, receivedMessage)
   443  		}
   444  	}
   445  }
   446  
   447  func TestHelpProvider(t *testing.T) {
   448  	enabledRepos := []config.OrgRepo{
   449  		{Org: "org1", Repo: "repo"},
   450  		{Org: "org2", Repo: "repo"},
   451  	}
   452  	cases := []struct {
   453  		name         string
   454  		config       *plugins.Configuration
   455  		enabledRepos []config.OrgRepo
   456  		err          bool
   457  	}{
   458  		{
   459  			name:         "Empty config",
   460  			config:       &plugins.Configuration{},
   461  			enabledRepos: enabledRepos,
   462  		},
   463  		{
   464  			name: "All configs enabled",
   465  			config: &plugins.Configuration{
   466  				Welcome: []plugins.Welcome{
   467  					{
   468  						Repos:           []string{"org2/repo"},
   469  						MessageTemplate: "Hello, welcome!",
   470  					},
   471  				},
   472  			},
   473  			enabledRepos: enabledRepos,
   474  		},
   475  	}
   476  	for _, c := range cases {
   477  		t.Run(c.name, func(t *testing.T) {
   478  			_, err := helpProvider(c.config, c.enabledRepos)
   479  			if err != nil && !c.err {
   480  				t.Fatalf("helpProvider error: %v", err)
   481  			}
   482  		})
   483  	}
   484  }