3 * Copyright (C) 2009-2024 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.Collection;
23 import java.util.Collections;
24 import java.util.stream.Stream;
25 import org.junit.Rule;
26 import org.junit.Test;
27 import org.slf4j.event.Level;
28 import org.sonar.api.issue.DefaultTransitions;
29 import org.sonar.api.rule.RuleKey;
30 import org.sonar.api.testfixtures.log.LogTester;
31 import org.sonar.ce.task.log.CeTaskMessages;
32 import org.sonar.ce.task.projectanalysis.component.Component;
33 import org.sonar.ce.task.projectanalysis.component.ComponentImpl;
34 import org.sonar.ce.task.projectanalysis.component.ProjectAttributes;
35 import org.sonar.ce.task.projectanalysis.component.ReportAttributes;
36 import org.sonar.core.issue.AnticipatedTransition;
37 import org.sonar.core.issue.DefaultIssue;
39 import static org.assertj.core.api.Assertions.assertThat;
40 import static org.mockito.ArgumentMatchers.any;
41 import static org.mockito.Mockito.doThrow;
42 import static org.mockito.Mockito.mock;
43 import static org.mockito.Mockito.times;
44 import static org.mockito.Mockito.verify;
45 import static org.mockito.Mockito.verifyNoInteractions;
46 import static org.mockito.Mockito.verifyNoMoreInteractions;
47 import static org.mockito.Mockito.when;
48 import static org.sonar.api.issue.Issue.STATUS_OPEN;
49 import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
50 import static org.sonar.ce.task.projectanalysis.component.Component.Type.PROJECT;
52 public class TransitionIssuesToAnticipatedStatesVisitorTest {
54 public LogTester logTester = new LogTester();
55 private final IssueLifecycle issueLifecycle = mock(IssueLifecycle.class);
57 private final AnticipatedTransitionRepository anticipatedTransitionRepository = mock(AnticipatedTransitionRepository.class);
59 private final CeTaskMessages ceTaskMessages = mock(CeTaskMessages.class);
61 private final TransitionIssuesToAnticipatedStatesVisitor underTest = new TransitionIssuesToAnticipatedStatesVisitor(anticipatedTransitionRepository, issueLifecycle, ceTaskMessages);
64 public void givenMatchingAnticipatedTransitions_transitionsShouldBeAppliedToIssues() {
65 Component component = getComponent(Component.Type.FILE);
66 when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(getAnticipatedTransitions("projectKey", "fileName"));
68 DefaultIssue issue = getDefaultIssue(1, "abcdefghi", "issue message");
70 underTest.beforeComponent(component);
71 underTest.onIssue(component, issue);
73 assertThat(issue.isBeingClosed()).isTrue();
74 assertThat(issue.getAnticipatedTransitionUuid()).isPresent();
75 verify(issueLifecycle).doManualTransition(issue, DefaultTransitions.ACCEPT, "admin");
76 verify(issueLifecycle).addComment(issue, "doing the transition in an anticipated way", "admin");
80 public void givenMatchingAnticipatedTransitions_whenExceptionIsThrown_transitionsShouldNotBeAppliedAndWarningLogged() {
81 Component component = getComponent(Component.Type.FILE);
82 String exceptionMessage = "Cannot apply transition";
84 when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(getAnticipatedTransitions("projectKey", "fileName"));
85 doThrow(new IllegalStateException(exceptionMessage)).when(issueLifecycle).doManualTransition(any(), any(), any());
86 DefaultIssue issue = getDefaultIssue(1, "abcdefghi", "issue message");
87 issue.setComponentKey(component.getKey());
89 underTest.beforeComponent(component);
90 underTest.onIssue(component, issue);
92 assertThat(issue.isBeingClosed()).isFalse();
93 assertThat(issue.getAnticipatedTransitionUuid()).isEmpty();
94 verify(issueLifecycle).doManualTransition(issue, DefaultTransitions.ACCEPT, "admin");
95 verifyNoMoreInteractions(issueLifecycle);
96 assertThat(logTester.logs(Level.WARN))
97 .contains(String.format("Cannot resolve issue at line %s of %s due to: %s", issue.getLine(), issue.componentKey(), exceptionMessage));
98 verify(ceTaskMessages, times(1)).add(any());
102 public void givenMatchingAnticipatedTransitionsOnResolvedIssue_transitionsShouldNotBeAppliedToIssues() {
103 Component component = getComponent(Component.Type.FILE);
104 when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(getAnticipatedTransitions("projectKey", "fileName"));
106 DefaultIssue issue = getDefaultIssue(1, "abcdefghi", "issue message");
107 issue.setStatus(STATUS_RESOLVED);
109 underTest.beforeComponent(component);
110 underTest.onIssue(component, issue);
112 assertThat(issue.isBeingClosed()).isFalse();
113 assertThat(issue.getAnticipatedTransitionUuid()).isNotPresent();
114 verifyNoInteractions(issueLifecycle);
118 public void givenMatchingAnticipatedTransitions_whenIssueIsNotNew_transitionsShouldNotBeAppliedToIssues() {
119 Component component = getComponent(Component.Type.FILE);
120 when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(getAnticipatedTransitions("projectKey", "fileName"));
122 DefaultIssue issue = getDefaultIssue(1, "abcdefghi", "issue message");
125 underTest.beforeComponent(component);
126 underTest.onIssue(component, issue);
128 assertThat(issue.isBeingClosed()).isFalse();
129 assertThat(issue.getAnticipatedTransitionUuid()).isNotPresent();
130 verifyNoInteractions(issueLifecycle);
134 public void givenNonMatchingAnticipatedTransitions_transitionsAreNotAppliedToIssues() {
135 Component component = getComponent(Component.Type.FILE);
136 when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(getAnticipatedTransitions("projectKey", "fileName"));
138 DefaultIssue issue = getDefaultIssue(2, "abcdefghf", "another issue message");
140 underTest.beforeComponent(component);
141 underTest.onIssue(component, issue);
143 assertThat(issue.isBeingClosed()).isFalse();
144 assertThat(issue.getAnticipatedTransitionUuid()).isNotPresent();
145 verifyNoInteractions(issueLifecycle);
149 public void givenMatchingAnticipatedTransitionsWithEmptyComment_transitionsShouldBeAppliedToIssuesAndDefaultCommentApplied() {
150 Component component = getComponent(Component.Type.FILE);
151 when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(getAnticipatedTransitionsWithEmptyComment("projectKey", "fileName"));
153 DefaultIssue issue = getDefaultIssue(1, "abcdefghi", "issue message");
155 underTest.beforeComponent(component);
156 underTest.onIssue(component, issue);
158 assertThat(issue.isBeingClosed()).isTrue();
159 assertThat(issue.getAnticipatedTransitionUuid()).isPresent();
160 verify(issueLifecycle).doManualTransition(issue, DefaultTransitions.ACCEPT, "admin");
161 verify(issueLifecycle).addComment(issue, "Automatically transitioned from SonarLint", "admin");
165 public void givenAFileComponent_theRepositoryIsHitForFetchingAnticipatedTransitions() {
166 Component component = getComponent(Component.Type.FILE);
167 when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(Collections.emptyList());
169 underTest.beforeComponent(component);
171 verify(anticipatedTransitionRepository).getAnticipatedTransitionByComponent(component);
175 public void givenAProjecComponent_theRepositoryIsNotQueriedForAnticipatedTransitions() {
176 Component component = getComponent(PROJECT);
177 when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(Collections.emptyList());
179 underTest.beforeComponent(component);
181 verifyNoInteractions(anticipatedTransitionRepository);
184 private Collection<AnticipatedTransition> getAnticipatedTransitions(String projecKey, String fileName) {
185 return Stream.of(new AnticipatedTransition("atuuid", projecKey, "admin", RuleKey.parse("repo:id"), "issue message", fileName, 1, "abcdefghi", DefaultTransitions.ACCEPT, "doing the transition in an anticipated way")).toList();
188 private Collection<AnticipatedTransition> getAnticipatedTransitionsWithEmptyComment(String projecKey, String fileName) {
189 return Stream.of(new AnticipatedTransition("atuuid", projecKey, "admin", RuleKey.parse("repo:id"), "issue message", fileName, 1, "abcdefghi", DefaultTransitions.ACCEPT, null)).toList();
192 private Component getComponent(Component.Type type) {
193 ComponentImpl.Builder builder = ComponentImpl.builder(type)
194 .setUuid("componentUuid")
195 .setKey("projectKey:filename")
197 .setStatus(Component.Status.ADDED)
198 .setShortName("filename")
199 .setReportAttributes(mock(ReportAttributes.class));
201 if (PROJECT.equals(type)) {
202 builder.setProjectAttributes(mock(ProjectAttributes.class));
205 return builder.build();
208 private DefaultIssue getDefaultIssue(Integer line, String hash, String message) {
209 DefaultIssue defaultIssue = new DefaultIssue();
210 defaultIssue.setStatus(STATUS_OPEN);
211 defaultIssue.setResolution(null);
212 defaultIssue.setLine(line);
213 defaultIssue.setChecksum(hash);
214 defaultIssue.setMessage(message);
215 defaultIssue.setRuleKey(RuleKey.of("repo", "id"));