3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.issue.notification;
22 import com.google.common.collect.ImmutableSet;
23 import com.tngtech.java.junit.dataprovider.DataProvider;
24 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
25 import com.tngtech.java.junit.dataprovider.UseDataProvider;
26 import java.util.Arrays;
27 import java.util.Collections;
28 import java.util.List;
29 import java.util.Locale;
30 import java.util.Random;
32 import java.util.function.Function;
33 import java.util.stream.IntStream;
34 import java.util.stream.Stream;
35 import org.elasticsearch.common.util.set.Sets;
36 import org.junit.Test;
37 import org.junit.runner.RunWith;
38 import org.sonar.api.config.EmailSettings;
39 import org.sonar.api.notifications.Notification;
40 import org.sonar.api.rules.RuleType;
41 import org.sonar.core.i18n.I18n;
42 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
43 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
44 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
45 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
46 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
47 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
48 import org.sonar.test.html.HtmlFragmentAssert;
49 import org.sonar.test.html.HtmlListAssert;
50 import org.sonar.test.html.HtmlParagraphAssert;
52 import static java.util.stream.Collectors.joining;
53 import static java.util.stream.Collectors.toList;
54 import static java.util.stream.Collectors.toSet;
55 import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
56 import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4;
57 import static org.assertj.core.api.Assertions.assertThat;
58 import static org.assertj.core.api.Assertions.assertThatThrownBy;
59 import static org.mockito.Mockito.mock;
60 import static org.mockito.Mockito.when;
61 import static org.sonar.api.issue.Issue.STATUS_CLOSED;
62 import static org.sonar.api.issue.Issue.STATUS_CONFIRMED;
63 import static org.sonar.api.issue.Issue.STATUS_OPEN;
64 import static org.sonar.api.issue.Issue.STATUS_REOPENED;
65 import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
66 import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
67 import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
68 import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
69 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newAnalysisChange;
70 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newBranch;
71 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newChangedIssue;
72 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject;
73 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
74 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRule;
75 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newSecurityHotspotRule;
76 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newUserChange;
77 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.randomRuleTypeHotspotExcluded;
79 @RunWith(DataProviderRunner.class)
80 public class ChangesOnMyIssuesEmailTemplateTest {
81 private static final String[] ISSUE_STATUSES = {STATUS_OPEN, STATUS_RESOLVED, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_CLOSED};
82 private static final String[] SECURITY_HOTSPOTS_STATUSES = {STATUS_TO_REVIEW, STATUS_REVIEWED};
85 private I18n i18n = mock(I18n.class);
86 private EmailSettings emailSettings = mock(EmailSettings.class);
87 private ChangesOnMyIssuesEmailTemplate underTest = new ChangesOnMyIssuesEmailTemplate(i18n, emailSettings);
90 public void format_returns_null_on_Notification() {
91 EmailMessage emailMessage = underTest.format(mock(Notification.class));
93 assertThat(emailMessage).isNull();
97 public void formats_fails_with_ISE_if_change_from_Analysis_and_no_issue() {
98 AnalysisChange analysisChange = newAnalysisChange();
100 assertThatThrownBy(() -> underTest.format(new ChangesOnMyIssuesNotification(analysisChange, Collections.emptySet())))
101 .isInstanceOf(IllegalStateException.class)
102 .hasMessage("changedIssues can't be empty");
106 public void format_sets_message_id_with_project_key_of_first_issue_in_set_when_change_from_Analysis() {
107 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
108 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
110 AnalysisChange analysisChange = newAnalysisChange();
112 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
114 assertThat(emailMessage.getMessageId()).isEqualTo("changes-on-my-issues/" + changedIssues.iterator().next().getProject().getKey());
118 public void format_sets_subject_with_project_name_of_first_issue_in_set_when_change_from_Analysis() {
119 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
120 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
122 AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
124 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
126 Project project = changedIssues.iterator().next().getProject();
127 assertThat(emailMessage.getSubject()).isEqualTo("Analysis has changed some of your issues in " + project.getProjectName());
131 public void format_sets_subject_with_project_name_and_branch_name_of_first_issue_in_set_when_change_from_Analysis() {
132 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
133 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newBranch("prj_" + i, "br_" + i), newRandomNotAHotspotRule("rule_" + i)))
135 AnalysisChange analysisChange = newAnalysisChange();
137 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
139 Project project = changedIssues.iterator().next().getProject();
140 assertThat(emailMessage.getSubject()).isEqualTo("Analysis has changed some of your issues in " + project.getProjectName() + " (" + project.getBranchName().get() + ")");
144 public void format_set_html_message_with_header_dealing_with_plural_when_change_from_Analysis() {
145 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
146 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
148 AnalysisChange analysisChange = newAnalysisChange();
150 EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues.stream().limit(1).collect(toSet())));
151 EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
153 HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
155 .hasParagraph("An analysis has updated an issue assigned to you:");
156 HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
158 .hasParagraph("An analysis has updated issues assigned to you:");
162 public void format_sets_static_message_id_when_change_from_User() {
163 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
164 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
166 UserChange userChange = newUserChange();
168 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
170 assertThat(emailMessage.getMessageId()).isEqualTo("changes-on-my-issues");
174 public void format_sets_static_subject_when_change_from_User() {
175 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
176 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
178 UserChange userChange = newUserChange();
180 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
182 assertThat(emailMessage.getSubject()).isEqualTo("A manual update has changed some of your issues/hotspots");
186 public void format_set_html_message_with_header_dealing_with_plural_issues_when_change_from_User() {
187 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
188 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
190 UserChange userChange = newUserChange();
192 EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
193 userChange, changedIssues.stream().limit(1).collect(toSet())));
194 EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
196 HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
199 .hasParagraph("A manual change has updated an issue assigned to you:")
201 HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
204 .hasParagraph("A manual change has updated issues assigned to you:")
209 public void format_set_html_message_with_header_dealing_with_plural_security_hotspots_when_change_from_User() {
210 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
211 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newSecurityHotspotRule("rule_" + i)))
213 UserChange userChange = newUserChange();
215 EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
216 userChange, changedIssues.stream().limit(1).collect(toSet())));
217 EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
219 HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
222 .hasParagraph("A manual change has updated a hotspot assigned to you:")
224 HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
227 .hasParagraph("A manual change has updated hotspots assigned to you:")
232 public void format_set_html_message_with_header_dealing_with_plural_security_hotspots_and_issues_when_change_from_User() {
233 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
234 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
237 Set<ChangedIssue> changedHotspots = IntStream.range(0, 2 + new Random().nextInt(4))
238 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newSecurityHotspotRule("rule_" + i)))
241 Set<ChangedIssue> issuesAndHotspots = Sets.union(changedIssues, changedHotspots);
243 UserChange userChange = newUserChange();
245 EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, issuesAndHotspots));
247 HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
250 .hasParagraph("A manual change has updated issues/hotspots assigned to you:")
255 @UseDataProvider("issueStatuses")
256 public void format_set_html_message_with_footer_when_issue_change_from_user(String issueStatus) {
257 UserChange userChange = newUserChange();
258 format_set_html_message_with_footer(userChange, issueStatus, c -> c
260 .hasParagraph() // skip project header
261 .hasList(), // rule list,
262 randomRuleTypeHotspotExcluded());
266 @UseDataProvider("issueStatuses")
267 public void format_set_html_message_with_footer_when_issue_change_from_analysis(String issueStatus) {
268 AnalysisChange analysisChange = newAnalysisChange();
269 format_set_html_message_with_footer(analysisChange, issueStatus, c -> c
270 .hasParagraph() // status
271 .hasList(), // rule list,
272 randomRuleTypeHotspotExcluded());
276 @UseDataProvider("securityHotspotsStatuses")
277 public void format_set_html_message_with_footer_when_security_hotspot_change_from_analysis(String securityHotspotStatus) {
278 AnalysisChange analysisChange = newAnalysisChange();
279 format_set_html_message_with_footer(analysisChange, securityHotspotStatus, c -> c
281 .hasList(), // rule list
286 @UseDataProvider("securityHotspotsStatuses")
287 public void format_set_html_message_with_footer_when_security_hotspot_change_from_user(String securityHotspotStatus) {
288 UserChange userChange = newUserChange();
289 format_set_html_message_with_footer(userChange, securityHotspotStatus, c -> c
291 .hasList(), // rule list
296 public static Object[][] issueStatuses() {
297 return Arrays.stream(ISSUE_STATUSES)
298 .map(t -> new Object[] {t})
299 .toArray(Object[][]::new);
303 public static Object[][] securityHotspotsStatuses() {
304 return Arrays.stream(SECURITY_HOTSPOTS_STATUSES)
305 .map(t -> new Object[] {t})
306 .toArray(Object[][]::new);
309 private void format_set_html_message_with_footer(Change change, String issueStatus, Function<HtmlParagraphAssert, HtmlListAssert> skipContent, RuleType ruleType) {
310 String wordingNotification = randomAlphabetic(20);
311 String host = randomAlphabetic(15);
312 when(i18n.message(Locale.ENGLISH, "notification.dispatcher.ChangesOnMyIssue", "notification.dispatcher.ChangesOnMyIssue"))
313 .thenReturn(wordingNotification);
314 when(emailSettings.getServerBaseURL()).thenReturn(host);
315 Project project = newProject("foo");
316 Rule rule = newRule("bar", ruleType);
317 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
318 .mapToObj(i -> newChangedIssue(i + "", issueStatus, project, rule))
321 EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
322 change, changedIssues.stream().limit(1).collect(toSet())));
323 EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(change, changedIssues));
325 Stream.of(singleIssueMessage, multiIssueMessage)
326 .forEach(issueMessage -> {
327 HtmlParagraphAssert htmlAssert = HtmlFragmentAssert.assertThat(issueMessage.getMessage())
328 .hasParagraph().hasParagraph(); // skip header
330 HtmlListAssert htmlListAssert = skipContent.apply(htmlAssert);
332 String footerText = "You received this email because you are subscribed to \"" + wordingNotification + "\" notifications from SonarQube."
333 + " Click here to edit your email preferences.";
334 htmlListAssert.hasEmptyParagraph()
335 .hasParagraph(footerText)
336 .withSmallOn(footerText)
337 .withLink("here", host + "/account/notifications")
343 public void format_set_html_message_with_issues_grouped_by_status_closed_or_any_other_when_change_from_analysis() {
344 Project project = newProject("foo");
345 Rule rule = newRandomNotAHotspotRule("bar");
346 Set<ChangedIssue> changedIssues = Arrays.stream(ISSUE_STATUSES)
347 .map(status -> newChangedIssue(status + "", status, project, rule))
349 AnalysisChange analysisChange = newAnalysisChange();
351 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
353 HtmlListAssert htmlListAssert = HtmlFragmentAssert.assertThat(emailMessage.getMessage())
354 .hasParagraph().hasParagraph() // skip header
355 .hasParagraph("Closed issue:")
357 .hasList("Rule " + rule.getName() + " - See the single issue")
358 .withLinkOn("See the single issue")
359 .hasParagraph("Open issues:")
361 .hasList("Rule " + rule.getName() + " - See all " + (ISSUE_STATUSES.length - 1) + " issues")
362 .withLinkOn("See all " + (ISSUE_STATUSES.length - 1) + " issues");
363 verifyEnd(htmlListAssert);
367 public void format_set_html_message_with_issue_status_title_handles_plural_when_change_from_analysis() {
368 Project project = newProject("foo");
369 Rule rule = newRandomNotAHotspotRule("bar");
370 Set<ChangedIssue> closedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
371 .mapToObj(status -> newChangedIssue(status + "", STATUS_CLOSED, project, rule))
373 Set<ChangedIssue> openIssues = IntStream.range(0, 2 + new Random().nextInt(5))
374 .mapToObj(status -> newChangedIssue(status + "", STATUS_OPEN, project, rule))
376 AnalysisChange analysisChange = newAnalysisChange();
378 EmailMessage closedIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, closedIssues));
379 EmailMessage openIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, openIssues));
381 HtmlListAssert htmlListAssert = HtmlFragmentAssert.assertThat(closedIssuesMessage.getMessage())
382 .hasParagraph().hasParagraph() // skip header
383 .hasParagraph("Closed issues:")
385 verifyEnd(htmlListAssert);
386 htmlListAssert = HtmlFragmentAssert.assertThat(openIssuesMessage.getMessage())
387 .hasParagraph().hasParagraph() // skip header
388 .hasParagraph("Open issues:")
390 verifyEnd(htmlListAssert);
394 public void formats_returns_html_message_for_single_issue_on_master_when_analysis_change() {
395 Project project = newProject("1");
396 String ruleName = randomAlphabetic(8);
397 String host = randomAlphabetic(15);
398 ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
399 AnalysisChange analysisChange = newAnalysisChange();
400 when(emailSettings.getServerBaseURL()).thenReturn(host);
402 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
404 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
405 .hasParagraph().hasParagraph() // skip header
406 .hasParagraph()// skip title based on status
407 .hasList("Rule " + ruleName + " - See the single issue")
408 .withLink("See the single issue", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
409 .hasParagraph().hasParagraph() // skip footer
414 public void user_input_content_should_be_html_escape() {
415 Project project = new Project.Builder("uuid").setProjectName("</projectName>").setKey("project_key").build();
416 String ruleName = "</RandomRule>";
417 String host = randomAlphabetic(15);
418 Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
419 List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
420 .mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule))
422 UserChange userChange = newUserChange();
423 when(emailSettings.getServerBaseURL()).thenReturn(host);
425 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
427 assertThat(emailMessage.getMessage())
428 .doesNotContain(project.getProjectName())
429 .contains(escapeHtml4(project.getProjectName()))
430 .doesNotContain(ruleName)
431 .contains(escapeHtml4(ruleName));
433 String expectedHref = host + "/project/issues?id=" + project.getKey()
434 + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
435 String expectedLinkText = "See all " + changedIssues.size() + " issues";
436 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
437 .hasParagraph().hasParagraph() // skip header
438 .hasParagraph(project.getProjectName())
439 .hasList("Rule " + ruleName + " - " + expectedLinkText)
440 .withLink(expectedLinkText, expectedHref)
441 .hasParagraph().hasParagraph() // skip footer
446 public void formats_returns_html_message_for_single_issue_on_master_when_user_change() {
447 Project project = newProject("1");
448 String ruleName = randomAlphabetic(8);
449 String host = randomAlphabetic(15);
450 ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
451 UserChange userChange = newUserChange();
452 when(emailSettings.getServerBaseURL()).thenReturn(host);
454 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
456 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
457 .hasParagraph().hasParagraph() // skip header
458 .hasParagraph(project.getProjectName())
459 .hasList("Rule " + ruleName + " - See the single issue")
460 .withLink("See the single issue", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
461 .hasParagraph().hasParagraph() // skip footer
466 public void formats_returns_html_message_for_single_issue_on_branch_when_analysis_change() {
467 String branchName = randomAlphabetic(6);
468 Project project = newBranch("1", branchName);
469 String ruleName = randomAlphabetic(8);
470 String host = randomAlphabetic(15);
472 ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
473 AnalysisChange analysisChange = newAnalysisChange();
474 when(emailSettings.getServerBaseURL()).thenReturn(host);
476 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
478 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
479 .hasParagraph().hasParagraph() // skip header
480 .hasParagraph()// skip title based on status
481 .hasList("Rule " + ruleName + " - See the single issue")
482 .withLink("See the single issue",
483 host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
484 .hasParagraph().hasParagraph() // skip footer
489 public void formats_returns_html_message_for_single_issue_on_branch_when_user_change() {
490 String branchName = randomAlphabetic(6);
491 Project project = newBranch("1", branchName);
492 String ruleName = randomAlphabetic(8);
493 String host = randomAlphabetic(15);
495 ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
496 UserChange userChange = newUserChange();
497 when(emailSettings.getServerBaseURL()).thenReturn(host);
499 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
501 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
502 .hasParagraph().hasParagraph() // skip header
503 .hasParagraph(project.getProjectName() + ", " + branchName)
504 .hasList("Rule " + ruleName + " - See the single issue")
505 .withLink("See the single issue",
506 host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
507 .hasParagraph().hasParagraph() // skip footer
512 public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master_when_analysis_change() {
513 Project project = newProject("1");
514 String ruleName = randomAlphabetic(8);
515 String host = randomAlphabetic(15);
516 Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
517 String issueStatus = randomValidStatus();
518 List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
519 .mapToObj(i -> newChangedIssue("issue_" + i, issueStatus, project, rule))
521 AnalysisChange analysisChange = newAnalysisChange();
522 when(emailSettings.getServerBaseURL()).thenReturn(host);
524 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
526 String expectedHref = host + "/project/issues?id=" + project.getKey()
527 + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
528 String expectedLinkText = "See all " + changedIssues.size() + " issues";
529 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
530 .hasParagraph().hasParagraph() // skip header
531 .hasParagraph() // skip title based on status
532 .hasList("Rule " + ruleName + " - " + expectedLinkText)
533 .withLink(expectedLinkText, expectedHref)
534 .hasParagraph().hasParagraph() // skip footer
539 public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master_when_user_change() {
540 Project project = newProject("1");
541 String ruleName = randomAlphabetic(8);
542 String host = randomAlphabetic(15);
543 Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
544 List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
545 .mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule))
547 UserChange userChange = newUserChange();
548 when(emailSettings.getServerBaseURL()).thenReturn(host);
550 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
552 String expectedHref = host + "/project/issues?id=" + project.getKey()
553 + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
554 String expectedLinkText = "See all " + changedIssues.size() + " issues";
555 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
556 .hasParagraph().hasParagraph() // skip header
557 .hasParagraph(project.getProjectName())
558 .hasList("Rule " + ruleName + " - " + expectedLinkText)
559 .withLink(expectedLinkText, expectedHref)
560 .hasParagraph().hasParagraph() // skip footer
565 public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch_when_analysis_change() {
566 String branchName = randomAlphabetic(19);
567 Project project = newBranch("1", branchName);
568 String ruleName = randomAlphabetic(8);
569 String host = randomAlphabetic(15);
570 Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
571 String status = randomValidStatus();
572 List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
573 .mapToObj(i -> newChangedIssue("issue_" + i, status, project, rule))
575 AnalysisChange analysisChange = newAnalysisChange();
576 when(emailSettings.getServerBaseURL()).thenReturn(host);
578 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
580 String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName
581 + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
582 String expectedLinkText = "See all " + changedIssues.size() + " issues";
583 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
584 .hasParagraph().hasParagraph() // skip header
585 .hasParagraph()// skip title based on status
586 .hasList("Rule " + ruleName + " - " + expectedLinkText)
587 .withLink(expectedLinkText, expectedHref)
588 .hasParagraph().hasParagraph() // skip footer
593 public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch_when_user_change() {
594 String branchName = randomAlphabetic(19);
595 Project project = newBranch("1", branchName);
596 String ruleName = randomAlphabetic(8);
597 String host = randomAlphabetic(15);
598 Rule rule = newRandomNotAHotspotRule(ruleName);
599 List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
600 .mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule))
602 UserChange userChange = newUserChange();
603 when(emailSettings.getServerBaseURL()).thenReturn(host);
605 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
607 String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName
608 + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
609 String expectedLinkText = "See all " + changedIssues.size() + " issues";
610 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
611 .hasParagraph().hasParagraph() // skip header
612 .hasParagraph(project.getProjectName() + ", " + branchName)
613 .hasList("Rule " + ruleName + " - " + expectedLinkText)
614 .withLink(expectedLinkText, expectedHref)
615 .hasParagraph().hasParagraph() // skip footer
620 public void formats_returns_html_message_with_projects_ordered_by_name_when_user_change() {
621 Project project1 = newProject("1");
622 Project project1Branch1 = newBranch("1", "a");
623 Project project1Branch2 = newBranch("1", "b");
624 Project project2 = newProject("B");
625 Project project2Branch1 = newBranch("B", "a");
626 Project project3 = newProject("C");
627 String host = randomAlphabetic(15);
628 List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3)
629 .map(project -> newChangedIssue("issue_" + project.getUuid(), randomValidStatus(), project, newRule(randomAlphabetic(2), randomRuleTypeHotspotExcluded())))
631 Collections.shuffle(changedIssues);
632 UserChange userChange = newUserChange();
633 when(emailSettings.getServerBaseURL()).thenReturn(host);
635 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
637 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
638 .hasParagraph().hasParagraph() // skip header
639 .hasParagraph(project1.getProjectName())
641 .hasParagraph(project1Branch1.getProjectName() + ", " + project1Branch1.getBranchName().get())
643 .hasParagraph(project1Branch2.getProjectName() + ", " + project1Branch2.getBranchName().get())
645 .hasParagraph(project2.getProjectName())
647 .hasParagraph(project2Branch1.getProjectName() + ", " + project2Branch1.getBranchName().get())
649 .hasParagraph(project3.getProjectName())
651 .hasParagraph().hasParagraph() // skip footer
656 public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change() {
657 Project project = newProject("1");
658 Rule rule1 = newRandomNotAHotspotRule("1");
659 Rule rule2 = newRandomNotAHotspotRule("a");
660 Rule rule3 = newRandomNotAHotspotRule("b");
661 Rule rule4 = newRandomNotAHotspotRule("X");
663 String host = randomAlphabetic(15);
664 String issueStatus = randomValidStatus();
665 List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
666 .map(rule -> newChangedIssue("issue_" + rule.getName(), issueStatus, project, rule))
668 Collections.shuffle(changedIssues);
669 AnalysisChange analysisChange = newAnalysisChange();
670 when(emailSettings.getServerBaseURL()).thenReturn(host);
672 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
674 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
675 .hasParagraph().hasParagraph() // skip header
676 .hasParagraph()// skip title based on status
678 "Rule " + rule1.getName() + " - See the single issue",
679 "Rule " + rule2.getName() + " - See the single issue",
680 "Rule " + rule3.getName() + " - See the single issue",
681 "Rule " + rule4.getName() + " - See the single issue")
682 .hasParagraph().hasParagraph() // skip footer
687 public void formats_returns_html_message_with_rules_ordered_by_name_user_change() {
688 Project project = newProject("1");
689 Rule rule1 = newRandomNotAHotspotRule("1");
690 Rule rule2 = newRandomNotAHotspotRule("a");
691 Rule rule3 = newRandomNotAHotspotRule("b");
692 Rule rule4 = newRandomNotAHotspotRule("X");
694 Rule hotspot1 = newSecurityHotspotRule("S");
695 Rule hotspot2 = newSecurityHotspotRule("Z");
696 Rule hotspot3 = newSecurityHotspotRule("N");
697 Rule hotspot4 = newSecurityHotspotRule("M");
699 String host = randomAlphabetic(15);
700 List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4, hotspot1, hotspot2, hotspot3, hotspot4)
701 .map(rule -> newChangedIssue("issue_" + rule.getName(), randomValidStatus(), project, rule))
703 Collections.shuffle(changedIssues);
704 UserChange userChange = newUserChange();
705 when(emailSettings.getServerBaseURL()).thenReturn(host);
707 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
709 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
712 .hasParagraph() // skip project name
714 "Rule " + rule1.getName() + " - See the single issue",
715 "Rule " + rule2.getName() + " - See the single issue",
716 "Rule " + rule3.getName() + " - See the single issue",
717 "Rule " + rule4.getName() + " - See the single issue")
720 "Rule " + hotspot1.getName() + " - See the single hotspot",
721 "Rule " + hotspot2.getName() + " - See the single hotspot",
722 "Rule " + hotspot3.getName() + " - See the single hotspot",
723 "Rule " + hotspot4.getName() + " - See the single hotspot")
724 .hasParagraph().hasParagraph() // skip footer
729 public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_when_analysis_change() {
730 Project project1 = newProject("1");
731 Rule rule1 = newRandomNotAHotspotRule("1");
732 Rule rule2 = newRandomNotAHotspotRule("a");
734 String host = randomAlphabetic(15);
735 String issueStatusClosed = STATUS_CLOSED;
736 String otherIssueStatus = STATUS_RESOLVED;
738 List<ChangedIssue> changedIssues = Stream.of(
739 IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, issueStatusClosed, project1, rule1)),
740 IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, issueStatusClosed, project1, rule2)),
741 IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, otherIssueStatus, project1, rule2)),
742 IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, otherIssueStatus, project1, rule1)))
746 Collections.shuffle(changedIssues);
747 AnalysisChange analysisChange = newAnalysisChange();
748 when(emailSettings.getServerBaseURL()).thenReturn(host);
750 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
752 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
753 .hasParagraph().hasParagraph() // skip header
754 .hasParagraph("Closed issues:") // skip title based on status
756 "Rule " + rule1.getName() + " - See all 39 issues",
757 "Rule " + rule2.getName() + " - See all 40 issues")
758 .withLink("See all 39 issues",
759 host + "/project/issues?id=" + project1.getKey()
760 + "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
761 .withLink("See all 40 issues",
762 host + "/project/issues?id=" + project1.getKey()
763 + "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
764 .hasParagraph("Open issues:")
766 "Rule " + rule2.getName() + " - See issues 1-40 41-80 81",
767 "Rule " + rule1.getName() + " - See all 6 issues")
769 host + "/project/issues?id=" + project1.getKey()
770 + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
772 host + "/project/issues?id=" + project1.getKey()
773 + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
775 host + "/project/issues?id=" + project1.getKey()
776 + "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
777 .withLink("See all 6 issues",
778 host + "/project/issues?id=" + project1.getKey()
779 + "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
780 .hasParagraph().hasParagraph() // skip footer
785 public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_and_hotspots_when_user_change() {
786 Project project1 = newProject("1");
787 Project project2 = newProject("V");
788 Project project2Branch = newBranch("V", "AB");
789 Rule rule1 = newRule("1", randomRuleTypeHotspotExcluded());
790 Rule rule2 = newRule("a", randomRuleTypeHotspotExcluded());
792 Rule hotspot1 = newSecurityHotspotRule("h1");
793 Rule hotspot2 = newSecurityHotspotRule("h2");
795 String status = randomValidStatus();
796 String host = randomAlphabetic(15);
797 List<ChangedIssue> changedIssues = Stream.of(
798 IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, status, project1, rule1)),
799 IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, status, project1, rule2)),
800 IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, status, project2, rule2)),
801 IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, status, project2Branch, rule1)),
803 IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, STATUS_REVIEWED, project1, hotspot1)),
804 IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, STATUS_REVIEWED, project1, hotspot2)),
805 IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, STATUS_TO_REVIEW, project2, hotspot2)),
806 IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, STATUS_TO_REVIEW, project2Branch, hotspot1)))
809 Collections.shuffle(changedIssues);
810 UserChange userChange = newUserChange();
811 when(emailSettings.getServerBaseURL()).thenReturn(host);
813 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
815 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
816 .hasParagraph().hasParagraph() // skip header
817 .hasParagraph(project1.getProjectName())
820 "Rule " + rule1.getName() + " - See all 39 issues",
821 "Rule " + rule2.getName() + " - See all 40 issues")
822 .withLink("See all 39 issues",
823 host + "/project/issues?id=" + project1.getKey()
824 + "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
825 .withLink("See all 40 issues",
826 host + "/project/issues?id=" + project1.getKey()
827 + "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
831 "Rule " + hotspot1.getName() + " - See all 39 hotspots",
832 "Rule " + hotspot2.getName() + " - See all 40 hotspots")
833 .withLink("See all 39 hotspots",
834 host + "/security_hotspots?id=" + project1.getKey()
835 + "&hotspots=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
836 .withLink("See all 40 hotspots",
837 host + "/security_hotspots?id=" + project1.getKey()
838 + "&hotspots=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
839 .hasParagraph(project2.getProjectName())
841 "Rule " + rule2.getName() + " - See issues 1-40 41-80 81")
843 host + "/project/issues?id=" + project2.getKey()
844 + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
846 host + "/project/issues?id=" + project2.getKey()
847 + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
849 host + "/project/issues?id=" + project2.getKey()
850 + "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
852 .hasList("Rule " + hotspot2.getName() + " - See hotspots 1-40 41-80 81")
854 host + "/security_hotspots?id=" + project2.getKey()
855 + "&hotspots=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
857 host + "/security_hotspots?id=" + project2.getKey()
858 + "&hotspots=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
860 host + "/security_hotspots?id=" + project2.getKey()
861 + "&hotspots=" + "1-40_41-80_1_9")
862 .hasParagraph(project2Branch.getProjectName() + ", " + project2Branch.getBranchName().get())
864 "Rule " + rule1.getName() + " - See all 6 issues")
865 .withLink("See all 6 issues",
866 host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
867 + "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
869 .hasList("Rule " + hotspot1.getName() + " - See all 6 hotspots")
870 .withLink("See all 6 hotspots",
871 host + "/security_hotspots?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
872 + "&hotspots=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
873 .hasParagraph().hasParagraph() // skip footer
877 private static String randomValidStatus() {
878 return ISSUE_STATUSES[new Random().nextInt(ISSUE_STATUSES.length)];
881 private void verifyEnd(HtmlListAssert htmlListAssert) {