]> source.dussan.org Git - sonarqube.git/blob
4b57f1288fddcf536af8798d0ac73e9875857f4a
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2021 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
20 package org.sonar.server.issue.notification;
21
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;
31 import java.util.Set;
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;
52
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;
77
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};
82
83   @org.junit.Rule
84   public ExpectedException expectedException = ExpectedException.none();
85
86   private I18n i18n = mock(I18n.class);
87   private EmailSettings emailSettings = mock(EmailSettings.class);
88   private ChangesOnMyIssuesEmailTemplate underTest = new ChangesOnMyIssuesEmailTemplate(i18n, emailSettings);
89
90   @Test
91   public void format_returns_null_on_Notification() {
92     EmailMessage emailMessage = underTest.format(mock(Notification.class));
93
94     assertThat(emailMessage).isNull();
95   }
96
97   @Test
98   public void formats_fails_with_ISE_if_change_from_Analysis_and_no_issue() {
99     AnalysisChange analysisChange = newAnalysisChange();
100
101     expectedException.expect(IllegalStateException.class);
102     expectedException.expectMessage("changedIssues can't be empty");
103
104     underTest.format(new ChangesOnMyIssuesNotification(analysisChange, Collections.emptySet()));
105   }
106
107   @Test
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)))
111       .collect(toSet());
112     AnalysisChange analysisChange = newAnalysisChange();
113
114     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
115
116     assertThat(emailMessage.getMessageId()).isEqualTo("changes-on-my-issues/" + changedIssues.iterator().next().getProject().getKey());
117   }
118
119   @Test
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)))
123       .collect(toSet());
124     AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
125
126     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
127
128     Project project = changedIssues.iterator().next().getProject();
129     assertThat(emailMessage.getSubject()).isEqualTo("Analysis has changed some of your issues in " + project.getProjectName());
130   }
131
132   @Test
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)))
136       .collect(toSet());
137     AnalysisChange analysisChange = newAnalysisChange();
138
139     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
140
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());
143   }
144
145   @Test
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)))
149       .collect(toSet());
150     AnalysisChange analysisChange = newAnalysisChange();
151
152     EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues.stream().limit(1).collect(toSet())));
153     EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
154
155     HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
156       .hasParagraph("Hi,")
157       .hasParagraph("An analysis has updated an issue assigned to you:");
158     HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
159       .hasParagraph("Hi,")
160       .hasParagraph("An analysis has updated issues assigned to you:");
161   }
162
163   @Test
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)))
167       .collect(toSet());
168     UserChange userChange = newUserChange();
169
170     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
171
172     assertThat(emailMessage.getMessageId()).isEqualTo("changes-on-my-issues");
173   }
174
175   @Test
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)))
179       .collect(toSet());
180     UserChange userChange = newUserChange();
181
182     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
183
184     assertThat(emailMessage.getSubject()).isEqualTo("A manual update has changed some of your issues/hotspots");
185   }
186
187   @Test
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)))
191       .collect(toSet());
192     UserChange userChange = newUserChange();
193
194     EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
195       userChange, changedIssues.stream().limit(1).collect(toSet())));
196     EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
197
198     HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
199       .hasParagraph("Hi,")
200       .withoutLink()
201       .hasParagraph("A manual change has updated an issue assigned to you:")
202       .withoutLink();
203     HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
204       .hasParagraph("Hi,")
205       .withoutLink()
206       .hasParagraph("A manual change has updated issues assigned to you:")
207       .withoutLink();
208   }
209
210   @Test
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)))
214       .collect(toSet());
215     UserChange userChange = newUserChange();
216
217     EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
218       userChange, changedIssues.stream().limit(1).collect(toSet())));
219     EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
220
221     HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
222       .hasParagraph("Hi,")
223       .withoutLink()
224       .hasParagraph("A manual change has updated a hotspot assigned to you:")
225       .withoutLink();
226     HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
227       .hasParagraph("Hi,")
228       .withoutLink()
229       .hasParagraph("A manual change has updated hotspots assigned to you:")
230       .withoutLink();
231   }
232
233   @Test
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)))
237       .collect(toSet());
238
239     Set<ChangedIssue> changedHotspots = IntStream.range(0, 2 + new Random().nextInt(4))
240       .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newSecurityHotspotRule("rule_" + i)))
241       .collect(toSet());
242
243     Set<ChangedIssue> issuesAndHotspots = Sets.union(changedIssues, changedHotspots);
244
245     UserChange userChange = newUserChange();
246
247     EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, issuesAndHotspots));
248
249     HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
250       .hasParagraph("Hi,")
251       .withoutLink()
252       .hasParagraph("A manual change has updated issues/hotspots assigned to you:")
253       .withoutLink();
254   }
255
256   @Test
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
261       // skip content
262       .hasParagraph() // skip project header
263       .hasList(), // rule list,
264       randomRuleTypeHotspotExcluded());
265   }
266
267   @Test
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());
275   }
276
277   @Test
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
282       .hasParagraph()
283       .hasList(), // rule list
284       SECURITY_HOTSPOT);
285   }
286
287   @Test
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
292       .hasParagraph()
293       .hasList(), // rule list
294       SECURITY_HOTSPOT);
295   }
296
297   @DataProvider
298   public static Object[][] issueStatuses() {
299     return Arrays.stream(ISSUE_STATUSES)
300       .map(t -> new Object[] {t})
301       .toArray(Object[][]::new);
302   }
303
304   @DataProvider
305   public static Object[][] securityHotspotsStatuses() {
306     return Arrays.stream(SECURITY_HOTSPOTS_STATUSES)
307       .map(t -> new Object[] {t})
308       .toArray(Object[][]::new);
309   }
310
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))
321       .collect(toSet());
322
323     EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
324       change, changedIssues.stream().limit(1).collect(toSet())));
325     EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(change, changedIssues));
326
327     Stream.of(singleIssueMessage, multiIssueMessage)
328       .forEach(issueMessage -> {
329         HtmlParagraphAssert htmlAssert = HtmlFragmentAssert.assertThat(issueMessage.getMessage())
330           .hasParagraph().hasParagraph(); // skip header
331         // skip content
332         HtmlListAssert htmlListAssert = skipContent.apply(htmlAssert);
333
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")
340           .noMoreBlock();
341       });
342   }
343
344   @Test
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))
350       .collect(toSet());
351     AnalysisChange analysisChange = newAnalysisChange();
352
353     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
354
355     HtmlListAssert htmlListAssert = HtmlFragmentAssert.assertThat(emailMessage.getMessage())
356       .hasParagraph().hasParagraph() // skip header
357       .hasParagraph("Closed issue:")
358       .withoutLink()
359       .hasList("Rule " + rule.getName() + " - See the single issue")
360       .withLinkOn("See the single issue")
361       .hasParagraph("Open issues:")
362       .withoutLink()
363       .hasList("Rule " + rule.getName() + " - See all " + (ISSUE_STATUSES.length - 1) + " issues")
364       .withLinkOn("See all " + (ISSUE_STATUSES.length - 1) + " issues");
365     verifyEnd(htmlListAssert);
366   }
367
368   @Test
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))
374       .collect(toSet());
375     Set<ChangedIssue> openIssues = IntStream.range(0, 2 + new Random().nextInt(5))
376       .mapToObj(status -> newChangedIssue(status + "", STATUS_OPEN, project, rule))
377       .collect(toSet());
378     AnalysisChange analysisChange = newAnalysisChange();
379
380     EmailMessage closedIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, closedIssues));
381     EmailMessage openIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, openIssues));
382
383     HtmlListAssert htmlListAssert = HtmlFragmentAssert.assertThat(closedIssuesMessage.getMessage())
384       .hasParagraph().hasParagraph() // skip header
385       .hasParagraph("Closed issues:")
386       .hasList();
387     verifyEnd(htmlListAssert);
388     htmlListAssert = HtmlFragmentAssert.assertThat(openIssuesMessage.getMessage())
389       .hasParagraph().hasParagraph() // skip header
390       .hasParagraph("Open issues:")
391       .hasList();
392     verifyEnd(htmlListAssert);
393   }
394
395   @Test
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);
403
404     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
405
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
412       .noMoreBlock();
413   }
414
415   @Test
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);
423
424     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
425
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
432       .noMoreBlock();
433   }
434
435   @Test
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);
441     String key = "key";
442     ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
443     AnalysisChange analysisChange = newAnalysisChange();
444     when(emailSettings.getServerBaseURL()).thenReturn(host);
445
446     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
447
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
455       .noMoreBlock();
456   }
457
458   @Test
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);
464     String key = "key";
465     ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
466     UserChange userChange = newUserChange();
467     when(emailSettings.getServerBaseURL()).thenReturn(host);
468
469     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
470
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
478       .noMoreBlock();
479   }
480
481   @Test
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))
490       .collect(toList());
491     AnalysisChange analysisChange = newAnalysisChange();
492     when(emailSettings.getServerBaseURL()).thenReturn(host);
493
494     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
495
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
505       .noMoreBlock();
506   }
507
508   @Test
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))
516       .collect(toList());
517     UserChange userChange = newUserChange();
518     when(emailSettings.getServerBaseURL()).thenReturn(host);
519
520     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
521
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
531       .noMoreBlock();
532   }
533
534   @Test
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))
544       .collect(toList());
545     AnalysisChange analysisChange = newAnalysisChange();
546     when(emailSettings.getServerBaseURL()).thenReturn(host);
547
548     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
549
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
559       .noMoreBlock();
560   }
561
562   @Test
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))
571       .collect(toList());
572     UserChange userChange = newUserChange();
573     when(emailSettings.getServerBaseURL()).thenReturn(host);
574
575     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
576
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
586       .noMoreBlock();
587   }
588
589   @Test
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())))
600       .collect(toList());
601     Collections.shuffle(changedIssues);
602     UserChange userChange = newUserChange();
603     when(emailSettings.getServerBaseURL()).thenReturn(host);
604
605     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
606
607     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
608       .hasParagraph().hasParagraph() // skip header
609       .hasParagraph(project1.getProjectName())
610       .hasList()
611       .hasParagraph(project1Branch1.getProjectName() + ", " + project1Branch1.getBranchName().get())
612       .hasList()
613       .hasParagraph(project1Branch2.getProjectName() + ", " + project1Branch2.getBranchName().get())
614       .hasList()
615       .hasParagraph(project2.getProjectName())
616       .hasList()
617       .hasParagraph(project2Branch1.getProjectName() + ", " + project2Branch1.getBranchName().get())
618       .hasList()
619       .hasParagraph(project3.getProjectName())
620       .hasList()
621       .hasParagraph().hasParagraph() // skip footer
622       .noMoreBlock();
623   }
624
625   @Test
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");
632
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))
637       .collect(toList());
638     Collections.shuffle(changedIssues);
639     AnalysisChange analysisChange = newAnalysisChange();
640     when(emailSettings.getServerBaseURL()).thenReturn(host);
641
642     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
643
644     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
645       .hasParagraph().hasParagraph() // skip header
646       .hasParagraph()// skip title based on status
647       .hasList(
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
653       .noMoreBlock();
654   }
655
656   @Test
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");
663
664     Rule hotspot1 = newSecurityHotspotRule("S");
665     Rule hotspot2 = newSecurityHotspotRule("Z");
666     Rule hotspot3 = newSecurityHotspotRule("N");
667     Rule hotspot4 = newSecurityHotspotRule("M");
668
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))
672       .collect(toList());
673     Collections.shuffle(changedIssues);
674     UserChange userChange = newUserChange();
675     when(emailSettings.getServerBaseURL()).thenReturn(host);
676
677     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
678
679     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
680       .hasParagraph()
681       .hasParagraph()
682       .hasParagraph() // skip project name
683       .hasList(
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")
688       .hasEmptyParagraph()
689       .hasList(
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
695       .noMoreBlock();
696   }
697
698   @Test
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");
703
704     String host = randomAlphabetic(15);
705     String issueStatusClosed = STATUS_CLOSED;
706     String otherIssueStatus = STATUS_RESOLVED;
707
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)))
713       .flatMap(t -> t)
714       .collect(toList());
715
716     Collections.shuffle(changedIssues);
717     AnalysisChange analysisChange = newAnalysisChange();
718     when(emailSettings.getServerBaseURL()).thenReturn(host);
719
720     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
721
722     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
723       .hasParagraph().hasParagraph() // skip header
724       .hasParagraph("Closed issues:") // skip title based on status
725       .hasList(
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:")
735       .hasList(
736         "Rule " + rule2.getName() + " - See issues 1-40 41-80 81",
737         "Rule " + rule1.getName() + " - See all 6 issues")
738       .withLink("1-40",
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")))
741       .withLink("41-80",
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")))
744       .withLink("81",
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
751       .noMoreBlock();
752   }
753
754   @Test
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());
761
762     Rule hotspot1 = newSecurityHotspotRule("h1");
763     Rule hotspot2 = newSecurityHotspotRule("h2");
764
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)),
772
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)))
777       .flatMap(t -> t)
778       .collect(toList());
779     Collections.shuffle(changedIssues);
780     UserChange userChange = newUserChange();
781     when(emailSettings.getServerBaseURL()).thenReturn(host);
782
783     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
784
785     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
786       .hasParagraph().hasParagraph() // skip header
787       .hasParagraph(project1.getProjectName())
788       .hasList()
789       .withItemTexts(
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")))
798       .hasEmptyParagraph()
799       .hasList()
800       .withItemTexts(
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())
810       .hasList(
811         "Rule " + rule2.getName() + " - See issues 1-40 41-80 81")
812       .withLink("1-40",
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")))
815       .withLink("41-80",
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")))
818       .withLink("81",
819         host + "/project/issues?id=" + project2.getKey()
820           + "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
821       .hasEmptyParagraph()
822       .hasList("Rule " + hotspot2.getName() + " - See hotspots 1-40 41-80 81")
823       .withLink("1-40",
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")))
826       .withLink("41-80",
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")))
829       .withLink("81",
830         host + "/security_hotspots?id=" + project2.getKey()
831           + "&hotspots=" + "1-40_41-80_1_9")
832       .hasParagraph(project2Branch.getProjectName() + ", " + project2Branch.getBranchName().get())
833       .hasList(
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")))
838       .hasEmptyParagraph()
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
844       .noMoreBlock();
845   }
846
847   private static String randomValidStatus() {
848     return ISSUE_STATUSES[new Random().nextInt(ISSUE_STATUSES.length)];
849   }
850
851   private void verifyEnd(HtmlListAssert htmlListAssert) {
852     htmlListAssert
853       .hasEmptyParagraph()
854       .hasParagraph()
855       .noMoreBlock();
856   }
857
858 }