3 * Copyright (C) 2009-2021 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.rules.ExpectedException;
38 import org.junit.runner.RunWith;
39 import org.sonar.api.config.EmailSettings;
40 import org.sonar.api.notifications.Notification;
41 import org.sonar.api.rules.RuleType;
42 import org.sonar.core.i18n.I18n;
43 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
44 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
45 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
46 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
47 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
48 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
49 import org.sonar.test.html.HtmlFragmentAssert;
50 import org.sonar.test.html.HtmlListAssert;
51 import org.sonar.test.html.HtmlParagraphAssert;
53 import static java.util.stream.Collectors.joining;
54 import static java.util.stream.Collectors.toList;
55 import static java.util.stream.Collectors.toSet;
56 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
57 import static org.assertj.core.api.Assertions.assertThat;
58 import static org.mockito.Mockito.mock;
59 import static org.mockito.Mockito.when;
60 import static org.sonar.api.issue.Issue.STATUS_CLOSED;
61 import static org.sonar.api.issue.Issue.STATUS_CONFIRMED;
62 import static org.sonar.api.issue.Issue.STATUS_OPEN;
63 import static org.sonar.api.issue.Issue.STATUS_REOPENED;
64 import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
65 import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
66 import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
67 import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
68 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newAnalysisChange;
69 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newBranch;
70 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newChangedIssue;
71 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject;
72 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
73 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRule;
74 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newSecurityHotspotRule;
75 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newUserChange;
76 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.randomRuleTypeHotspotExcluded;
78 @RunWith(DataProviderRunner.class)
79 public class ChangesOnMyIssuesEmailTemplateTest {
80 private static final String[] ISSUE_STATUSES = {STATUS_OPEN, STATUS_RESOLVED, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_CLOSED};
81 private static final String[] SECURITY_HOTSPOTS_STATUSES = {STATUS_TO_REVIEW, STATUS_REVIEWED};
84 public ExpectedException expectedException = ExpectedException.none();
86 private I18n i18n = mock(I18n.class);
87 private EmailSettings emailSettings = mock(EmailSettings.class);
88 private ChangesOnMyIssuesEmailTemplate underTest = new ChangesOnMyIssuesEmailTemplate(i18n, emailSettings);
91 public void format_returns_null_on_Notification() {
92 EmailMessage emailMessage = underTest.format(mock(Notification.class));
94 assertThat(emailMessage).isNull();
98 public void formats_fails_with_ISE_if_change_from_Analysis_and_no_issue() {
99 AnalysisChange analysisChange = newAnalysisChange();
101 expectedException.expect(IllegalStateException.class);
102 expectedException.expectMessage("changedIssues can't be empty");
104 underTest.format(new ChangesOnMyIssuesNotification(analysisChange, Collections.emptySet()));
108 public void format_sets_message_id_with_project_key_of_first_issue_in_set_when_change_from_Analysis() {
109 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
110 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
112 AnalysisChange analysisChange = newAnalysisChange();
114 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
116 assertThat(emailMessage.getMessageId()).isEqualTo("changes-on-my-issues/" + changedIssues.iterator().next().getProject().getKey());
120 public void format_sets_subject_with_project_name_of_first_issue_in_set_when_change_from_Analysis() {
121 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
122 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
124 AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
126 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
128 Project project = changedIssues.iterator().next().getProject();
129 assertThat(emailMessage.getSubject()).isEqualTo("Analysis has changed some of your issues in " + project.getProjectName());
133 public void format_sets_subject_with_project_name_and_branch_name_of_first_issue_in_set_when_change_from_Analysis() {
134 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
135 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newBranch("prj_" + i, "br_" + i), newRandomNotAHotspotRule("rule_" + i)))
137 AnalysisChange analysisChange = newAnalysisChange();
139 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
141 Project project = changedIssues.iterator().next().getProject();
142 assertThat(emailMessage.getSubject()).isEqualTo("Analysis has changed some of your issues in " + project.getProjectName() + ", " + project.getBranchName().get());
146 public void format_set_html_message_with_header_dealing_with_plural_when_change_from_Analysis() {
147 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
148 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
150 AnalysisChange analysisChange = newAnalysisChange();
152 EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues.stream().limit(1).collect(toSet())));
153 EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
155 HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
157 .hasParagraph("An analysis has updated an issue assigned to you:");
158 HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
160 .hasParagraph("An analysis has updated issues assigned to you:");
164 public void format_sets_static_message_id_when_change_from_User() {
165 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
166 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
168 UserChange userChange = newUserChange();
170 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
172 assertThat(emailMessage.getMessageId()).isEqualTo("changes-on-my-issues");
176 public void format_sets_static_subject_when_change_from_User() {
177 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
178 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
180 UserChange userChange = newUserChange();
182 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
184 assertThat(emailMessage.getSubject()).isEqualTo("A manual update has changed some of your issues/hotspots");
188 public void format_set_html_message_with_header_dealing_with_plural_issues_when_change_from_User() {
189 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
190 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
192 UserChange userChange = newUserChange();
194 EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
195 userChange, changedIssues.stream().limit(1).collect(toSet())));
196 EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
198 HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
201 .hasParagraph("A manual change has updated an issue assigned to you:")
203 HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
206 .hasParagraph("A manual change has updated issues assigned to you:")
211 public void format_set_html_message_with_header_dealing_with_plural_security_hotspots_when_change_from_User() {
212 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
213 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newSecurityHotspotRule("rule_" + i)))
215 UserChange userChange = newUserChange();
217 EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
218 userChange, changedIssues.stream().limit(1).collect(toSet())));
219 EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
221 HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
224 .hasParagraph("A manual change has updated a hotspot assigned to you:")
226 HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
229 .hasParagraph("A manual change has updated hotspots assigned to you:")
234 public void format_set_html_message_with_header_dealing_with_plural_security_hotspots_and_issues_when_change_from_User() {
235 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
236 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
239 Set<ChangedIssue> changedHotspots = IntStream.range(0, 2 + new Random().nextInt(4))
240 .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newSecurityHotspotRule("rule_" + i)))
243 Set<ChangedIssue> issuesAndHotspots = Sets.union(changedIssues, changedHotspots);
245 UserChange userChange = newUserChange();
247 EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, issuesAndHotspots));
249 HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
252 .hasParagraph("A manual change has updated issues/hotspots assigned to you:")
257 @UseDataProvider("issueStatuses")
258 public void format_set_html_message_with_footer_when_issue_change_from_user(String issueStatus) {
259 UserChange userChange = newUserChange();
260 format_set_html_message_with_footer(userChange, issueStatus, c -> c
262 .hasParagraph() // skip project header
263 .hasList(), // rule list,
264 randomRuleTypeHotspotExcluded());
268 @UseDataProvider("issueStatuses")
269 public void format_set_html_message_with_footer_when_issue_change_from_analysis(String issueStatus) {
270 AnalysisChange analysisChange = newAnalysisChange();
271 format_set_html_message_with_footer(analysisChange, issueStatus, c -> c
272 .hasParagraph() // status
273 .hasList(), // rule list,
274 randomRuleTypeHotspotExcluded());
278 @UseDataProvider("securityHotspotsStatuses")
279 public void format_set_html_message_with_footer_when_security_hotspot_change_from_analysis(String securityHotspotStatus) {
280 AnalysisChange analysisChange = newAnalysisChange();
281 format_set_html_message_with_footer(analysisChange, securityHotspotStatus, c -> c
283 .hasList(), // rule list
288 @UseDataProvider("securityHotspotsStatuses")
289 public void format_set_html_message_with_footer_when_security_hotspot_change_from_user(String securityHotspotStatus) {
290 UserChange userChange = newUserChange();
291 format_set_html_message_with_footer(userChange, securityHotspotStatus, c -> c
293 .hasList(), // rule list
298 public static Object[][] issueStatuses() {
299 return Arrays.stream(ISSUE_STATUSES)
300 .map(t -> new Object[] {t})
301 .toArray(Object[][]::new);
305 public static Object[][] securityHotspotsStatuses() {
306 return Arrays.stream(SECURITY_HOTSPOTS_STATUSES)
307 .map(t -> new Object[] {t})
308 .toArray(Object[][]::new);
311 private void format_set_html_message_with_footer(Change change, String issueStatus, Function<HtmlParagraphAssert, HtmlListAssert> skipContent, RuleType ruleType) {
312 String wordingNotification = randomAlphabetic(20);
313 String host = randomAlphabetic(15);
314 when(i18n.message(Locale.ENGLISH, "notification.dispatcher.ChangesOnMyIssue", "notification.dispatcher.ChangesOnMyIssue"))
315 .thenReturn(wordingNotification);
316 when(emailSettings.getServerBaseURL()).thenReturn(host);
317 Project project = newProject("foo");
318 Rule rule = newRule("bar", ruleType);
319 Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
320 .mapToObj(i -> newChangedIssue(i + "", issueStatus, project, rule))
323 EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
324 change, changedIssues.stream().limit(1).collect(toSet())));
325 EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(change, changedIssues));
327 Stream.of(singleIssueMessage, multiIssueMessage)
328 .forEach(issueMessage -> {
329 HtmlParagraphAssert htmlAssert = HtmlFragmentAssert.assertThat(issueMessage.getMessage())
330 .hasParagraph().hasParagraph(); // skip header
332 HtmlListAssert htmlListAssert = skipContent.apply(htmlAssert);
334 String footerText = "You received this email because you are subscribed to \"" + wordingNotification + "\" notifications from SonarQube."
335 + " Click here to edit your email preferences.";
336 htmlListAssert.hasEmptyParagraph()
337 .hasParagraph(footerText)
338 .withSmallOn(footerText)
339 .withLink("here", host + "/account/notifications")
345 public void format_set_html_message_with_issues_grouped_by_status_closed_or_any_other_when_change_from_analysis() {
346 Project project = newProject("foo");
347 Rule rule = newRandomNotAHotspotRule("bar");
348 Set<ChangedIssue> changedIssues = Arrays.stream(ISSUE_STATUSES)
349 .map(status -> newChangedIssue(status + "", status, project, rule))
351 AnalysisChange analysisChange = newAnalysisChange();
353 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
355 HtmlListAssert htmlListAssert = HtmlFragmentAssert.assertThat(emailMessage.getMessage())
356 .hasParagraph().hasParagraph() // skip header
357 .hasParagraph("Closed issue:")
359 .hasList("Rule " + rule.getName() + " - See the single issue")
360 .withLinkOn("See the single issue")
361 .hasParagraph("Open issues:")
363 .hasList("Rule " + rule.getName() + " - See all " + (ISSUE_STATUSES.length - 1) + " issues")
364 .withLinkOn("See all " + (ISSUE_STATUSES.length - 1) + " issues");
365 verifyEnd(htmlListAssert);
369 public void format_set_html_message_with_issue_status_title_handles_plural_when_change_from_analysis() {
370 Project project = newProject("foo");
371 Rule rule = newRandomNotAHotspotRule("bar");
372 Set<ChangedIssue> closedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
373 .mapToObj(status -> newChangedIssue(status + "", STATUS_CLOSED, project, rule))
375 Set<ChangedIssue> openIssues = IntStream.range(0, 2 + new Random().nextInt(5))
376 .mapToObj(status -> newChangedIssue(status + "", STATUS_OPEN, project, rule))
378 AnalysisChange analysisChange = newAnalysisChange();
380 EmailMessage closedIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, closedIssues));
381 EmailMessage openIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, openIssues));
383 HtmlListAssert htmlListAssert = HtmlFragmentAssert.assertThat(closedIssuesMessage.getMessage())
384 .hasParagraph().hasParagraph() // skip header
385 .hasParagraph("Closed issues:")
387 verifyEnd(htmlListAssert);
388 htmlListAssert = HtmlFragmentAssert.assertThat(openIssuesMessage.getMessage())
389 .hasParagraph().hasParagraph() // skip header
390 .hasParagraph("Open issues:")
392 verifyEnd(htmlListAssert);
396 public void formats_returns_html_message_for_single_issue_on_master_when_analysis_change() {
397 Project project = newProject("1");
398 String ruleName = randomAlphabetic(8);
399 String host = randomAlphabetic(15);
400 ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
401 AnalysisChange analysisChange = newAnalysisChange();
402 when(emailSettings.getServerBaseURL()).thenReturn(host);
404 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
406 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
407 .hasParagraph().hasParagraph() // skip header
408 .hasParagraph()// skip title based on status
409 .hasList("Rule " + ruleName + " - See the single issue")
410 .withLink("See the single issue", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
411 .hasParagraph().hasParagraph() // skip footer
416 public void formats_returns_html_message_for_single_issue_on_master_when_user_change() {
417 Project project = newProject("1");
418 String ruleName = randomAlphabetic(8);
419 String host = randomAlphabetic(15);
420 ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
421 UserChange userChange = newUserChange();
422 when(emailSettings.getServerBaseURL()).thenReturn(host);
424 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
426 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
427 .hasParagraph().hasParagraph() // skip header
428 .hasParagraph(project.getProjectName())
429 .hasList("Rule " + ruleName + " - See the single issue")
430 .withLink("See the single issue", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
431 .hasParagraph().hasParagraph() // skip footer
436 public void formats_returns_html_message_for_single_issue_on_branch_when_analysis_change() {
437 String branchName = randomAlphabetic(6);
438 Project project = newBranch("1", branchName);
439 String ruleName = randomAlphabetic(8);
440 String host = randomAlphabetic(15);
442 ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
443 AnalysisChange analysisChange = newAnalysisChange();
444 when(emailSettings.getServerBaseURL()).thenReturn(host);
446 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
448 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
449 .hasParagraph().hasParagraph() // skip header
450 .hasParagraph()// skip title based on status
451 .hasList("Rule " + ruleName + " - See the single issue")
452 .withLink("See the single issue",
453 host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
454 .hasParagraph().hasParagraph() // skip footer
459 public void formats_returns_html_message_for_single_issue_on_branch_when_user_change() {
460 String branchName = randomAlphabetic(6);
461 Project project = newBranch("1", branchName);
462 String ruleName = randomAlphabetic(8);
463 String host = randomAlphabetic(15);
465 ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
466 UserChange userChange = newUserChange();
467 when(emailSettings.getServerBaseURL()).thenReturn(host);
469 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
471 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
472 .hasParagraph().hasParagraph() // skip header
473 .hasParagraph(project.getProjectName() + ", " + branchName)
474 .hasList("Rule " + ruleName + " - See the single issue")
475 .withLink("See the single issue",
476 host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
477 .hasParagraph().hasParagraph() // skip footer
482 public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master_when_analysis_change() {
483 Project project = newProject("1");
484 String ruleName = randomAlphabetic(8);
485 String host = randomAlphabetic(15);
486 Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
487 String issueStatus = randomValidStatus();
488 List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
489 .mapToObj(i -> newChangedIssue("issue_" + i, issueStatus, project, rule))
491 AnalysisChange analysisChange = newAnalysisChange();
492 when(emailSettings.getServerBaseURL()).thenReturn(host);
494 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
496 String expectedHref = host + "/project/issues?id=" + project.getKey()
497 + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
498 String expectedLinkText = "See all " + changedIssues.size() + " issues";
499 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
500 .hasParagraph().hasParagraph() // skip header
501 .hasParagraph() // skip title based on status
502 .hasList("Rule " + ruleName + " - " + expectedLinkText)
503 .withLink(expectedLinkText, expectedHref)
504 .hasParagraph().hasParagraph() // skip footer
509 public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master_when_user_change() {
510 Project project = newProject("1");
511 String ruleName = randomAlphabetic(8);
512 String host = randomAlphabetic(15);
513 Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
514 List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
515 .mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule))
517 UserChange userChange = newUserChange();
518 when(emailSettings.getServerBaseURL()).thenReturn(host);
520 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
522 String expectedHref = host + "/project/issues?id=" + project.getKey()
523 + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
524 String expectedLinkText = "See all " + changedIssues.size() + " issues";
525 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
526 .hasParagraph().hasParagraph() // skip header
527 .hasParagraph(project.getProjectName())
528 .hasList("Rule " + ruleName + " - " + expectedLinkText)
529 .withLink(expectedLinkText, expectedHref)
530 .hasParagraph().hasParagraph() // skip footer
535 public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch_when_analysis_change() {
536 String branchName = randomAlphabetic(19);
537 Project project = newBranch("1", branchName);
538 String ruleName = randomAlphabetic(8);
539 String host = randomAlphabetic(15);
540 Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
541 String status = randomValidStatus();
542 List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
543 .mapToObj(i -> newChangedIssue("issue_" + i, status, project, rule))
545 AnalysisChange analysisChange = newAnalysisChange();
546 when(emailSettings.getServerBaseURL()).thenReturn(host);
548 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
550 String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName
551 + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
552 String expectedLinkText = "See all " + changedIssues.size() + " issues";
553 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
554 .hasParagraph().hasParagraph() // skip header
555 .hasParagraph()// skip title based on status
556 .hasList("Rule " + ruleName + " - " + expectedLinkText)
557 .withLink(expectedLinkText, expectedHref)
558 .hasParagraph().hasParagraph() // skip footer
563 public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch_when_user_change() {
564 String branchName = randomAlphabetic(19);
565 Project project = newBranch("1", branchName);
566 String ruleName = randomAlphabetic(8);
567 String host = randomAlphabetic(15);
568 Rule rule = newRandomNotAHotspotRule(ruleName);
569 List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
570 .mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule))
572 UserChange userChange = newUserChange();
573 when(emailSettings.getServerBaseURL()).thenReturn(host);
575 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
577 String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName
578 + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
579 String expectedLinkText = "See all " + changedIssues.size() + " issues";
580 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
581 .hasParagraph().hasParagraph() // skip header
582 .hasParagraph(project.getProjectName() + ", " + branchName)
583 .hasList("Rule " + ruleName + " - " + expectedLinkText)
584 .withLink(expectedLinkText, expectedHref)
585 .hasParagraph().hasParagraph() // skip footer
590 public void formats_returns_html_message_with_projects_ordered_by_name_when_user_change() {
591 Project project1 = newProject("1");
592 Project project1Branch1 = newBranch("1", "a");
593 Project project1Branch2 = newBranch("1", "b");
594 Project project2 = newProject("B");
595 Project project2Branch1 = newBranch("B", "a");
596 Project project3 = newProject("C");
597 String host = randomAlphabetic(15);
598 List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3)
599 .map(project -> newChangedIssue("issue_" + project.getUuid(), randomValidStatus(), project, newRule(randomAlphabetic(2), randomRuleTypeHotspotExcluded())))
601 Collections.shuffle(changedIssues);
602 UserChange userChange = newUserChange();
603 when(emailSettings.getServerBaseURL()).thenReturn(host);
605 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
607 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
608 .hasParagraph().hasParagraph() // skip header
609 .hasParagraph(project1.getProjectName())
611 .hasParagraph(project1Branch1.getProjectName() + ", " + project1Branch1.getBranchName().get())
613 .hasParagraph(project1Branch2.getProjectName() + ", " + project1Branch2.getBranchName().get())
615 .hasParagraph(project2.getProjectName())
617 .hasParagraph(project2Branch1.getProjectName() + ", " + project2Branch1.getBranchName().get())
619 .hasParagraph(project3.getProjectName())
621 .hasParagraph().hasParagraph() // skip footer
626 public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change() {
627 Project project = newProject("1");
628 Rule rule1 = newRandomNotAHotspotRule("1");
629 Rule rule2 = newRandomNotAHotspotRule("a");
630 Rule rule3 = newRandomNotAHotspotRule("b");
631 Rule rule4 = newRandomNotAHotspotRule("X");
633 String host = randomAlphabetic(15);
634 String issueStatus = randomValidStatus();
635 List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
636 .map(rule -> newChangedIssue("issue_" + rule.getName(), issueStatus, project, rule))
638 Collections.shuffle(changedIssues);
639 AnalysisChange analysisChange = newAnalysisChange();
640 when(emailSettings.getServerBaseURL()).thenReturn(host);
642 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
644 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
645 .hasParagraph().hasParagraph() // skip header
646 .hasParagraph()// skip title based on status
648 "Rule " + rule1.getName() + " - See the single issue",
649 "Rule " + rule2.getName() + " - See the single issue",
650 "Rule " + rule3.getName() + " - See the single issue",
651 "Rule " + rule4.getName() + " - See the single issue")
652 .hasParagraph().hasParagraph() // skip footer
657 public void formats_returns_html_message_with_rules_ordered_by_name_user_change() {
658 Project project = newProject("1");
659 Rule rule1 = newRandomNotAHotspotRule("1");
660 Rule rule2 = newRandomNotAHotspotRule("a");
661 Rule rule3 = newRandomNotAHotspotRule("b");
662 Rule rule4 = newRandomNotAHotspotRule("X");
664 Rule hotspot1 = newSecurityHotspotRule("S");
665 Rule hotspot2 = newSecurityHotspotRule("Z");
666 Rule hotspot3 = newSecurityHotspotRule("N");
667 Rule hotspot4 = newSecurityHotspotRule("M");
669 String host = randomAlphabetic(15);
670 List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4, hotspot1, hotspot2, hotspot3, hotspot4)
671 .map(rule -> newChangedIssue("issue_" + rule.getName(), randomValidStatus(), project, rule))
673 Collections.shuffle(changedIssues);
674 UserChange userChange = newUserChange();
675 when(emailSettings.getServerBaseURL()).thenReturn(host);
677 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
679 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
682 .hasParagraph() // skip project name
684 "Rule " + rule1.getName() + " - See the single issue",
685 "Rule " + rule2.getName() + " - See the single issue",
686 "Rule " + rule3.getName() + " - See the single issue",
687 "Rule " + rule4.getName() + " - See the single issue")
690 "Rule " + hotspot1.getName() + " - See the single hotspot",
691 "Rule " + hotspot2.getName() + " - See the single hotspot",
692 "Rule " + hotspot3.getName() + " - See the single hotspot",
693 "Rule " + hotspot4.getName() + " - See the single hotspot")
694 .hasParagraph().hasParagraph() // skip footer
699 public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_when_analysis_change() {
700 Project project1 = newProject("1");
701 Rule rule1 = newRandomNotAHotspotRule("1");
702 Rule rule2 = newRandomNotAHotspotRule("a");
704 String host = randomAlphabetic(15);
705 String issueStatusClosed = STATUS_CLOSED;
706 String otherIssueStatus = STATUS_RESOLVED;
708 List<ChangedIssue> changedIssues = Stream.of(
709 IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, issueStatusClosed, project1, rule1)),
710 IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, issueStatusClosed, project1, rule2)),
711 IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, otherIssueStatus, project1, rule2)),
712 IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, otherIssueStatus, project1, rule1)))
716 Collections.shuffle(changedIssues);
717 AnalysisChange analysisChange = newAnalysisChange();
718 when(emailSettings.getServerBaseURL()).thenReturn(host);
720 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
722 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
723 .hasParagraph().hasParagraph() // skip header
724 .hasParagraph("Closed issues:") // skip title based on status
726 "Rule " + rule1.getName() + " - See all 39 issues",
727 "Rule " + rule2.getName() + " - See all 40 issues")
728 .withLink("See all 39 issues",
729 host + "/project/issues?id=" + project1.getKey()
730 + "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
731 .withLink("See all 40 issues",
732 host + "/project/issues?id=" + project1.getKey()
733 + "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
734 .hasParagraph("Open issues:")
736 "Rule " + rule2.getName() + " - See issues 1-40 41-80 81",
737 "Rule " + rule1.getName() + " - See all 6 issues")
739 host + "/project/issues?id=" + project1.getKey()
740 + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
742 host + "/project/issues?id=" + project1.getKey()
743 + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
745 host + "/project/issues?id=" + project1.getKey()
746 + "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
747 .withLink("See all 6 issues",
748 host + "/project/issues?id=" + project1.getKey()
749 + "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
750 .hasParagraph().hasParagraph() // skip footer
755 public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_and_hotspots_when_user_change() {
756 Project project1 = newProject("1");
757 Project project2 = newProject("V");
758 Project project2Branch = newBranch("V", "AB");
759 Rule rule1 = newRule("1", randomRuleTypeHotspotExcluded());
760 Rule rule2 = newRule("a", randomRuleTypeHotspotExcluded());
762 Rule hotspot1 = newSecurityHotspotRule("h1");
763 Rule hotspot2 = newSecurityHotspotRule("h2");
765 String status = randomValidStatus();
766 String host = randomAlphabetic(15);
767 List<ChangedIssue> changedIssues = Stream.of(
768 IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, status, project1, rule1)),
769 IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, status, project1, rule2)),
770 IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, status, project2, rule2)),
771 IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, status, project2Branch, rule1)),
773 IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, STATUS_REVIEWED, project1, hotspot1)),
774 IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, STATUS_REVIEWED, project1, hotspot2)),
775 IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, STATUS_TO_REVIEW, project2, hotspot2)),
776 IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, STATUS_TO_REVIEW, project2Branch, hotspot1)))
779 Collections.shuffle(changedIssues);
780 UserChange userChange = newUserChange();
781 when(emailSettings.getServerBaseURL()).thenReturn(host);
783 EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
785 HtmlFragmentAssert.assertThat(emailMessage.getMessage())
786 .hasParagraph().hasParagraph() // skip header
787 .hasParagraph(project1.getProjectName())
790 "Rule " + rule1.getName() + " - See all 39 issues",
791 "Rule " + rule2.getName() + " - See all 40 issues")
792 .withLink("See all 39 issues",
793 host + "/project/issues?id=" + project1.getKey()
794 + "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
795 .withLink("See all 40 issues",
796 host + "/project/issues?id=" + project1.getKey()
797 + "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
801 "Rule " + hotspot1.getName() + " - See all 39 hotspots",
802 "Rule " + hotspot2.getName() + " - See all 40 hotspots")
803 .withLink("See all 39 hotspots",
804 host + "/security_hotspots?id=" + project1.getKey()
805 + "&hotspots=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
806 .withLink("See all 40 hotspots",
807 host + "/security_hotspots?id=" + project1.getKey()
808 + "&hotspots=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
809 .hasParagraph(project2.getProjectName())
811 "Rule " + rule2.getName() + " - See issues 1-40 41-80 81")
813 host + "/project/issues?id=" + project2.getKey()
814 + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
816 host + "/project/issues?id=" + project2.getKey()
817 + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
819 host + "/project/issues?id=" + project2.getKey()
820 + "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
822 .hasList("Rule " + hotspot2.getName() + " - See hotspots 1-40 41-80 81")
824 host + "/security_hotspots?id=" + project2.getKey()
825 + "&hotspots=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
827 host + "/security_hotspots?id=" + project2.getKey()
828 + "&hotspots=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
830 host + "/security_hotspots?id=" + project2.getKey()
831 + "&hotspots=" + "1-40_41-80_1_9")
832 .hasParagraph(project2Branch.getProjectName() + ", " + project2Branch.getBranchName().get())
834 "Rule " + rule1.getName() + " - See all 6 issues")
835 .withLink("See all 6 issues",
836 host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
837 + "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
839 .hasList("Rule " + hotspot1.getName() + " - See all 6 hotspots")
840 .withLink("See all 6 hotspots",
841 host + "/security_hotspots?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
842 + "&hotspots=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
843 .hasParagraph().hasParagraph() // skip footer
847 private static String randomValidStatus() {
848 return ISSUE_STATUSES[new Random().nextInt(ISSUE_STATUSES.length)];
851 private void verifyEnd(HtmlListAssert htmlListAssert) {