3 * Copyright (C) 2009-2021 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.ce.task.projectanalysis.issue;
22 import com.google.common.collect.ImmutableMap;
23 import java.util.Date;
24 import org.junit.Rule;
25 import org.junit.Test;
26 import org.sonar.api.rules.RuleType;
27 import org.sonar.api.utils.Duration;
28 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
29 import org.sonar.ce.task.projectanalysis.analysis.Branch;
30 import org.sonar.core.issue.DefaultIssue;
31 import org.sonar.core.issue.DefaultIssueComment;
32 import org.sonar.core.issue.FieldDiffs;
33 import org.sonar.core.issue.IssueChangeContext;
34 import org.sonar.db.component.BranchType;
35 import org.sonar.db.protobuf.DbCommons;
36 import org.sonar.db.protobuf.DbIssues;
37 import org.sonar.server.issue.IssueFieldsSetter;
38 import org.sonar.server.issue.workflow.IssueWorkflow;
40 import static com.google.common.collect.Lists.newArrayList;
41 import static org.assertj.core.api.Assertions.assertThat;
42 import static org.assertj.core.api.Assertions.assertThatThrownBy;
43 import static org.assertj.core.api.Assertions.entry;
44 import static org.assertj.core.groups.Tuple.tuple;
45 import static org.mockito.Mockito.mock;
46 import static org.mockito.Mockito.never;
47 import static org.mockito.Mockito.verify;
48 import static org.mockito.Mockito.verifyNoInteractions;
49 import static org.mockito.Mockito.when;
50 import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
51 import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
52 import static org.sonar.api.issue.Issue.STATUS_CLOSED;
53 import static org.sonar.api.issue.Issue.STATUS_OPEN;
54 import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
55 import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
56 import static org.sonar.api.rule.Severity.BLOCKER;
57 import static org.sonar.api.utils.DateUtils.parseDate;
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);
64 private final DumbRule rule = new DumbRule(XOO_X1);
67 public RuleRepositoryRule ruleRepository = new RuleRepositoryRule().add(rule);
69 public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule();
71 private final IssueChangeContext issueChangeContext = IssueChangeContext.createUser(DEFAULT_DATE, "default_user_uuid");
72 private final IssueWorkflow workflow = mock(IssueWorkflow.class);
73 private final IssueFieldsSetter updater = mock(IssueFieldsSetter.class);
74 private final DebtCalculator debtCalculator = mock(DebtCalculator.class);
75 private final IssueLifecycle underTest = new IssueLifecycle(analysisMetadataHolder, issueChangeContext, workflow, updater, debtCalculator, ruleRepository);
78 public void initNewOpenIssue() {
79 DefaultIssue issue = new DefaultIssue()
81 when(debtCalculator.calculate(issue)).thenReturn(DEFAULT_DURATION);
83 underTest.initNewOpenIssue(issue);
85 assertThat(issue.key()).isNotNull();
86 assertThat(issue.creationDate()).isNotNull();
87 assertThat(issue.updateDate()).isNotNull();
88 assertThat(issue.status()).isEqualTo(STATUS_OPEN);
89 assertThat(issue.effort()).isEqualTo(DEFAULT_DURATION);
90 assertThat(issue.isNew()).isTrue();
91 assertThat(issue.isCopied()).isFalse();
95 public void initNewOpenHotspot() {
96 rule.setType(RuleType.SECURITY_HOTSPOT);
97 DefaultIssue issue = new DefaultIssue()
99 when(debtCalculator.calculate(issue)).thenReturn(DEFAULT_DURATION);
101 underTest.initNewOpenIssue(issue);
103 assertThat(issue.key()).isNotNull();
104 assertThat(issue.creationDate()).isNotNull();
105 assertThat(issue.updateDate()).isNotNull();
106 assertThat(issue.status()).isEqualTo(STATUS_TO_REVIEW);
107 assertThat(issue.resolution()).isNull();
108 assertThat(issue.effort()).isEqualTo(DEFAULT_DURATION);
109 assertThat(issue.isNew()).isTrue();
110 assertThat(issue.isCopied()).isFalse();
114 public void mergeIssueFromPRIntoBranch() {
115 DefaultIssue raw = new DefaultIssue()
117 DefaultIssue fromShort = new DefaultIssue()
119 fromShort.setResolution("resolution");
120 fromShort.setStatus("status");
122 Date commentDate = new Date();
123 fromShort.addComment(new DefaultIssueComment()
124 .setIssueKey("short")
125 .setCreatedAt(commentDate)
126 .setUserUuid("user_uuid")
127 .setMarkdownText("A comment"));
129 Date diffDate = new Date();
131 fromShort.addChange(new FieldDiffs()
132 .setCreationDate(diffDate)
133 .setIssueKey("short")
134 .setUserUuid("user_uuid")
135 .setDiff("file", "uuidA1", "uuidB1"));
136 // file diff with another field
137 fromShort.addChange(new FieldDiffs()
138 .setCreationDate(diffDate)
139 .setIssueKey("short")
140 .setUserUuid("user_uuid")
141 .setDiff("severity", "MINOR", "MAJOR")
142 .setDiff("file", "uuidA2", "uuidB2"));
144 Branch branch = mock(Branch.class);
145 when(branch.getName()).thenReturn("master");
146 analysisMetadataHolder.setBranch(branch);
148 underTest.mergeConfirmedOrResolvedFromPr(raw, fromShort, "2");
150 assertThat(raw.resolution()).isEqualTo("resolution");
151 assertThat(raw.status()).isEqualTo("status");
152 assertThat(raw.defaultIssueComments())
153 .extracting(DefaultIssueComment::issueKey, DefaultIssueComment::createdAt, DefaultIssueComment::userUuid, DefaultIssueComment::markdownText)
154 .containsOnly(tuple("raw", commentDate, "user_uuid", "A comment"));
155 assertThat(raw.changes()).hasSize(2);
156 assertThat(raw.changes().get(0).creationDate()).isEqualTo(diffDate);
157 assertThat(raw.changes().get(0).userUuid()).isEqualTo("user_uuid");
158 assertThat(raw.changes().get(0).issueKey()).isEqualTo("raw");
159 assertThat(raw.changes().get(0).diffs()).containsOnlyKeys("severity");
160 assertThat(raw.changes().get(1).userUuid()).isEqualTo("default_user_uuid");
161 assertThat(raw.changes().get(1).diffs()).containsOnlyKeys(IssueFieldsSetter.FROM_BRANCH);
162 assertThat(raw.changes().get(1).get(IssueFieldsSetter.FROM_BRANCH).oldValue()).isEqualTo("#2");
163 assertThat(raw.changes().get(1).get(IssueFieldsSetter.FROM_BRANCH).newValue()).isEqualTo("master");
167 public void copyExistingIssuesFromSourceBranchOfPullRequest() {
168 String pullRequestKey = "1";
169 Branch branch = mock(Branch.class);
170 when(branch.getType()).thenReturn(BranchType.PULL_REQUEST);
171 when(branch.getName()).thenReturn("sourceBranch-1");
172 when(branch.getPullRequestKey()).thenReturn(pullRequestKey);
173 analysisMetadataHolder.setBranch(branch);
174 analysisMetadataHolder.setPullRequestKey(pullRequestKey);
175 DefaultIssue raw = new DefaultIssue()
177 DefaultIssue fromShort = new DefaultIssue()
179 fromShort.setResolution("resolution");
180 fromShort.setStatus("status");
182 Date commentDate = new Date();
183 fromShort.addComment(new DefaultIssueComment()
184 .setIssueKey("short")
185 .setCreatedAt(commentDate)
186 .setUserUuid("user_uuid")
187 .setMarkdownText("A comment"));
189 Date diffDate = new Date();
191 fromShort.addChange(new FieldDiffs()
192 .setCreationDate(diffDate)
193 .setIssueKey("short")
194 .setUserUuid("user_uuid")
195 .setDiff("file", "uuidA1", "uuidB1"));
196 // file diff with another field
197 fromShort.addChange(new FieldDiffs()
198 .setCreationDate(diffDate)
199 .setIssueKey("short")
200 .setUserUuid("user_uuid")
201 .setDiff("severity", "MINOR", "MAJOR")
202 .setDiff("file", "uuidA2", "uuidB2"));
204 underTest.copyExistingIssueFromSourceBranchToPullRequest(raw, fromShort);
206 assertThat(raw.resolution()).isEqualTo("resolution");
207 assertThat(raw.status()).isEqualTo("status");
208 assertThat(raw.defaultIssueComments())
209 .extracting(DefaultIssueComment::issueKey, DefaultIssueComment::createdAt, DefaultIssueComment::userUuid, DefaultIssueComment::markdownText)
210 .containsOnly(tuple("raw", commentDate, "user_uuid", "A comment"));
211 assertThat(raw.changes()).hasSize(2);
212 assertThat(raw.changes().get(0).creationDate()).isEqualTo(diffDate);
213 assertThat(raw.changes().get(0).userUuid()).isEqualTo("user_uuid");
214 assertThat(raw.changes().get(0).issueKey()).isEqualTo("raw");
215 assertThat(raw.changes().get(0).diffs()).containsOnlyKeys("severity");
216 assertThat(raw.changes().get(1).userUuid()).isEqualTo("default_user_uuid");
217 assertThat(raw.changes().get(1).diffs()).containsOnlyKeys(IssueFieldsSetter.FROM_BRANCH);
218 assertThat(raw.changes().get(1).get(IssueFieldsSetter.FROM_BRANCH).oldValue()).isEqualTo("sourceBranch-1");
219 assertThat(raw.changes().get(1).get(IssueFieldsSetter.FROM_BRANCH).newValue()).isEqualTo("#1");
223 public void copyExistingIssuesFromSourceBranchOfPullRequest_only_works_for_pull_requests() {
224 DefaultIssue raw = new DefaultIssue()
226 DefaultIssue from = new DefaultIssue()
228 from.setResolution("resolution");
229 from.setStatus("status");
230 Branch branch = mock(Branch.class);
231 when(branch.getType()).thenReturn(BranchType.BRANCH);
232 analysisMetadataHolder.setBranch(branch);
234 assertThatThrownBy(() -> underTest.copyExistingIssueFromSourceBranchToPullRequest(raw, from))
235 .isInstanceOf(IllegalStateException.class)
236 .hasMessage("This operation should be done only on pull request analysis");
240 public void copiedIssue() {
241 DefaultIssue raw = new DefaultIssue()
244 .setCreationDate(parseDate("2015-10-01"))
245 .setUpdateDate(parseDate("2015-10-02"))
246 .setCloseDate(parseDate("2015-10-03"));
248 DbIssues.Locations issueLocations = DbIssues.Locations.newBuilder()
249 .setTextRange(DbCommons.TextRange.newBuilder()
254 DefaultIssue base = new DefaultIssue()
256 .setCreationDate(parseDate("2015-01-01"))
257 .setUpdateDate(parseDate("2015-01-02"))
258 .setCloseDate(parseDate("2015-01-03"))
259 .setResolution(RESOLUTION_FIXED)
260 .setStatus(STATUS_CLOSED)
261 .setSeverity(BLOCKER)
262 .setAssigneeUuid("base assignee uuid")
263 .setAuthorLogin("base author")
264 .setTags(newArrayList("base tag"))
265 .setOnDisabledRule(true)
266 .setSelectedAt(1000L)
268 .setMessage("message")
270 .setEffort(Duration.create(15L))
271 .setManualSeverity(false)
272 .setLocations(issueLocations);
274 when(debtCalculator.calculate(raw)).thenReturn(DEFAULT_DURATION);
276 Branch branch = mock(Branch.class);
277 when(branch.getName()).thenReturn("release-2.x");
278 analysisMetadataHolder.setBranch(branch);
280 underTest.copyExistingOpenIssueFromBranch(raw, base, "master");
282 assertThat(raw.isNew()).isFalse();
283 assertThat(raw.isCopied()).isTrue();
284 assertThat(raw.key()).isNotNull();
285 assertThat(raw.key()).isNotEqualTo(base.key());
286 assertThat(raw.creationDate()).isEqualTo(base.creationDate());
287 assertThat(raw.updateDate()).isEqualTo(base.updateDate());
288 assertThat(raw.closeDate()).isEqualTo(base.closeDate());
289 assertThat(raw.resolution()).isEqualTo(RESOLUTION_FIXED);
290 assertThat(raw.status()).isEqualTo(STATUS_CLOSED);
291 assertThat(raw.assignee()).isEqualTo("base assignee uuid");
292 assertThat(raw.authorLogin()).isEqualTo("base author");
293 assertThat(raw.tags()).containsOnly("base tag");
294 assertThat(raw.effort()).isEqualTo(DEFAULT_DURATION);
295 assertThat(raw.isOnDisabledRule()).isTrue();
296 assertThat(raw.selectedAt()).isEqualTo(1000L);
297 assertThat(raw.changes().get(0).get(IssueFieldsSetter.FROM_BRANCH).oldValue()).isEqualTo("master");
298 assertThat(raw.changes().get(0).get(IssueFieldsSetter.FROM_BRANCH).newValue()).isEqualTo("release-2.x");
300 verifyNoInteractions(updater);
304 public void doAutomaticTransition() {
305 DefaultIssue issue = new DefaultIssue();
307 underTest.doAutomaticTransition(issue);
309 verify(workflow).doAutomaticTransition(issue, issueChangeContext);
313 public void mergeExistingOpenIssue() {
314 DefaultIssue raw = new DefaultIssue()
318 .setCreationDate(parseDate("2015-10-01"))
319 .setUpdateDate(parseDate("2015-10-02"))
320 .setCloseDate(parseDate("2015-10-03"));
322 DbIssues.Locations issueLocations = DbIssues.Locations.newBuilder()
323 .setTextRange(DbCommons.TextRange.newBuilder()
328 DefaultIssue base = new DefaultIssue()
330 .setCreationDate(parseDate("2015-01-01"))
331 .setUpdateDate(parseDate("2015-01-02"))
332 .setResolution(RESOLUTION_FALSE_POSITIVE)
333 .setStatus(STATUS_RESOLVED)
334 .setSeverity(BLOCKER)
335 .setAssigneeUuid("base assignee uuid")
336 .setAuthorLogin("base author")
337 .setTags(newArrayList("base tag"))
338 .setOnDisabledRule(true)
339 .setSelectedAt(1000L)
341 .setMessage("message")
343 .setEffort(Duration.create(15L))
344 .setManualSeverity(false)
345 .setLocations(issueLocations)
346 .addChange(new FieldDiffs().setDiff("foo", "bar", "donut"))
347 .addChange(new FieldDiffs().setDiff("file", "A", "B"));
349 when(debtCalculator.calculate(raw)).thenReturn(DEFAULT_DURATION);
351 underTest.mergeExistingOpenIssue(raw, base);
353 assertThat(raw.isNew()).isFalse();
354 assertThat(raw.key()).isEqualTo("BASE_KEY");
355 assertThat(raw.creationDate()).isEqualTo(base.creationDate());
356 assertThat(raw.updateDate()).isEqualTo(base.updateDate());
357 assertThat(raw.resolution()).isEqualTo(RESOLUTION_FALSE_POSITIVE);
358 assertThat(raw.status()).isEqualTo(STATUS_RESOLVED);
359 assertThat(raw.assignee()).isEqualTo("base assignee uuid");
360 assertThat(raw.authorLogin()).isEqualTo("base author");
361 assertThat(raw.tags()).containsOnly("base tag");
362 assertThat(raw.effort()).isEqualTo(DEFAULT_DURATION);
363 assertThat(raw.isOnDisabledRule()).isTrue();
364 assertThat(raw.selectedAt()).isEqualTo(1000L);
365 assertThat(raw.isChanged()).isFalse();
366 assertThat(raw.changes()).hasSize(2);
367 assertThat(raw.changes().get(0).diffs())
368 .containsOnly(entry("foo", new FieldDiffs.Diff<>("bar", "donut")));
369 assertThat(raw.changes().get(1).diffs())
370 .containsOnly(entry("file", new FieldDiffs.Diff<>("A", "B")));
372 verify(updater).setPastSeverity(raw, BLOCKER, issueChangeContext);
373 verify(updater).setPastLine(raw, 10);
374 verify(updater).setPastMessage(raw, "message", issueChangeContext);
375 verify(updater).setPastEffort(raw, Duration.create(15L), issueChangeContext);
376 verify(updater).setPastLocations(raw, issueLocations);
380 public void mergeExistingOpenIssue_with_manual_severity() {
381 DefaultIssue raw = new DefaultIssue()
385 DefaultIssue base = new DefaultIssue()
387 .setResolution(RESOLUTION_FIXED)
388 .setStatus(STATUS_CLOSED)
389 .setSeverity(BLOCKER)
390 .setManualSeverity(true);
392 underTest.mergeExistingOpenIssue(raw, base);
394 assertThat(raw.manualSeverity()).isTrue();
395 assertThat(raw.severity()).isEqualTo(BLOCKER);
397 verify(updater, never()).setPastSeverity(raw, BLOCKER, issueChangeContext);
401 public void mergeExistingOpenIssue_with_base_changed() {
402 DefaultIssue raw = new DefaultIssue()
406 DefaultIssue base = new DefaultIssue()
409 .setResolution(RESOLUTION_FALSE_POSITIVE)
410 .setStatus(STATUS_RESOLVED);
412 underTest.mergeExistingOpenIssue(raw, base);
414 assertThat(raw.isChanged()).isTrue();
418 public void mergeExistingOpenIssue_with_attributes() {
419 DefaultIssue raw = new DefaultIssue()
423 DefaultIssue base = new DefaultIssue()
425 .setResolution(RESOLUTION_FIXED)
426 .setStatus(STATUS_CLOSED)
427 .setSeverity(BLOCKER)
428 .setAttributes(ImmutableMap.of("JIRA", "SONAR-01"));
430 underTest.mergeExistingOpenIssue(raw, base);
432 assertThat(raw.attributes()).containsEntry("JIRA", "SONAR-01");