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 }