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.server.pushapi.issues;
22 import java.nio.charset.StandardCharsets;
23 import java.util.Deque;
24 import java.util.HashMap;
26 import java.util.Optional;
28 import javax.annotation.Nullable;
29 import org.junit.Rule;
30 import org.junit.Test;
31 import org.sonar.api.rules.RuleType;
32 import org.sonar.core.issue.DefaultIssue;
33 import org.sonar.core.issue.FieldDiffs;
34 import org.sonar.db.DbTester;
35 import org.sonar.db.component.BranchDto;
36 import org.sonar.db.component.BranchType;
37 import org.sonar.db.component.ComponentDto;
38 import org.sonar.db.component.ProjectData;
39 import org.sonar.db.issue.IssueDto;
40 import org.sonar.db.project.ProjectDto;
41 import org.sonar.db.pushevent.PushEventDto;
42 import org.sonar.db.rule.RuleDto;
43 import org.sonarqube.ws.Common;
45 import static org.assertj.core.api.Assertions.assertThat;
46 import static org.assertj.core.api.Assertions.tuple;
47 import static org.sonar.api.issue.DefaultTransitions.ACCEPT;
48 import static org.sonar.api.issue.DefaultTransitions.CONFIRM;
49 import static org.sonar.api.issue.DefaultTransitions.FALSE_POSITIVE;
50 import static org.sonar.api.issue.DefaultTransitions.REOPEN;
51 import static org.sonar.api.issue.DefaultTransitions.RESOLVE;
52 import static org.sonar.api.issue.DefaultTransitions.UNCONFIRM;
53 import static org.sonar.api.issue.DefaultTransitions.WONT_FIX;
54 import static org.sonar.api.rules.RuleType.CODE_SMELL;
55 import static org.sonar.db.component.ComponentTesting.newFileDto;
56 import static org.sonarqube.ws.Common.Severity.BLOCKER;
57 import static org.sonarqube.ws.Common.Severity.CRITICAL;
58 import static org.sonarqube.ws.Common.Severity.MAJOR;
60 public class IssueChangeEventServiceImplTest {
63 public DbTester db = DbTester.create();
65 public final IssueChangeEventServiceImpl underTest = new IssueChangeEventServiceImpl(db.getDbClient());
68 public void distributeIssueChangeEvent_whenSingleIssueChange_shouldChangeSeverity() {
69 ProjectData projectData = db.components().insertPublicProject();
70 RuleDto rule = db.rules().insert();
71 IssueDto issue = db.issues().insert(rule, projectData.getMainBranchDto(), projectData.getMainBranchComponent(), i -> i.setSeverity(MAJOR.name()));
73 assertPushEventIsPersisted(projectData.getProjectDto(), projectData.getMainBranchDto(), issue, BLOCKER.name(), null, null, null, 1);
77 public void distributeIssueChangeEvent_whenSingleIssueChange_shouldChangeType() {
78 ProjectData projectData = db.components().insertPublicProject();
79 RuleDto rule = db.rules().insert();
80 IssueDto issue = db.issues().insert(rule, projectData.getMainBranchDto(), projectData.getMainBranchComponent(), i -> i.setSeverity(MAJOR.name()));
82 assertPushEventIsPersisted(projectData.getProjectDto(), projectData.getMainBranchDto(), issue, null, Common.RuleType.BUG.name(), null, null, 1);
86 public void distributeIssueChangeEvent_whenSingleIssueChange_shouldExecuteTransitionChanges() {
87 ProjectData projectData = db.components().insertPublicProject();
88 ProjectDto project = projectData.getProjectDto();
89 BranchDto mainBranch = projectData.getMainBranchDto();
90 RuleDto rule = db.rules().insert();
91 IssueDto issue = db.issues().insert(rule, mainBranch, projectData.getMainBranchComponent(), i -> i.setSeverity(MAJOR.name()));
93 assertPushEventIsPersisted(project, mainBranch, issue, null, null, ACCEPT, true, 1);
94 assertPushEventIsPersisted(project, mainBranch, issue, null, null, WONT_FIX, true, 2);
95 assertPushEventIsPersisted(project, mainBranch, issue, null, null, REOPEN, false, 3);
96 assertPushEventIsPersisted(project, mainBranch, issue, null, null, FALSE_POSITIVE, true, 4);
97 assertPushEventIsPersisted(project, mainBranch, issue, null, null, REOPEN, false, 5);
98 assertPushEventIsPersisted(project, mainBranch, issue, null, null, RESOLVE, false, 6);
99 assertPushEventIsPersisted(project, mainBranch, issue, null, null, REOPEN, false, 7);
100 assertNoIssueDistribution(project, mainBranch, issue, null, null, CONFIRM, 8);
101 assertNoIssueDistribution(project, mainBranch, issue, null, null, UNCONFIRM, 9);
105 public void distributeIssueChangeEvent_whenSingleIssueChangeOnABranch_shouldChangeSeverity() {
106 ProjectData projectData = db.components().insertPublicProject();
107 BranchDto featureBranch = db.components().insertProjectBranch(projectData.getProjectDto(), b -> b.setKey("feature1"));
108 ComponentDto branchComponent = db.components().insertFile(featureBranch);
109 RuleDto rule = db.rules().insert();
110 IssueDto issue = db.issues().insert(rule, featureBranch, branchComponent, i -> i.setSeverity(MAJOR.name()));
111 assertPushEventIsPersisted(projectData.getProjectDto(), featureBranch, issue, BLOCKER.name(), null, null, null, 1);
115 public void distributeIssueChangeEvent_whenSingleIssueChange_shouldExecuteSeveralChanges() {
116 ProjectData projectData = db.components().insertPublicProject();
117 RuleDto rule = db.rules().insert();
118 IssueDto issue = db.issues().insert(rule, projectData.getMainBranchDto(), projectData.getMainBranchComponent(), i -> i.setSeverity(MAJOR.name()));
120 assertPushEventIsPersisted(projectData.getProjectDto(), projectData.getMainBranchDto(), issue, BLOCKER.name(), Common.RuleType.BUG.name(), ACCEPT, true, 1);
124 public void distributeIssueChangeEvent_whenBulkIssueChange_shouldDistributesEvents() {
125 RuleDto rule = db.rules().insert();
127 ProjectData projectData1 = db.components().insertPublicProject();
128 ProjectDto project1 = projectData1.getProjectDto();
129 BranchDto branch1 = projectData1.getMainBranchDto();
130 ComponentDto componentDto1 = projectData1.getMainBranchComponent();
131 IssueDto issue1 = db.issues().insert(rule, branch1, componentDto1, i -> i.setSeverity(MAJOR.name()).setType(RuleType.BUG));
133 ProjectData projectData2 = db.components().insertPublicProject();
134 ProjectDto project2 = projectData2.getProjectDto();
135 BranchDto branch2 = projectData2.getMainBranchDto();
136 ComponentDto componentDto2 = projectData2.getMainBranchComponent();
137 IssueDto issue2 = db.issues().insert(rule, branch2, componentDto2, i -> i.setSeverity(MAJOR.name()).setType(RuleType.BUG));
139 ProjectData projectData3 = db.components().insertPublicProject();
140 ProjectDto project3 = projectData3.getProjectDto();
141 BranchDto branch3 = projectData3.getMainBranchDto();
142 ComponentDto componentDto3 = projectData3.getMainBranchComponent();
143 IssueDto issue3 = db.issues().insert(rule, branch3, componentDto3, i -> i.setSeverity(MAJOR.name()).setType(RuleType.BUG));
145 DefaultIssue defaultIssue1 = issue1.toDefaultIssue().setCurrentChangeWithoutAddChange(new FieldDiffs()
146 .setDiff("resolution", null, null)
147 .setDiff("severity", MAJOR.name(), CRITICAL.name())
148 .setDiff("type", RuleType.BUG.name(), CODE_SMELL.name()));
149 DefaultIssue defaultIssue2 = issue2.toDefaultIssue().setCurrentChangeWithoutAddChange(new FieldDiffs()
150 .setDiff("resolution", "OPEN", "FALSE-POSITIVE")
151 .setDiff("severity", MAJOR.name(), CRITICAL.name())
152 .setDiff("type", RuleType.BUG.name(), CODE_SMELL.name()));
154 Set<DefaultIssue> issues = Set.of(defaultIssue1, defaultIssue2, issue3.toDefaultIssue());
155 Map<String, ComponentDto> projectsByUuid = new HashMap<>();
156 projectsByUuid.put(componentDto1.branchUuid(), componentDto1);
157 projectsByUuid.put(componentDto2.branchUuid(), componentDto2);
158 projectsByUuid.put(componentDto3.branchUuid(), componentDto3);
159 Map<String, BranchDto> branchesByProjectUuid = new HashMap<>();
160 branchesByProjectUuid.put(componentDto1.branchUuid(), branch1);
161 branchesByProjectUuid.put(componentDto2.branchUuid(), branch2);
162 branchesByProjectUuid.put(componentDto3.branchUuid(), branch3);
164 underTest.distributeIssueChangeEvent(issues, projectsByUuid, branchesByProjectUuid);
166 Deque<PushEventDto> issueChangedEvents = db.getDbClient().pushEventDao()
167 .selectChunkByProjectUuids(db.getSession(), Set.of(project1.getUuid(), project2.getUuid()),
170 assertThat(issueChangedEvents).hasSize(2);
172 assertThat(issueChangedEvents)
173 .extracting(PushEventDto::getName, PushEventDto::getProjectUuid)
174 .containsExactlyInAnyOrder(
175 tuple("IssueChanged", project1.getUuid()),
176 tuple("IssueChanged", project2.getUuid()));
178 Optional<PushEventDto> project1Event = issueChangedEvents.stream().filter(e -> e.getProjectUuid().equals(project1.getUuid())).findFirst();
179 Optional<PushEventDto> project2Event = issueChangedEvents.stream().filter(e -> e.getProjectUuid().equals(project2.getUuid())).findFirst();
181 assertThat(project1Event).isPresent();
182 assertThat(project2Event).isPresent();
184 String firstPayload = new String(project1Event.get().getPayload(), StandardCharsets.UTF_8);
185 assertThat(firstPayload)
186 .contains("\"userSeverity\":\"" + CRITICAL.name() + "\"",
187 "\"userType\":\"" + CODE_SMELL.name() + "\"",
188 "\"resolved\":" + false);
190 String secondPayload = new String(project2Event.get().getPayload(), StandardCharsets.UTF_8);
191 assertThat(secondPayload)
192 .contains("\"userSeverity\":\"" + CRITICAL.name() + "\"",
193 "\"userType\":\"" + CODE_SMELL.name() + "\"",
194 "\"resolved\":" + true);
198 public void distributeIssueChangeEvent_whenPullRequestIssues_shouldNotDistributeEvents() {
199 RuleDto rule = db.rules().insert();
201 ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
202 ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setKey("myBranch1")
203 .setBranchType(BranchType.PULL_REQUEST)
204 .setMergeBranchUuid(project.uuid()));
205 BranchDto branch1 = db.getDbClient().branchDao().selectByUuid(db.getSession(), pullRequest.uuid()).get();
206 ComponentDto file = db.components().insertComponent(newFileDto(pullRequest));
207 IssueDto issue1 = db.issues().insert(rule, pullRequest, file, i -> i.setSeverity(MAJOR.name()).setType(RuleType.BUG));
209 DefaultIssue defaultIssue1 = issue1.toDefaultIssue().setCurrentChangeWithoutAddChange(new FieldDiffs()
210 .setDiff("resolution", null, null)
211 .setDiff("severity", MAJOR.name(), CRITICAL.name())
212 .setDiff("type", RuleType.BUG.name(), CODE_SMELL.name()));
214 Set<DefaultIssue> issues = Set.of(defaultIssue1);
215 Map<String, ComponentDto> projectsByUuid = new HashMap<>();
216 projectsByUuid.put(project.branchUuid(), project);
217 Map<String, BranchDto> branchesByProjectUuid = new HashMap<>();
218 branchesByProjectUuid.put(project.branchUuid(), branch1);
220 underTest.distributeIssueChangeEvent(issues, projectsByUuid, branchesByProjectUuid);
222 Deque<PushEventDto> events = db.getDbClient().pushEventDao()
223 .selectChunkByProjectUuids(db.getSession(), Set.of(project.uuid()), 1l, null, 20);
224 assertThat(events).isEmpty();
227 private void assertNoIssueDistribution(ProjectDto project, BranchDto branch, IssueDto issue, @Nullable String severity,
228 @Nullable String type, @Nullable String transition, int page) {
229 underTest.distributeIssueChangeEvent(issue.toDefaultIssue(), severity, type, transition, branch, project.getKey());
231 Deque<PushEventDto> events = db.getDbClient().pushEventDao()
232 .selectChunkByProjectUuids(db.getSession(), Set.of(project.getUuid()), 1l, null, page);
233 assertThat(events).hasSizeLessThan(page);
236 private void assertPushEventIsPersisted(ProjectDto project, BranchDto branch, IssueDto issue, @Nullable String severity,
237 @Nullable String type, @Nullable String transition, Boolean resolved, int page) {
238 underTest.distributeIssueChangeEvent(issue.toDefaultIssue(), severity, type, transition, branch, project.getKey());
240 Deque<PushEventDto> events = db.getDbClient().pushEventDao()
241 .selectChunkByProjectUuids(db.getSession(), Set.of(project.getUuid()), 1l, null, page);
242 assertThat(events).isNotEmpty();
243 assertThat(events).extracting(PushEventDto::getName, PushEventDto::getProjectUuid)
244 .contains(tuple("IssueChanged", project.getUuid()));
246 String payload = new String(events.getLast().getPayload(), StandardCharsets.UTF_8);
247 if (severity != null) {
248 assertThat(payload).contains("\"userSeverity\":\"" + severity + "\"");
252 assertThat(payload).contains("\"userType\":\"" + type + "\"");
255 if (resolved != null) {
256 assertThat(payload).contains("\"resolved\":" + resolved);