]> source.dussan.org Git - sonarqube.git/blob
b3a7700a67170fc506dc06fd9c77f5ac8fa6e863
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 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.Collections;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.Random;
30 import java.util.stream.IntStream;
31 import java.util.stream.Stream;
32 import org.junit.Test;
33 import org.junit.runner.RunWith;
34 import org.sonar.api.config.EmailSettings;
35 import org.sonar.api.notifications.Notification;
36 import org.sonar.api.rule.RuleKey;
37 import org.sonar.api.rules.RuleType;
38 import org.sonar.core.i18n.I18n;
39 import org.sonar.server.issue.notification.FPOrAcceptedNotification.FpPrAccepted;
40 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
41 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
42 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
43 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
44 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
45 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
46 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
47 import org.sonar.test.html.HtmlFragmentAssert;
48
49 import static java.util.stream.Collectors.joining;
50 import static java.util.stream.Collectors.toList;
51 import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
52 import static org.assertj.core.api.Assertions.assertThat;
53 import static org.mockito.Mockito.mock;
54 import static org.mockito.Mockito.when;
55 import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
56 import static org.sonar.server.issue.notification.FPOrAcceptedNotification.FpPrAccepted.ACCEPTED;
57 import static org.sonar.server.issue.notification.FPOrAcceptedNotification.FpPrAccepted.FP;
58 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
59 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newSecurityHotspotRule;
60 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.randomRuleTypeHotspotExcluded;
61
62 @RunWith(DataProviderRunner.class)
63 public class FpPrAcceptedEmailTemplateTest {
64   private I18n i18n = mock(I18n.class);
65   private EmailSettings emailSettings = mock(EmailSettings.class);
66   private FpOrAcceptedEmailTemplate underTest = new FpOrAcceptedEmailTemplate(i18n, emailSettings);
67
68   @Test
69   public void format_returns_null_on_Notification() {
70     EmailMessage emailMessage = underTest.format(mock(Notification.class));
71
72     assertThat(emailMessage).isNull();
73   }
74
75   @Test
76   public void format_sets_message_id_specific_to_fp() {
77     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(mock(Change.class), Collections.emptySet(), FP));
78
79     assertThat(emailMessage.getMessageId()).isEqualTo("fp-issue-changes");
80   }
81
82   @Test
83   public void format_sets_message_id_specific_to_wont_fix() {
84     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(mock(Change.class), Collections.emptySet(), ACCEPTED));
85
86     assertThat(emailMessage.getMessageId()).isEqualTo("accepted-issue-changes");
87   }
88
89   @Test
90   public void format_sets_subject_specific_to_fp() {
91     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(mock(Change.class), Collections.emptySet(), FP));
92
93     assertThat(emailMessage.getSubject()).isEqualTo("Issues marked as False Positive");
94   }
95
96   @Test
97   public void format_sets_subject_specific_to_wont_fix() {
98     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(mock(Change.class), Collections.emptySet(), ACCEPTED));
99
100     assertThat(emailMessage.getSubject()).isEqualTo("Issues marked as Accepted");
101   }
102
103   @Test
104   public void format_sets_from_to_name_of_author_change_when_available() {
105     UserChange change = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6), randomAlphabetic(7)));
106     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, Collections.emptySet(), ACCEPTED));
107
108     assertThat(emailMessage.getFrom()).isEqualTo(change.getUser().getName().get());
109   }
110
111   @Test
112   public void format_sets_from_to_login_of_author_change_when_name_is_not_available() {
113     UserChange change = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6), null));
114     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, Collections.emptySet(), ACCEPTED));
115
116     assertThat(emailMessage.getFrom()).isEqualTo(change.getUser().getLogin());
117   }
118
119   @Test
120   public void format_sets_from_to_null_when_analysisChange() {
121     AnalysisChange change = new AnalysisChange(new Random().nextLong());
122     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, Collections.emptySet(), ACCEPTED));
123
124     assertThat(emailMessage.getFrom()).isNull();
125   }
126
127   @Test
128   @UseDataProvider("userOrAnalysisChange")
129   public void formats_returns_html_message_with_only_footer_and_header_when_no_issue_for_FPs(Change change) {
130     formats_returns_html_message_with_only_footer_and_header_when_no_issue(change, FP, "False Positive");
131   }
132
133   @Test
134   @UseDataProvider("userOrAnalysisChange")
135   public void formats_returns_html_message_with_only_footer_and_header_when_no_issue_for_Wont_fixs(Change change) {
136     formats_returns_html_message_with_only_footer_and_header_when_no_issue(change, ACCEPTED, "Accepted");
137   }
138
139   public void formats_returns_html_message_with_only_footer_and_header_when_no_issue(Change change, FpPrAccepted fpPrAccepted, String fpOrWontFixLabel) {
140     String wordingNotification = randomAlphabetic(20);
141     String host = randomAlphabetic(15);
142     when(i18n.message(Locale.ENGLISH, "notification.dispatcher.NewFalsePositiveIssue", "notification.dispatcher.NewFalsePositiveIssue"))
143       .thenReturn(wordingNotification);
144     when(emailSettings.getServerBaseURL()).thenReturn(host);
145
146     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, Collections.emptySet(), fpPrAccepted));
147
148     String footerText = "You received this email because you are subscribed to \"" + wordingNotification + "\" notifications from SonarQube."
149       + " Click here to edit your email preferences.";
150     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
151       .hasParagraph("Hi,")
152       .withoutLink()
153       .hasParagraph("A manual change has resolved an issue as " + fpOrWontFixLabel + ":")
154       .withoutLink()
155       .hasEmptyParagraph()
156       .hasParagraph(footerText)
157       .withSmallOn(footerText)
158       .withLink("here", host + "/account/notifications")
159       .noMoreBlock();
160   }
161
162   @Test
163   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
164   public void formats_returns_html_message_for_single_issue_on_master(Change change, FpPrAccepted fpPrAccepted) {
165     Project project = newProject("1");
166     String ruleName = randomAlphabetic(8);
167     String host = randomAlphabetic(15);
168     ChangedIssue changedIssue = newChangedIssue("key", project, ruleName, randomRuleTypeHotspotExcluded());
169     when(emailSettings.getServerBaseURL()).thenReturn(host);
170
171     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, ImmutableSet.of(changedIssue), fpPrAccepted));
172
173     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
174       .hasParagraph().hasParagraph() // skip header
175       .hasParagraph(project.getProjectName())
176       .hasList("Rule " + ruleName + " - See the single issue")
177       .withLink("See the single issue", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
178       .hasParagraph().hasParagraph() // skip footer
179       .noMoreBlock();
180   }
181
182   @Test
183   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
184   public void formats_returns_html_message_for_single_hotspot_on_master(Change change, FpPrAccepted fpPrAccepted) {
185     Project project = newProject("1");
186     String ruleName = randomAlphabetic(8);
187     String host = randomAlphabetic(15);
188     ChangedIssue changedIssue = newChangedIssue("key", project, ruleName, SECURITY_HOTSPOT);
189     when(emailSettings.getServerBaseURL()).thenReturn(host);
190
191     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, ImmutableSet.of(changedIssue), fpPrAccepted));
192
193     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
194       .hasParagraph().hasParagraph() // skip header
195       .hasParagraph(project.getProjectName())
196       .hasList("Rule " + ruleName + " - See the single hotspot")
197       .withLink("See the single hotspot", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
198       .hasParagraph().hasParagraph() // skip footer
199       .noMoreBlock();
200   }
201
202   @Test
203   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
204   public void formats_returns_html_message_for_single_issue_on_branch(Change change, FpPrAccepted fpPrAccepted) {
205     String branchName = randomAlphabetic(6);
206     Project project = newBranch("1", branchName);
207     String ruleName = randomAlphabetic(8);
208     String host = randomAlphabetic(15);
209     String key = "key";
210     ChangedIssue changedIssue = newChangedIssue(key, project, ruleName, randomRuleTypeHotspotExcluded());
211     when(emailSettings.getServerBaseURL()).thenReturn(host);
212
213     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, ImmutableSet.of(changedIssue), fpPrAccepted));
214
215     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
216       .hasParagraph().hasParagraph() // skip header
217       .hasParagraph(project.getProjectName() + ", " + branchName)
218       .hasList("Rule " + ruleName + " - See the single issue")
219       .withLink("See the single issue",
220         host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
221       .hasParagraph().hasParagraph() // skip footer
222       .noMoreBlock();
223   }
224
225   @Test
226   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
227   public void formats_returns_html_message_for_single_hotspot_on_branch(Change change, FpPrAccepted fpPrAccepted) {
228     String branchName = randomAlphabetic(6);
229     Project project = newBranch("1", branchName);
230     String ruleName = randomAlphabetic(8);
231     String host = randomAlphabetic(15);
232     String key = "key";
233     ChangedIssue changedIssue = newChangedIssue(key, project, ruleName, SECURITY_HOTSPOT);
234     when(emailSettings.getServerBaseURL()).thenReturn(host);
235
236     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, ImmutableSet.of(changedIssue), fpPrAccepted));
237
238     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
239       .hasParagraph().hasParagraph() // skip header
240       .hasParagraph(project.getProjectName() + ", " + branchName)
241       .hasList("Rule " + ruleName + " - See the single hotspot")
242       .withLink("See the single hotspot",
243         host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
244       .hasParagraph().hasParagraph() // skip footer
245       .noMoreBlock();
246   }
247
248   @Test
249   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
250   public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master(Change change, FpPrAccepted fpPrAccepted) {
251     Project project = newProject("1");
252     String ruleName = randomAlphabetic(8);
253     String host = randomAlphabetic(15);
254     Rule rule = newRandomNotAHotspotRule(ruleName);
255     List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
256       .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
257       .collect(toList());
258     when(emailSettings.getServerBaseURL()).thenReturn(host);
259
260     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, ImmutableSet.copyOf(changedIssues), fpPrAccepted));
261
262     String expectedHref = host + "/project/issues?id=" + project.getKey()
263       + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
264     String expectedLinkText = "See all " + changedIssues.size() + " issues";
265     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
266       .hasParagraph().hasParagraph() // skip header
267       .hasParagraph(project.getProjectName())
268       .hasList("Rule " + ruleName + " - " + expectedLinkText)
269       .withLink(expectedLinkText, expectedHref)
270       .hasParagraph().hasParagraph() // skip footer
271       .noMoreBlock();
272   }
273
274   @Test
275   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
276   public void formats_returns_html_message_for_multiple_hotspots_of_same_rule_on_same_project_on_master(Change change, FpPrAccepted fpPrAccepted) {
277     Project project = newProject("1");
278     String ruleName = randomAlphabetic(8);
279     String host = randomAlphabetic(15);
280     Rule rule = newSecurityHotspotRule(ruleName);
281     List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
282       .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
283       .collect(toList());
284     when(emailSettings.getServerBaseURL()).thenReturn(host);
285
286     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, ImmutableSet.copyOf(changedIssues), fpPrAccepted));
287
288     String expectedHref = host + "/project/issues?id=" + project.getKey()
289       + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
290     String expectedLinkText = "See all " + changedIssues.size() + " hotspots";
291     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
292       .hasParagraph().hasParagraph() // skip header
293       .hasParagraph(project.getProjectName())
294       .hasList("Rule " + ruleName + " - " + expectedLinkText)
295       .withLink(expectedLinkText, expectedHref)
296       .hasParagraph().hasParagraph() // skip footer
297       .noMoreBlock();
298   }
299
300   @Test
301   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
302   public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch(Change change, FpPrAccepted fpPrAccepted) {
303     String branchName = randomAlphabetic(19);
304     Project project = newBranch("1", branchName);
305     String ruleName = randomAlphabetic(8);
306     String host = randomAlphabetic(15);
307     Rule rule = newRandomNotAHotspotRule(ruleName);
308     List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
309       .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
310       .collect(toList());
311     when(emailSettings.getServerBaseURL()).thenReturn(host);
312
313     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, ImmutableSet.copyOf(changedIssues), fpPrAccepted));
314
315     String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName
316       + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
317     String expectedLinkText = "See all " + changedIssues.size() + " issues";
318     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
319       .hasParagraph().hasParagraph() // skip header
320       .hasParagraph(project.getProjectName() + ", " + branchName)
321       .hasList("Rule " + ruleName + " - " + expectedLinkText)
322       .withLink(expectedLinkText, expectedHref)
323       .hasParagraph().hasParagraph() // skip footer
324       .noMoreBlock();
325   }
326
327   @Test
328   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
329   public void formats_returns_html_message_for_multiple_hotspots_of_same_rule_on_same_project_on_branch(Change change, FpPrAccepted fpPrAccepted) {
330     String branchName = randomAlphabetic(19);
331     Project project = newBranch("1", branchName);
332     String ruleName = randomAlphabetic(8);
333     String host = randomAlphabetic(15);
334     Rule rule = newSecurityHotspotRule(ruleName);
335     List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
336       .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
337       .collect(toList());
338     when(emailSettings.getServerBaseURL()).thenReturn(host);
339
340     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, ImmutableSet.copyOf(changedIssues), fpPrAccepted));
341
342     String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName
343       + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
344     String expectedLinkText = "See all " + changedIssues.size() + " hotspots";
345     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
346       .hasParagraph().hasParagraph() // skip header
347       .hasParagraph(project.getProjectName() + ", " + branchName)
348       .hasList("Rule " + ruleName + " - " + expectedLinkText)
349       .withLink(expectedLinkText, expectedHref)
350       .hasParagraph().hasParagraph() // skip footer
351       .noMoreBlock();
352   }
353
354   @Test
355   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
356   public void formats_returns_html_message_with_projects_ordered_by_name(Change change, FpPrAccepted fpPrAccepted) {
357     Project project1 = newProject("1");
358     Project project1Branch1 = newBranch("1", "a");
359     Project project1Branch2 = newBranch("1", "b");
360     Project project2 = newProject("B");
361     Project project2Branch1 = newBranch("B", "a");
362     Project project3 = newProject("C");
363     String host = randomAlphabetic(15);
364     List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3)
365       .map(project -> newChangedIssue("issue_" + project.getUuid(), project, newRandomNotAHotspotRule(randomAlphabetic(2))))
366       .collect(toList());
367     Collections.shuffle(changedIssues);
368     when(emailSettings.getServerBaseURL()).thenReturn(host);
369
370     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, ImmutableSet.copyOf(changedIssues), fpPrAccepted));
371
372     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
373       .hasParagraph().hasParagraph() // skip header
374       .hasParagraph(project1.getProjectName())
375       .hasList()
376       .hasParagraph(project1Branch1.getProjectName() + ", " + project1Branch1.getBranchName().get())
377       .hasList()
378       .hasParagraph(project1Branch2.getProjectName() + ", " + project1Branch2.getBranchName().get())
379       .hasList()
380       .hasParagraph(project2.getProjectName())
381       .hasList()
382       .hasParagraph(project2Branch1.getProjectName() + ", " + project2Branch1.getBranchName().get())
383       .hasList()
384       .hasParagraph(project3.getProjectName())
385       .hasList()
386       .hasParagraph().hasParagraph() // skip footer
387       .noMoreBlock();
388   }
389
390   @Test
391   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
392   public void formats_returns_html_message_with_rules_ordered_by_name(Change change, FpPrAccepted fpPrAccepted) {
393     Project project = newProject("1");
394     Rule rule1 = newRandomNotAHotspotRule("1");
395     Rule rule2 = newRandomNotAHotspotRule("a");
396     Rule rule3 = newRandomNotAHotspotRule("b");
397     Rule rule4 = newRandomNotAHotspotRule("X");
398     String host = randomAlphabetic(15);
399     List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
400       .map(rule -> newChangedIssue("issue_" + rule.getName(), project, rule))
401       .collect(toList());
402     Collections.shuffle(changedIssues);
403     when(emailSettings.getServerBaseURL()).thenReturn(host);
404
405     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, ImmutableSet.copyOf(changedIssues), fpPrAccepted));
406
407     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
408       .hasParagraph().hasParagraph() // skip header
409       .hasParagraph(project.getProjectName())
410       .hasList(
411         "Rule " + rule1.getName() + " - See the single issue",
412         "Rule " + rule2.getName() + " - See the single issue",
413         "Rule " + rule3.getName() + " - See the single issue",
414         "Rule " + rule4.getName() + " - See the single issue")
415       .hasParagraph().hasParagraph() // skip footer
416       .noMoreBlock();
417   }
418
419   @Test
420   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
421   public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues(Change change, FpPrAccepted fpPrAccepted) {
422     Project project1 = newProject("1");
423     Project project2 = newProject("V");
424     Project project2Branch = newBranch("V", "AB");
425     Rule rule1 = newRandomNotAHotspotRule("1");
426     Rule rule2 = newRandomNotAHotspotRule("a");
427     String host = randomAlphabetic(15);
428     List<ChangedIssue> changedIssues = Stream.of(
429         IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, project1, rule1)),
430         IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, project1, rule2)),
431         IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, project2, rule2)),
432         IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, project2Branch, rule1)))
433       .flatMap(t -> t)
434       .collect(toList());
435     Collections.shuffle(changedIssues);
436     when(emailSettings.getServerBaseURL()).thenReturn(host);
437
438     EmailMessage emailMessage = underTest.format(new FPOrAcceptedNotification(change, ImmutableSet.copyOf(changedIssues), fpPrAccepted));
439
440     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
441       .hasParagraph().hasParagraph() // skip header
442       .hasParagraph(project1.getProjectName())
443       .hasList()
444       .withItemTexts(
445         "Rule " + rule1.getName() + " - See all 39 issues",
446         "Rule " + rule2.getName() + " - See all 40 issues")
447       .withLink("See all 39 issues",
448         host + "/project/issues?id=" + project1.getKey()
449           + "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
450       .withLink("See all 40 issues",
451         host + "/project/issues?id=" + project1.getKey()
452           + "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
453       .hasParagraph(project2.getProjectName())
454       .hasList("Rule " + rule2.getName() + " - See issues 1-40 41-80 81")
455       .withLink("1-40",
456         host + "/project/issues?id=" + project2.getKey()
457           + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
458       .withLink("41-80",
459         host + "/project/issues?id=" + project2.getKey()
460           + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
461       .withLink("81",
462         host + "/project/issues?id=" + project2.getKey()
463           + "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
464       .hasParagraph(project2Branch.getProjectName() + ", " + project2Branch.getBranchName().get())
465       .hasList("Rule " + rule1.getName() + " - See all 6 issues")
466       .withLink("See all 6 issues",
467         host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
468           + "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
469       .hasParagraph().hasParagraph() // skip footer
470       .noMoreBlock();
471   }
472
473   @DataProvider
474   public static Object[][] userOrAnalysisChange() {
475     AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong());
476     UserChange userChange = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6),
477       new Random().nextBoolean() ? null : randomAlphabetic(7)));
478     return new Object[][] {
479       {analysisChange},
480       {userChange}
481     };
482   }
483
484   @DataProvider
485   public static Object[][] fpOrWontFixValuesByUserOrAnalysisChange() {
486     AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong());
487     UserChange userChange = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6),
488       new Random().nextBoolean() ? null : randomAlphabetic(7)));
489     return new Object[][] {
490       {analysisChange, FP},
491       {analysisChange, ACCEPTED},
492       {userChange, FP},
493       {userChange, ACCEPTED}
494     };
495   }
496
497   private static ChangedIssue newChangedIssue(String key, Project project, String ruleName, RuleType ruleType) {
498     return newChangedIssue(key, project, newRule(ruleName, ruleType));
499   }
500
501   private static ChangedIssue newChangedIssue(String key, Project project, Rule rule) {
502     return new ChangedIssue.Builder(key)
503       .setNewStatus(randomAlphabetic(19))
504       .setProject(project)
505       .setRule(rule)
506       .build();
507   }
508
509   private static Rule newRule(String ruleName, RuleType ruleType) {
510     return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleType, ruleName);
511   }
512
513   private static Project newProject(String uuid) {
514     return new Project.Builder(uuid).setProjectName(uuid + "_name").setKey(uuid + "_key").build();
515   }
516
517   private static Project newBranch(String uuid, String branchName) {
518     return new Project.Builder(uuid).setProjectName(uuid + "_name").setKey(uuid + "_key").setBranchName(branchName).build();
519   }
520 }