3 * Copyright (C) 2009-2023 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.ce.task.projectanalysis.issue;
22 import java.util.Date;
23 import org.junit.Rule;
24 import org.junit.Test;
25 import org.sonar.api.rules.RuleType;
26 import org.sonar.api.utils.Duration;
27 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
28 import org.sonar.ce.task.projectanalysis.analysis.Branch;
29 import org.sonar.core.issue.DefaultIssue;
30 import org.sonar.core.issue.DefaultIssueComment;
31 import org.sonar.core.issue.FieldDiffs;
32 import org.sonar.core.issue.IssueChangeContext;
33 import org.sonar.db.component.BranchType;
34 import org.sonar.db.protobuf.DbCommons;
35 import org.sonar.db.protobuf.DbIssues;
36 import org.sonar.server.issue.IssueFieldsSetter;
37 import org.sonar.server.issue.workflow.IssueWorkflow;
39 import static com.google.common.collect.Lists.newArrayList;
40 import static org.assertj.core.api.Assertions.assertThat;
41 import static org.assertj.core.api.Assertions.assertThatThrownBy;
42 import static org.assertj.core.api.Assertions.entry;
43 import static org.assertj.core.groups.Tuple.tuple;
44 import static org.mockito.Mockito.mock;
45 import static org.mockito.Mockito.never;
46 import static org.mockito.Mockito.verify;
47 import static org.mockito.Mockito.verifyNoInteractions;
48 import static org.mockito.Mockito.when;
49 import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
50 import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
51 import static org.sonar.api.issue.Issue.STATUS_CLOSED;
52 import static org.sonar.api.issue.Issue.STATUS_OPEN;
53 import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
54 import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
55 import static org.sonar.api.rule.Severity.BLOCKER;
56 import static org.sonar.api.utils.DateUtils.parseDate;
57 import static org.sonar.core.issue.IssueChangeContext.issueChangeContextByUserBuilder;
58 import static org.sonar.db.rule.RuleTesting.XOO_X1;
60 public class IssueLifecycleTest {
61 private static final Date DEFAULT_DATE = new Date();
62 private static final Duration DEFAULT_DURATION = Duration.create(10);
63 private static final String TEST_CONTEXT_KEY = "test_context_key";
65 private final DumbRule rule = new DumbRule(XOO_X1);
68 public RuleRepositoryRule ruleRepository = new RuleRepositoryRule().add(rule);
70 public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule();
72 private final IssueChangeContext issueChangeContext = issueChangeContextByUserBuilder(DEFAULT_DATE, "default_user_uuid").build();
73 private final IssueWorkflow workflow = mock(IssueWorkflow.class);
74 private final IssueFieldsSetter updater = mock(IssueFieldsSetter.class);
75 private final DebtCalculator debtCalculator = mock(DebtCalculator.class);
76 private final IssueLifecycle underTest = new IssueLifecycle(analysisMetadataHolder, issueChangeContext, workflow, updater, debtCalculator, ruleRepository);
79 public void initNewOpenIssue() {
80 DefaultIssue issue = new DefaultIssue()
82 when(debtCalculator.calculate(issue)).thenReturn(DEFAULT_DURATION);
84 underTest.initNewOpenIssue(issue);
86 assertThat(issue.key()).isNotNull();
87 assertThat(issue.creationDate()).isNotNull();
88 assertThat(issue.updateDate()).isNotNull();
89 assertThat(issue.status()).isEqualTo(STATUS_OPEN);
90 assertThat(issue.effort()).isEqualTo(DEFAULT_DURATION);
91 assertThat(issue.isNew()).isTrue();
92 assertThat(issue.isCopied()).isFalse();
96 public void initNewOpenHotspot() {
97 rule.setType(RuleType.SECURITY_HOTSPOT);
98 DefaultIssue issue = new DefaultIssue()
100 when(debtCalculator.calculate(issue)).thenReturn(DEFAULT_DURATION);
102 underTest.initNewOpenIssue(issue);
104 assertThat(issue.key()).isNotNull();
105 assertThat(issue.creationDate()).isNotNull();
106 assertThat(issue.updateDate()).isNotNull();
107 assertThat(issue.status()).isEqualTo(STATUS_TO_REVIEW);
108 assertThat(issue.resolution()).isNull();
109 assertThat(issue.effort()).isEqualTo(DEFAULT_DURATION);
110 assertThat(issue.isNew()).isTrue();
111 assertThat(issue.isCopied()).isFalse();
115 public void mergeIssueFromPRIntoBranch() {
116 DefaultIssue raw = new DefaultIssue()
118 DefaultIssue fromShort = new DefaultIssue()
120 .setIsNewCodeReferenceIssue(true);
121 fromShort.setResolution("resolution");
122 fromShort.setStatus("status");
124 Date commentDate = new Date();
125 fromShort.addComment(new DefaultIssueComment()
126 .setIssueKey("short")
127 .setCreatedAt(commentDate)
128 .setUserUuid("user_uuid")
129 .setMarkdownText("A comment"));
131 Date diffDate = new Date();
133 fromShort.addChange(new FieldDiffs()
134 .setCreationDate(diffDate)
135 .setIssueKey("short")
136 .setUserUuid("user_uuid")
137 .setDiff("file", "uuidA1", "uuidB1"));
138 // file diff with another field
139 fromShort.addChange(new FieldDiffs()
140 .setCreationDate(diffDate)
141 .setIssueKey("short")
142 .setUserUuid("user_uuid")
143 .setDiff("severity", "MINOR", "MAJOR")
144 .setDiff("file", "uuidA2", "uuidB2"));
146 Branch branch = mock(Branch.class);
147 when(branch.getName()).thenReturn("master");
148 analysisMetadataHolder.setBranch(branch);
150 underTest.mergeConfirmedOrResolvedFromPrOrBranch(raw, fromShort, BranchType.PULL_REQUEST, "2");
152 assertThat(raw.resolution()).isEqualTo("resolution");
153 assertThat(raw.status()).isEqualTo("status");
154 assertThat(raw.defaultIssueComments())
155 .extracting(DefaultIssueComment::issueKey, DefaultIssueComment::createdAt, DefaultIssueComment::userUuid, DefaultIssueComment::markdownText)
156 .containsOnly(tuple("raw", commentDate, "user_uuid", "A comment"));
157 assertThat(raw.changes()).hasSize(2);
158 assertThat(raw.changes().get(0).creationDate()).isEqualTo(diffDate);
159 assertThat(raw.changes().get(0).userUuid()).contains("user_uuid");
160 assertThat(raw.changes().get(0).issueKey()).contains("raw");
161 assertThat(raw.changes().get(0).diffs()).containsOnlyKeys("severity");
162 assertThat(raw.changes().get(1).userUuid()).contains("default_user_uuid");
163 assertThat(raw.changes().get(1).diffs()).containsOnlyKeys(IssueFieldsSetter.FROM_BRANCH);
164 assertThat(raw.changes().get(1).get(IssueFieldsSetter.FROM_BRANCH).oldValue()).isEqualTo("#2");
165 assertThat(raw.changes().get(1).get(IssueFieldsSetter.FROM_BRANCH).newValue()).isEqualTo("master");
166 assertThat(raw.isNewCodeReferenceIssue()).isTrue();
170 public void copyExistingIssuesFromSourceBranchOfPullRequest() {
171 String pullRequestKey = "1";
172 Branch branch = mock(Branch.class);
173 when(branch.getType()).thenReturn(BranchType.PULL_REQUEST);
174 when(branch.getName()).thenReturn("sourceBranch-1");
175 when(branch.getPullRequestKey()).thenReturn(pullRequestKey);
176 analysisMetadataHolder.setBranch(branch);
177 analysisMetadataHolder.setPullRequestKey(pullRequestKey);
178 DefaultIssue raw = new DefaultIssue()
180 DefaultIssue fromShort = new DefaultIssue()
182 fromShort.setResolution("resolution");
183 fromShort.setStatus("status");
185 Date commentDate = new Date();
186 fromShort.addComment(new DefaultIssueComment()
187 .setIssueKey("short")
188 .setCreatedAt(commentDate)
189 .setUserUuid("user_uuid")
190 .setMarkdownText("A comment"));
192 Date diffDate = new Date();
194 fromShort.addChange(new FieldDiffs()
195 .setCreationDate(diffDate)
196 .setIssueKey("short")
197 .setUserUuid("user_uuid")
198 .setDiff("file", "uuidA1", "uuidB1"));
199 // file diff with another field
200 fromShort.addChange(new FieldDiffs()
201 .setCreationDate(diffDate)
202 .setIssueKey("short")
203 .setUserUuid("user_uuid")
204 .setDiff("severity", "MINOR", "MAJOR")
205 .setDiff("file", "uuidA2", "uuidB2"));
207 underTest.copyExistingIssueFromSourceBranchToPullRequest(raw, fromShort);
209 assertThat(raw.resolution()).isEqualTo("resolution");
210 assertThat(raw.status()).isEqualTo("status");
211 assertThat(raw.defaultIssueComments())
212 .extracting(DefaultIssueComment::issueKey, DefaultIssueComment::createdAt, DefaultIssueComment::userUuid, DefaultIssueComment::markdownText)
213 .containsOnly(tuple("raw", commentDate, "user_uuid", "A comment"));
214 assertThat(raw.changes()).hasSize(2);
215 assertThat(raw.changes().get(0).creationDate()).isEqualTo(diffDate);
216 assertThat(raw.changes().get(0).userUuid()).contains("user_uuid");
217 assertThat(raw.changes().get(0).issueKey()).contains("raw");
218 assertThat(raw.changes().get(0).diffs()).containsOnlyKeys("severity");
219 assertThat(raw.changes().get(1).userUuid()).contains("default_user_uuid");
220 assertThat(raw.changes().get(1).diffs()).containsOnlyKeys(IssueFieldsSetter.FROM_BRANCH);
221 assertThat(raw.changes().get(1).get(IssueFieldsSetter.FROM_BRANCH).oldValue()).isEqualTo("sourceBranch-1");
222 assertThat(raw.changes().get(1).get(IssueFieldsSetter.FROM_BRANCH).newValue()).isEqualTo("#1");
226 public void copyExistingIssuesFromSourceBranchOfPullRequest_copyFieldDiffsCorrectly() {
227 String pullRequestKey = "1";
228 Branch branch = mock(Branch.class);
229 when(branch.getType()).thenReturn(BranchType.PULL_REQUEST);
230 when(branch.getName()).thenReturn("sourceBranch-1");
231 when(branch.getPullRequestKey()).thenReturn(pullRequestKey);
232 analysisMetadataHolder.setBranch(branch);
233 analysisMetadataHolder.setPullRequestKey(pullRequestKey);
234 DefaultIssue destIssue = new DefaultIssue()
236 DefaultIssue sourceIssue = new DefaultIssue()
238 sourceIssue.setResolution("resolution");
239 sourceIssue.setStatus("status");
241 FieldDiffs sourceFieldDiffs = new FieldDiffs();
242 sourceIssue.addChange(sourceFieldDiffs
243 .setCreationDate(new Date())
244 .setIssueKey("short")
245 .setUserUuid("user_uuid")
246 .setExternalUser("toto")
247 .setWebhookSource("github")
248 .setDiff("severity", "MINOR", "MAJOR"));
250 underTest.copyExistingIssueFromSourceBranchToPullRequest(destIssue, sourceIssue);
252 FieldDiffs actualFieldDiffs = destIssue.changes().iterator().next();
253 assertThat(actualFieldDiffs.issueKey()).contains(destIssue.key());
254 assertThat(actualFieldDiffs).usingRecursiveComparison().ignoringFields("issueKey").isEqualTo(sourceFieldDiffs);
258 public void copyExistingIssuesFromSourceBranchOfPullRequest_only_works_for_pull_requests() {
259 DefaultIssue raw = new DefaultIssue()
261 DefaultIssue from = new DefaultIssue()
263 from.setResolution("resolution");
264 from.setStatus("status");
265 Branch branch = mock(Branch.class);
266 when(branch.getType()).thenReturn(BranchType.BRANCH);
267 analysisMetadataHolder.setBranch(branch);
269 assertThatThrownBy(() -> underTest.copyExistingIssueFromSourceBranchToPullRequest(raw, from))
270 .isInstanceOf(IllegalStateException.class)
271 .hasMessage("This operation should be done only on pull request analysis");
275 public void copiedIssue() {
276 DefaultIssue raw = new DefaultIssue()
279 .setCreationDate(parseDate("2015-10-01"))
280 .setUpdateDate(parseDate("2015-10-02"))
281 .setCloseDate(parseDate("2015-10-03"))
282 .setRuleDescriptionContextKey(TEST_CONTEXT_KEY);
284 DbIssues.Locations issueLocations = DbIssues.Locations.newBuilder()
285 .setTextRange(DbCommons.TextRange.newBuilder()
290 DefaultIssue base = new DefaultIssue()
292 .setCreationDate(parseDate("2015-01-01"))
293 .setUpdateDate(parseDate("2015-01-02"))
294 .setCloseDate(parseDate("2015-01-03"))
295 .setResolution(RESOLUTION_FIXED)
296 .setStatus(STATUS_CLOSED)
297 .setSeverity(BLOCKER)
298 .setAssigneeUuid("base assignee uuid")
299 .setAuthorLogin("base author")
300 .setTags(newArrayList("base tag"))
301 .setOnDisabledRule(true)
302 .setSelectedAt(1000L)
304 .setMessage("message")
306 .setEffort(Duration.create(15L))
307 .setManualSeverity(false)
308 .setLocations(issueLocations);
310 when(debtCalculator.calculate(raw)).thenReturn(DEFAULT_DURATION);
312 Branch branch = mock(Branch.class);
313 when(branch.getName()).thenReturn("release-2.x");
314 analysisMetadataHolder.setBranch(branch);
316 underTest.copyExistingOpenIssueFromBranch(raw, base, "master");
318 assertThat(raw.isNew()).isFalse();
319 assertThat(raw.isCopied()).isTrue();
320 assertThat(raw.key()).isNotNull();
321 assertThat(raw.key()).isNotEqualTo(base.key());
322 assertThat(raw.creationDate()).isEqualTo(base.creationDate());
323 assertThat(raw.updateDate()).isEqualTo(base.updateDate());
324 assertThat(raw.closeDate()).isEqualTo(base.closeDate());
325 assertThat(raw.resolution()).isEqualTo(RESOLUTION_FIXED);
326 assertThat(raw.status()).isEqualTo(STATUS_CLOSED);
327 assertThat(raw.assignee()).isEqualTo("base assignee uuid");
328 assertThat(raw.authorLogin()).isEqualTo("base author");
329 assertThat(raw.tags()).containsOnly("base tag");
330 assertThat(raw.effort()).isEqualTo(DEFAULT_DURATION);
331 assertThat(raw.isOnDisabledRule()).isTrue();
332 assertThat(raw.selectedAt()).isEqualTo(1000L);
333 assertThat(raw.changes().get(0).get(IssueFieldsSetter.FROM_BRANCH).oldValue()).isEqualTo("master");
334 assertThat(raw.changes().get(0).get(IssueFieldsSetter.FROM_BRANCH).newValue()).isEqualTo("release-2.x");
335 assertThat(raw.getRuleDescriptionContextKey()).contains(TEST_CONTEXT_KEY);
337 verifyNoInteractions(updater);
341 public void doAutomaticTransition() {
342 DefaultIssue issue = new DefaultIssue();
344 underTest.doAutomaticTransition(issue);
346 verify(workflow).doAutomaticTransition(issue, issueChangeContext);
350 public void mergeExistingOpenIssue() {
351 DefaultIssue raw = new DefaultIssue()
355 .setRuleDescriptionContextKey("spring")
356 .setCreationDate(parseDate("2015-10-01"))
357 .setUpdateDate(parseDate("2015-10-02"))
358 .setCloseDate(parseDate("2015-10-03"));
360 DbIssues.Locations issueLocations = DbIssues.Locations.newBuilder()
361 .setTextRange(DbCommons.TextRange.newBuilder()
367 DbIssues.MessageFormattings messageFormattings = DbIssues.MessageFormattings.newBuilder()
368 .addMessageFormatting(DbIssues.MessageFormatting
372 .setType(DbIssues.MessageFormattingType.CODE)
376 DefaultIssue base = new DefaultIssue()
378 .setCreationDate(parseDate("2015-01-01"))
379 .setUpdateDate(parseDate("2015-01-02"))
380 .setResolution(RESOLUTION_FALSE_POSITIVE)
381 .setStatus(STATUS_RESOLVED)
382 .setSeverity(BLOCKER)
383 .setAssigneeUuid("base assignee uuid")
384 .setAuthorLogin("base author")
385 .setTags(newArrayList("base tag"))
386 .setOnDisabledRule(true)
387 .setSelectedAt(1000L)
389 .setMessage("message with code")
390 .setMessageFormattings(messageFormattings)
392 .setRuleDescriptionContextKey("hibernate")
393 .setEffort(Duration.create(15L))
394 .setManualSeverity(false)
395 .setLocations(issueLocations)
396 .addChange(new FieldDiffs().setDiff("foo", "bar", "donut"))
397 .addChange(new FieldDiffs().setDiff("file", "A", "B"));
399 when(debtCalculator.calculate(raw)).thenReturn(DEFAULT_DURATION);
401 underTest.mergeExistingOpenIssue(raw, base);
403 assertThat(raw.isNew()).isFalse();
404 assertThat(raw.key()).isEqualTo("BASE_KEY");
405 assertThat(raw.creationDate()).isEqualTo(base.creationDate());
406 assertThat(raw.updateDate()).isEqualTo(base.updateDate());
407 assertThat(raw.resolution()).isEqualTo(RESOLUTION_FALSE_POSITIVE);
408 assertThat(raw.status()).isEqualTo(STATUS_RESOLVED);
409 assertThat(raw.assignee()).isEqualTo("base assignee uuid");
410 assertThat(raw.authorLogin()).isEqualTo("base author");
411 assertThat(raw.tags()).containsOnly("base tag");
412 assertThat(raw.effort()).isEqualTo(DEFAULT_DURATION);
413 assertThat(raw.isOnDisabledRule()).isTrue();
414 assertThat(raw.selectedAt()).isEqualTo(1000L);
415 assertThat(raw.isChanged()).isFalse();
416 assertThat(raw.changes()).hasSize(2);
417 assertThat(raw.changes().get(0).diffs())
418 .containsOnly(entry("foo", new FieldDiffs.Diff<>("bar", "donut")));
419 assertThat(raw.changes().get(1).diffs())
420 .containsOnly(entry("file", new FieldDiffs.Diff<>("A", "B")));
422 verify(updater).setPastSeverity(raw, BLOCKER, issueChangeContext);
423 verify(updater).setPastLine(raw, 10);
424 verify(updater).setRuleDescriptionContextKey(raw, "hibernate");
425 verify(updater).setPastMessage(raw, "message with code", messageFormattings, issueChangeContext);
426 verify(updater).setPastEffort(raw, Duration.create(15L), issueChangeContext);
427 verify(updater).setPastLocations(raw, issueLocations);
431 public void mergeExistingOpenIssue_with_manual_severity() {
432 DefaultIssue raw = new DefaultIssue()
436 DefaultIssue base = new DefaultIssue()
438 .setResolution(RESOLUTION_FIXED)
439 .setStatus(STATUS_CLOSED)
440 .setSeverity(BLOCKER)
441 .setManualSeverity(true);
443 underTest.mergeExistingOpenIssue(raw, base);
445 assertThat(raw.manualSeverity()).isTrue();
446 assertThat(raw.severity()).isEqualTo(BLOCKER);
448 verify(updater, never()).setPastSeverity(raw, BLOCKER, issueChangeContext);
452 public void mergeExistingOpenIssue_with_base_changed() {
453 DefaultIssue raw = new DefaultIssue()
457 DefaultIssue base = new DefaultIssue()
460 .setResolution(RESOLUTION_FALSE_POSITIVE)
461 .setStatus(STATUS_RESOLVED);
463 underTest.mergeExistingOpenIssue(raw, base);
465 assertThat(raw.isChanged()).isTrue();
469 public void mergeExistingOpenIssue_with_rule_description_context_key_added() {
470 DefaultIssue raw = new DefaultIssue()
474 .setRuleDescriptionContextKey(TEST_CONTEXT_KEY);
475 DefaultIssue base = new DefaultIssue()
478 .setResolution(RESOLUTION_FALSE_POSITIVE)
479 .setStatus(STATUS_RESOLVED)
480 .setRuleDescriptionContextKey(null);
482 underTest.mergeExistingOpenIssue(raw, base);
484 assertThat(raw.isChanged()).isTrue();
485 assertThat(raw.getRuleDescriptionContextKey()).isEqualTo(raw.getRuleDescriptionContextKey());
489 public void mergeExistingOpenIssue_with_rule_description_context_key_removed() {
490 DefaultIssue raw = new DefaultIssue()
494 .setRuleDescriptionContextKey(null);
495 DefaultIssue base = new DefaultIssue()
498 .setResolution(RESOLUTION_FALSE_POSITIVE)
499 .setStatus(STATUS_RESOLVED)
500 .setRuleDescriptionContextKey(TEST_CONTEXT_KEY);
502 underTest.mergeExistingOpenIssue(raw, base);
504 assertThat(raw.isChanged()).isTrue();
505 assertThat(raw.getRuleDescriptionContextKey()).isEmpty();