sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/jira/jira_test.go (about) 1 /* 2 Copyright 2020 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 jira 18 19 import ( 20 "errors" 21 "reflect" 22 "testing" 23 24 "github.com/andygrunwald/go-jira" 25 "github.com/google/go-cmp/cmp" 26 "github.com/sirupsen/logrus" 27 "k8s.io/apimachinery/pkg/util/sets" 28 29 "sigs.k8s.io/prow/pkg/github" 30 "sigs.k8s.io/prow/pkg/github/fakegithub" 31 jiraclient "sigs.k8s.io/prow/pkg/jira" 32 "sigs.k8s.io/prow/pkg/jira/fakejira" 33 "sigs.k8s.io/prow/pkg/plugins" 34 ) 35 36 func TestRegex(t *testing.T) { 37 t.Parallel() 38 testCases := []struct { 39 name string 40 input string 41 expected []string 42 }{ 43 { 44 name: "Simple", 45 input: "issue-123", 46 expected: []string{"issue-123"}, 47 }, 48 { 49 name: "Simple with leading space", 50 input: " issue-123", 51 expected: []string{"issue-123"}, 52 }, 53 { 54 name: "Simple with trailing space", 55 input: "issue-123 ", 56 expected: []string{"issue-123"}, 57 }, 58 { 59 name: "Simple with leading newline", 60 input: "\nissue-123", 61 expected: []string{"issue-123"}, 62 }, 63 { 64 name: "Simple with trailing newline", 65 input: "issue-123\n", 66 expected: []string{"issue-123"}, 67 }, 68 { 69 name: "Simple with trailing colon", 70 input: "issue-123:", 71 expected: []string{"issue-123"}, 72 }, 73 { 74 name: "Multiple matches", 75 input: "issue-123\nissue-456", 76 expected: []string{"issue-123", "issue-456"}, 77 }, 78 { 79 name: "Trailing character, no match", 80 input: "issue-123a", 81 }, 82 { 83 name: "Issue from url", 84 input: "https://my-jira.com/browse/ABC-123", 85 expected: []string{"ABC-123"}, 86 }, 87 { 88 name: "Trailing special characters, no match", 89 input: "rehearse-15676-pull", 90 }, 91 { 92 name: "Included in markdown link", 93 input: "[Jira Bug ABC-123](https://my-jira.com/browse/ABC-123)", 94 expected: []string{"ABC-123", "ABC-123"}, 95 }, 96 } 97 98 for _, tc := range testCases { 99 t.Run(tc.name, func(t *testing.T) { 100 result := extractCandidatesFromText(tc.input) 101 if diff := cmp.Diff(tc.expected, result); diff != "" { 102 t.Errorf("expected differs from actual: %s", diff) 103 } 104 }) 105 } 106 } 107 108 func TestHandle(t *testing.T) { 109 t.Parallel() 110 testCases := []struct { 111 name string 112 event github.GenericCommentEvent 113 cfg *plugins.Jira 114 projectCache *threadsafeSet 115 getIssueClientError map[string]error 116 existingIssues []jira.Issue 117 existingLinks map[string][]jira.RemoteLink 118 expectedNewLinks []jira.RemoteLink 119 expectedCommentUpdates []string 120 }{ 121 { 122 name: "No issue referenced, nothing to do", 123 }, 124 { 125 name: "Link is created based on body", 126 event: github.GenericCommentEvent{ 127 CommentID: intPtr(1), 128 HTMLURL: "https://github.com/org/repo/issues/3", 129 IssueTitle: "Some issue", 130 Body: "Some text and also ABC-123", 131 Repo: github.Repo{FullName: "org/repo", Owner: github.User{Login: "org"}, Name: "repo"}, 132 Number: 3, 133 }, 134 projectCache: &threadsafeSet{data: sets.New[string]("abc")}, 135 existingIssues: []jira.Issue{{ID: "ABC-123"}}, 136 expectedNewLinks: []jira.RemoteLink{{Object: &jira.RemoteLinkObject{ 137 URL: "https://github.com/org/repo/issues/3", 138 Title: "org/repo#3: Some issue", 139 Icon: &jira.RemoteLinkIcon{ 140 Url16x16: "https://github.com/favicon.ico", 141 Title: "GitHub", 142 }, 143 }, 144 }}, 145 expectedCommentUpdates: []string{"org/repo#1:Some text and also [ABC-123](https://my-jira.com/browse/ABC-123)"}, 146 }, 147 { 148 name: "Link is created based on body with pasted link", 149 event: github.GenericCommentEvent{ 150 CommentID: intPtr(1), 151 HTMLURL: "https://github.com/org/repo/issues/3", 152 IssueTitle: "Some issue", 153 Body: "Some text and also https://my-jira.com/browse/ABC-123", 154 Repo: github.Repo{FullName: "org/repo", Owner: github.User{Login: "org"}, Name: "repo"}, 155 Number: 3, 156 }, 157 projectCache: &threadsafeSet{data: sets.New[string]("abc")}, 158 existingIssues: []jira.Issue{{ID: "ABC-123"}}, 159 expectedNewLinks: []jira.RemoteLink{{Object: &jira.RemoteLinkObject{ 160 URL: "https://github.com/org/repo/issues/3", 161 Title: "org/repo#3: Some issue", 162 Icon: &jira.RemoteLinkIcon{ 163 Url16x16: "https://github.com/favicon.ico", 164 Title: "GitHub", 165 }, 166 }, 167 }}, 168 }, 169 { 170 name: "Link is created based on body and issuecomment suffix is removed from url", 171 event: github.GenericCommentEvent{ 172 CommentID: intPtr(1), 173 HTMLURL: "https://github.com/org/repo/issues/3#issuecomment-705743977", 174 IssueTitle: "Some issue", 175 Body: "Some text and also ABC-123", 176 Repo: github.Repo{FullName: "org/repo", Owner: github.User{Login: "org"}, Name: "repo"}, 177 Number: 3, 178 }, 179 projectCache: &threadsafeSet{data: sets.New[string]("abc")}, 180 existingIssues: []jira.Issue{{ID: "ABC-123"}}, 181 expectedNewLinks: []jira.RemoteLink{{Object: &jira.RemoteLinkObject{ 182 URL: "https://github.com/org/repo/issues/3", 183 Title: "org/repo#3: Some issue", 184 Icon: &jira.RemoteLinkIcon{ 185 Url16x16: "https://github.com/favicon.ico", 186 Title: "GitHub", 187 }, 188 }, 189 }}, 190 expectedCommentUpdates: []string{"org/repo#1:Some text and also [ABC-123](https://my-jira.com/browse/ABC-123)"}, 191 }, 192 { 193 name: "Link is created based on title", 194 event: github.GenericCommentEvent{ 195 HTMLURL: "https://github.com/org/repo/issues/3", 196 IssueTitle: "ABC-123: Some issue", 197 Body: "Some text", 198 Repo: github.Repo{FullName: "org/repo"}, 199 Number: 3, 200 }, 201 projectCache: &threadsafeSet{data: sets.New[string]("abc")}, 202 existingIssues: []jira.Issue{{ID: "ABC-123"}}, 203 expectedNewLinks: []jira.RemoteLink{{Object: &jira.RemoteLinkObject{ 204 URL: "https://github.com/org/repo/issues/3", 205 Title: "org/repo#3: ABC-123: Some issue", 206 Icon: &jira.RemoteLinkIcon{ 207 Url16x16: "https://github.com/favicon.ico", 208 Title: "GitHub", 209 }, 210 }, 211 }}, 212 }, 213 { 214 name: "Multiple references for issue, one link is created", 215 event: github.GenericCommentEvent{ 216 CommentID: intPtr(1), 217 HTMLURL: "https://github.com/org/repo/issues/3", 218 IssueTitle: "Some issue", 219 Body: "Some text and also ABC-123 and again ABC-123", 220 Repo: github.Repo{FullName: "org/repo", Owner: github.User{Login: "org"}, Name: "repo"}, 221 Number: 3, 222 }, 223 projectCache: &threadsafeSet{data: sets.New[string]("abc")}, 224 existingIssues: []jira.Issue{{ID: "ABC-123"}}, 225 expectedNewLinks: []jira.RemoteLink{{Object: &jira.RemoteLinkObject{ 226 URL: "https://github.com/org/repo/issues/3", 227 Title: "org/repo#3: Some issue", 228 Icon: &jira.RemoteLinkIcon{ 229 Url16x16: "https://github.com/favicon.ico", 230 Title: "GitHub", 231 }, 232 }, 233 }}, 234 expectedCommentUpdates: []string{"org/repo#1:Some text and also [ABC-123](https://my-jira.com/browse/ABC-123) and again [ABC-123](https://my-jira.com/browse/ABC-123)"}, 235 }, 236 { 237 name: "Referenced issue doesn't exist, nothing to do", 238 event: github.GenericCommentEvent{ 239 HTMLURL: "https://github.com/org/repo/issues/3#issuecomment-705743977", 240 IssueTitle: "Some issue", 241 Body: "Some text and also ABC-123", 242 Repo: github.Repo{FullName: "org/repo"}, 243 Number: 3, 244 }, 245 projectCache: &threadsafeSet{data: sets.New[string]("abc")}, 246 }, 247 { 248 name: "Link already exists, nothing to do", 249 event: github.GenericCommentEvent{ 250 HTMLURL: "https://github.com/org/repo/issues/3", 251 IssueTitle: "Some issue", 252 Body: "Some text and also [ABC-123](https://my-jira.com/browse/ABC-123)", 253 Repo: github.Repo{FullName: "org/repo"}, 254 Number: 3, 255 }, 256 projectCache: &threadsafeSet{data: sets.New[string]("abc")}, 257 existingIssues: []jira.Issue{{ID: "ABC-123"}}, 258 existingLinks: map[string][]jira.RemoteLink{"ABC-123": {{Object: &jira.RemoteLinkObject{URL: "https://github.com/org/repo/issues/3", Title: "org/repo#3: Some issue"}}}}, 259 }, 260 { 261 name: "Link exists but title is different, replacing it", 262 event: github.GenericCommentEvent{ 263 HTMLURL: "https://github.com/org/repo/issues/3", 264 IssueTitle: "Some issue NEW", 265 Body: "Some text and also [ABC-123:](https://my-jira.com/browse/ABC-123)", 266 Repo: github.Repo{FullName: "org/repo"}, 267 Number: 3, 268 }, 269 projectCache: &threadsafeSet{data: sets.New[string]("abc")}, 270 existingIssues: []jira.Issue{{ID: "ABC-123"}}, 271 existingLinks: map[string][]jira.RemoteLink{ 272 "ABC-123": { 273 { 274 Object: &jira.RemoteLinkObject{ 275 URL: "https://github.com/org/repo/issues/3", 276 Title: "org/repo#3: Some issue", 277 Icon: &jira.RemoteLinkIcon{Url16x16: "https://github.com/favicon.ico", Title: "GitHub"}, 278 }, 279 }, 280 }, 281 }, 282 expectedNewLinks: []jira.RemoteLink{ 283 { 284 Object: &jira.RemoteLinkObject{ 285 URL: "https://github.com/org/repo/issues/3", 286 Title: "org/repo#3: Some issue NEW", 287 Icon: &jira.RemoteLinkIcon{Url16x16: "https://github.com/favicon.ico", Title: "GitHub"}, 288 }, 289 }, 290 }, 291 }, 292 { 293 name: "Valid issue in disabled project, case insensitive matching and no link", 294 event: github.GenericCommentEvent{ 295 HTMLURL: "https://github.com/org/repo/issues/3", 296 IssueTitle: "Some issue", 297 Body: "Some text and also ENTERPRISE-4", 298 Repo: github.Repo{FullName: "org/repo"}, 299 Number: 3, 300 }, 301 projectCache: &threadsafeSet{data: sets.New[string]("enterprise")}, 302 cfg: &plugins.Jira{DisabledJiraProjects: []string{"Enterprise"}}, 303 existingIssues: []jira.Issue{{ID: "ENTERPRISE-4"}}, 304 }, 305 { 306 name: "Valid issue in disabled project, multiple references, with markdown link, case insensitive matching, nothing to do", 307 event: github.GenericCommentEvent{ 308 HTMLURL: "https://github.com/org/repo/issues/3", 309 IssueTitle: "ABC-123: Fixes Some issue", 310 Body: "Some text and also [ABC-123](https://my-jira.com/browse/ABC-123)", 311 Repo: github.Repo{FullName: "org/repo"}, 312 Number: 3, 313 }, 314 projectCache: &threadsafeSet{data: sets.New[string]("abc")}, 315 cfg: &plugins.Jira{DisabledJiraProjects: []string{"abc"}}, 316 }, 317 { 318 name: "Project 404 gets served from cache, nothing happens", 319 event: github.GenericCommentEvent{ 320 HTMLURL: "https://github.com/org/repo/issues/3", 321 IssueTitle: "Some issue", 322 Body: "ABC-123", 323 Repo: github.Repo{FullName: "org/repo"}, 324 Number: 3, 325 }, 326 projectCache: &threadsafeSet{}, 327 getIssueClientError: map[string]error{"ABC-123": errors.New("error: didn't serve 404 from cache")}, 328 }, 329 } 330 331 for _, tc := range testCases { 332 t.Run(tc.name, func(t *testing.T) { 333 // convert []jira.Issue to []*jira.Issue 334 var ptrIssues []*jira.Issue 335 for index := range tc.existingIssues { 336 ptrIssues = append(ptrIssues, &tc.existingIssues[index]) 337 } 338 jiraClient := &fakejira.FakeClient{ 339 Issues: ptrIssues, 340 ExistingLinks: tc.existingLinks, 341 GetIssueError: tc.getIssueClientError, 342 } 343 githubClient := fakegithub.NewFakeClient() 344 345 if err := handleWithProjectCache(jiraClient, githubClient, tc.cfg, logrus.NewEntry(logrus.New()), &tc.event, tc.projectCache); err != nil { 346 t.Fatalf("handle failed: %v", err) 347 } 348 349 if diff := cmp.Diff(jiraClient.NewLinks, tc.expectedNewLinks); diff != "" { 350 t.Errorf("new links differs from expected new links: %s", diff) 351 } 352 353 if diff := cmp.Diff(githubClient.IssueCommentsEdited, tc.expectedCommentUpdates); diff != "" { 354 t.Errorf("comment updates differ from expected: %s", diff) 355 } 356 }) 357 } 358 359 } 360 361 func intPtr(i int) *int { 362 return &i 363 } 364 365 func TestInsertLinksIntoComment(t *testing.T) { 366 t.Parallel() 367 const issueName = "ABC-123" 368 testCases := []struct { 369 name string 370 body string 371 expected string 372 }{ 373 { 374 name: "Multiline body starting with issue name", 375 body: `ABC-123: Fix problems: 376 * First problem 377 * Second problem`, 378 expected: `[ABC-123](https://my-jira.com/browse/ABC-123): Fix problems: 379 * First problem 380 * Second problem`, 381 }, 382 { 383 name: "Multiline body starting with already replaced issue name", 384 body: `[ABC-123](https://my-jira.com/browse/ABC-123): Fix problems: 385 * First problem 386 * Second problem`, 387 expected: `[ABC-123](https://my-jira.com/browse/ABC-123): Fix problems: 388 * First problem 389 * Second problem`, 390 }, 391 { 392 name: "Multiline body with multiple occurrence in the middle", 393 body: `This change: 394 * Does stuff related to ABC-123 395 * And even more stuff related to ABC-123 396 * But also something else`, 397 expected: `This change: 398 * Does stuff related to [ABC-123](https://my-jira.com/browse/ABC-123) 399 * And even more stuff related to [ABC-123](https://my-jira.com/browse/ABC-123) 400 * But also something else`, 401 }, 402 { 403 name: "Multiline body with multiple occurrence in the middle, some already replaced", 404 body: `This change: 405 * Does stuff related to [ABC-123](https://my-jira.com/browse/ABC-123) 406 * And even more stuff related to ABC-123 407 * But also something else`, 408 expected: `This change: 409 * Does stuff related to [ABC-123](https://my-jira.com/browse/ABC-123) 410 * And even more stuff related to [ABC-123](https://my-jira.com/browse/ABC-123) 411 * But also something else`, 412 }, 413 { 414 name: "Multiline body with issue name at the end", 415 body: `This change: 416 is very important 417 because of ABC-123`, 418 expected: `This change: 419 is very important 420 because of [ABC-123](https://my-jira.com/browse/ABC-123)`, 421 }, 422 { 423 name: "Multiline body with already replaced issue name at the end", 424 body: `This change: 425 is very important 426 because of [ABC-123](https://my-jira.com/browse/ABC-123)`, 427 expected: `This change: 428 is very important 429 because of [ABC-123](https://my-jira.com/browse/ABC-123)`, 430 }, 431 { 432 name: "Pasted links are not replaced, as they are already clickable", 433 body: "https://my-jira.com/browse/ABC-123", 434 expected: "https://my-jira.com/browse/ABC-123", 435 }, 436 { 437 name: "code section is not replaced", 438 body: `This change: 439 is very important` + "\n```bash\n" + 440 `ABC-123` + 441 "\n```\n" + `ABC-123 442 `, 443 expected: `This change: 444 is very important` + "\n```bash\n" + 445 `ABC-123` + 446 "\n```\n" + `[ABC-123](https://my-jira.com/browse/ABC-123) 447 `, 448 }, 449 { 450 name: "inline code is not replaced", 451 body: `This change: 452 is very important` + "\n``ABC-123`` and `ABC-123` shouldn't be replaced, as well as ``ABC-123: text text``. " + 453 `ABC-123 should be replaced. 454 `, 455 expected: `This change: 456 is very important` + "\n``ABC-123`` and `ABC-123` shouldn't be replaced, as well as ``ABC-123: text text``. " + 457 `[ABC-123](https://my-jira.com/browse/ABC-123) should be replaced. 458 `, 459 }, 460 { 461 name: "Multiline codeblock that is denoted through four leading spaces", 462 body: "I meant to do this test:\r\n\r\n operator_test.go:1914: failed to read output from pod unique-id-header-test-1: container \"curl\" in pod \"unique-id-header-ABC-123\" is waiting to start: ContainerCreating\r\n\r\n", 463 expected: "I meant to do this test:\r\n\r\n operator_test.go:1914: failed to read output from pod unique-id-header-test-1: container \"curl\" in pod \"unique-id-header-ABC-123\" is waiting to start: ContainerCreating\r\n\r\n", 464 }, 465 { 466 name: "parts of words starting with a dash are not replaced", 467 body: "this shouldn't be replaced: whatever-ABC-123 and also inline `whatever-ABC-123`", 468 expected: "this shouldn't be replaced: whatever-ABC-123 and also inline `whatever-ABC-123`", 469 }, 470 } 471 472 for _, tc := range testCases { 473 t.Run(tc.name, func(t *testing.T) { 474 if diff := cmp.Diff(insertLinksIntoComment(tc.body, []string{issueName}, fakejira.FakeJiraUrl), tc.expected); diff != "" { 475 t.Errorf("actual result differs from expected result: %s", diff) 476 } 477 }) 478 } 479 } 480 481 func TestProjectCachingJiraClient(t *testing.T) { 482 t.Parallel() 483 lowerCaseIssue := jira.Issue{ID: "issue-123"} 484 upperCaseIssue := jira.Issue{ID: "ISSUE-123"} 485 testCases := []struct { 486 name string 487 client jiraclient.Client 488 issueToRequest string 489 cache *threadsafeSet 490 expectedError error 491 }{ 492 { 493 name: "404 gets served from cache", 494 client: &fakejira.FakeClient{}, 495 issueToRequest: "issue-123", 496 cache: &threadsafeSet{data: sets.Set[string]{}}, 497 expectedError: jiraclient.NewNotFoundError(errors.New("404 from cache")), 498 }, 499 { 500 name: "Success", 501 client: &fakejira.FakeClient{Issues: []*jira.Issue{&lowerCaseIssue}}, 502 issueToRequest: "issue-123", 503 cache: &threadsafeSet{data: sets.New[string]("issue")}, 504 }, 505 { 506 name: "Success case-insensitive", 507 client: &fakejira.FakeClient{Issues: []*jira.Issue{&upperCaseIssue}}, 508 issueToRequest: "ISSUE-123", 509 cache: &threadsafeSet{data: sets.New[string]("issue")}, 510 }, 511 } 512 513 for _, tc := range testCases { 514 t.Run(tc.name, func(t *testing.T) { 515 cachingClient := &projectCachingJiraClient{ 516 Client: tc.client, 517 cache: tc.cache, 518 } 519 520 _, err := cachingClient.GetIssue(tc.issueToRequest) 521 if diff := cmp.Diff(tc.expectedError, err, cmp.Exporter(func(_ reflect.Type) bool { return true })); diff != "" { 522 t.Fatalf("expected error differs from expected: %s", diff) 523 } 524 }) 525 } 526 } 527 528 func TestFilterOutDisabledJiraProjects(t *testing.T) { 529 t.Parallel() 530 testCases := []struct { 531 name string 532 candidates []string 533 jiraConfig *plugins.Jira 534 expectedOutput []string 535 }{{ 536 name: "empty jira config", 537 candidates: []string{"ABC-123", "DEF-567"}, 538 jiraConfig: nil, 539 expectedOutput: []string{"ABC-123", "DEF-567"}, 540 }, { 541 name: "upper case disabled list", 542 candidates: []string{"ABC-123", "DEF-567"}, 543 jiraConfig: &plugins.Jira{DisabledJiraProjects: []string{"ABC"}}, 544 expectedOutput: []string{"DEF-567"}, 545 }, { 546 name: "lower case disabled list", 547 candidates: []string{"ABC-123", "DEF-567"}, 548 jiraConfig: &plugins.Jira{DisabledJiraProjects: []string{"abc"}}, 549 expectedOutput: []string{"DEF-567"}, 550 }, { 551 name: "multiple disabled projects", 552 candidates: []string{"ABC-123", "DEF-567"}, 553 jiraConfig: &plugins.Jira{DisabledJiraProjects: []string{"abc", "def"}}, 554 expectedOutput: []string{}, 555 }} 556 557 for _, tc := range testCases { 558 t.Run(tc.name, func(t *testing.T) { 559 output := filterOutDisabledJiraProjects(tc.candidates, tc.jiraConfig) 560 if diff := cmp.Diff(tc.expectedOutput, output); diff != "" { 561 t.Fatalf("actual output differes from expected output: %s", diff) 562 } 563 }) 564 } 565 }