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 }