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.developers.ws;
22 import java.io.UnsupportedEncodingException;
23 import java.net.URLEncoder;
24 import java.util.Date;
25 import java.util.stream.Stream;
26 import org.junit.Rule;
27 import org.junit.Test;
28 import org.sonar.api.platform.Server;
29 import org.sonar.api.rules.RuleType;
30 import org.sonar.db.DbTester;
31 import org.sonar.db.ce.CeActivityDto;
32 import org.sonar.db.ce.CeQueueDto;
33 import org.sonar.db.ce.CeTaskTypes;
34 import org.sonar.db.component.BranchType;
35 import org.sonar.db.component.ComponentDto;
36 import org.sonar.db.component.ProjectData;
37 import org.sonar.db.component.SnapshotDto;
38 import org.sonar.db.rule.RuleDto;
39 import org.sonar.server.es.EsTester;
40 import org.sonar.server.issue.index.IssueIndex;
41 import org.sonar.server.issue.index.IssueIndexSyncProgressChecker;
42 import org.sonar.server.issue.index.IssueIndexer;
43 import org.sonar.server.issue.index.IssueIteratorFactory;
44 import org.sonar.server.tester.UserSessionRule;
45 import org.sonar.server.ws.WsActionTester;
46 import org.sonarqube.ws.Developers.SearchEventsWsResponse;
47 import org.sonarqube.ws.Developers.SearchEventsWsResponse.Event;
49 import static java.lang.String.format;
50 import static java.nio.charset.StandardCharsets.UTF_8;
51 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
52 import static org.apache.commons.lang.math.RandomUtils.nextInt;
53 import static org.apache.commons.lang.math.RandomUtils.nextLong;
54 import static org.assertj.core.api.Assertions.assertThat;
55 import static org.assertj.core.api.Assertions.tuple;
56 import static org.mockito.Mockito.mock;
57 import static org.mockito.Mockito.when;
58 import static org.sonar.api.utils.DateUtils.formatDateTime;
59 import static org.sonar.api.web.UserRole.USER;
60 import static org.sonar.db.component.BranchType.BRANCH;
61 import static org.sonar.server.developers.ws.SearchEventsAction.PARAM_FROM;
62 import static org.sonar.server.developers.ws.SearchEventsAction.PARAM_PROJECTS;
64 public class SearchEventsActionNewIssuesIT {
66 private static final RuleType[] RULE_TYPES_EXCEPT_HOTSPOT = Stream.of(RuleType.values())
67 .filter(r -> r != RuleType.SECURITY_HOTSPOT)
68 .toArray(RuleType[]::new);
71 public DbTester db = DbTester.create();
73 public EsTester es = EsTester.create();
75 public UserSessionRule userSession = UserSessionRule.standalone();
77 private Server server = mock(Server.class);
79 private IssueIndex issueIndex = new IssueIndex(es.client(), null, null, null);
80 private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()), null);
81 private IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class);
82 private WsActionTester ws = new WsActionTester(new SearchEventsAction(db.getDbClient(), userSession, server, issueIndex,
83 issueIndexSyncProgressChecker));
86 public void issue_event() {
88 when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
89 ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
90 userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
91 SnapshotDto analysis = insertAnalysis(project, 1_500_000_000_000L);
92 insertIssue(project, analysis);
93 insertIssue(project, analysis);
95 insertSecurityHotspot(project, analysis);
96 issueIndexer.indexAllIssues();
98 long from = analysis.getCreatedAt() - 1_000_000L;
99 SearchEventsWsResponse result = ws.newRequest()
100 .setParam(PARAM_PROJECTS, project.getKey())
101 .setParam(PARAM_FROM, formatDateTime(from))
102 .executeProtobuf(SearchEventsWsResponse.class);
104 assertThat(result.getEventsList())
105 .extracting(Event::getCategory, Event::getProject, Event::getMessage, Event::getLink, Event::getDate)
107 tuple("NEW_ISSUES", project.getKey(), format("You have 2 new issues on project '%s'", project.name()),
108 format("https://sonarcloud.io/project/issues?id=%s&createdAfter=%s&assignees=%s&resolved=false", project.getKey(), encode(formatDateTime(from + 1_000L)),
109 userSession.getLogin()),
110 formatDateTime(analysis.getCreatedAt())));
114 public void many_issues_events() {
116 long from = 1_500_000_000_000L;
117 ProjectData projectData = db.components().insertPrivateProject(p -> p.setName("SonarQube"));
118 ComponentDto mainBranchComponent = projectData.getMainBranchComponent();
119 userSession.addProjectPermission(USER, projectData.getProjectDto());
120 SnapshotDto analysis = insertAnalysis(mainBranchComponent, from);
121 insertIssue(mainBranchComponent, analysis);
122 insertIssue(mainBranchComponent, analysis);
123 issueIndexer.indexAllIssues();
124 String fromDate = formatDateTime(from - 1_000L);
126 SearchEventsWsResponse result = ws.newRequest()
127 .setParam(PARAM_PROJECTS, mainBranchComponent.getKey())
128 .setParam(PARAM_FROM, fromDate)
129 .executeProtobuf(SearchEventsWsResponse.class);
131 assertThat(result.getEventsList()).extracting(Event::getCategory, Event::getMessage, Event::getProject, Event::getDate)
132 .containsExactly(tuple("NEW_ISSUES", "You have 2 new issues on project 'SonarQube'", mainBranchComponent.getKey(),
133 formatDateTime(from)));
137 public void does_not_return_old_issue() {
139 ProjectData project = db.components().insertPrivateProject();
140 userSession.addProjectPermission(USER, project.getProjectDto());
141 SnapshotDto analysis = insertAnalysis(project.getMainBranchComponent(), 1_500_000_000_000L);
142 db.issues().insert(db.rules().insert(), project.getMainBranchComponent(), project.getMainBranchComponent(),
143 i -> i.setIssueCreationDate(new Date(analysis.getCreatedAt() - 10_000L)));
144 issueIndexer.indexAllIssues();
146 SearchEventsWsResponse result = ws.newRequest()
147 .setParam(PARAM_PROJECTS, project.projectKey())
148 .setParam(PARAM_FROM, formatDateTime(analysis.getCreatedAt() - 1_000L))
149 .executeProtobuf(SearchEventsWsResponse.class);
151 assertThat(result.getEventsList()).isEmpty();
155 public void return_link_to_issue_search_for_new_issues_event() {
156 userSession.logIn("my_login");
157 ComponentDto project = db.components().insertPrivateProject(p -> p.setKey("my_project")).getMainBranchComponent();
158 userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
159 SnapshotDto analysis = insertAnalysis(project, 1_400_000_000_000L);
160 insertIssue(project, analysis);
161 issueIndexer.indexAllIssues();
162 when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
164 SearchEventsWsResponse result = ws.newRequest()
165 .setParam(PARAM_PROJECTS, project.getKey())
166 .setParam(PARAM_FROM, formatDateTime(analysis.getCreatedAt() - 1_000L))
167 .executeProtobuf(SearchEventsWsResponse.class);
169 assertThat(result.getEventsList()).extracting(Event::getLink)
170 .containsExactly("https://sonarcloud.io/project/issues?id=my_project&createdAfter=" + encode(formatDateTime(analysis.getCreatedAt())) + "&assignees=my_login&resolved=false");
174 public void branch_issues_events() {
175 userSession.logIn().setSystemAdministrator();
176 when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
177 ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
178 userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
179 String branchName1 = "branch1";
180 ComponentDto branch1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH).setKey(branchName1));
181 SnapshotDto branch1Analysis = insertAnalysis(branch1, project.uuid(), 1_500_000_000_000L);
182 insertIssue(branch1, branch1Analysis);
183 insertIssue(branch1, branch1Analysis);
184 String branchName2 = "branch2";
185 ComponentDto branch2 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setKey(branchName2));
186 SnapshotDto branch2Analysis = insertAnalysis(branch2, project.uuid(), 1_300_000_000_000L);
187 insertIssue(branch2, branch2Analysis);
188 issueIndexer.indexAllIssues();
190 long from = 1_000_000_000_000L;
191 SearchEventsWsResponse result = ws.newRequest()
192 .setParam(PARAM_PROJECTS, project.getKey())
193 .setParam(PARAM_FROM, formatDateTime(from))
194 .executeProtobuf(SearchEventsWsResponse.class);
196 assertThat(result.getEventsList())
197 .extracting(Event::getCategory, Event::getProject, Event::getMessage, Event::getLink, Event::getDate)
199 tuple("NEW_ISSUES", project.getKey(), format("You have 2 new issues on project '%s' on branch '%s'", project.name(), branchName1),
200 format("https://sonarcloud.io/project/issues?id=%s&createdAfter=%s&assignees=%s&resolved=false&branch=%s", branch1.getKey(), encode(formatDateTime(from + 1_000L)),
201 userSession.getLogin(), branchName1),
202 formatDateTime(branch1Analysis.getCreatedAt())),
203 tuple("NEW_ISSUES", project.getKey(), format("You have 1 new issue on project '%s' on branch '%s'", project.name(), branchName2),
204 format("https://sonarcloud.io/project/issues?id=%s&createdAfter=%s&assignees=%s&resolved=false&branch=%s", branch2.getKey(), encode(formatDateTime(from + 1_000L)),
205 userSession.getLogin(), branchName2),
206 formatDateTime(branch2Analysis.getCreatedAt())));
210 public void pull_request_issues_events() {
211 userSession.logIn().setSystemAdministrator();
212 when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
213 ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
214 userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
215 String nonMainBranchName = "nonMain";
216 ComponentDto nonMainBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH).setKey(nonMainBranchName));
217 SnapshotDto nonMainBranchAnalysis = insertAnalysis(nonMainBranch, project.uuid(), 1_500_000_000_000L);
218 insertIssue(nonMainBranch, nonMainBranchAnalysis);
219 insertIssue(nonMainBranch, nonMainBranchAnalysis);
220 String pullRequestKey = "42";
221 ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.PULL_REQUEST).setKey(pullRequestKey));
222 SnapshotDto pullRequestAnalysis = insertAnalysis(pullRequest, project.uuid(), 1_300_000_000_000L);
223 insertIssue(pullRequest, pullRequestAnalysis);
224 issueIndexer.indexAllIssues();
226 long from = 1_000_000_000_000L;
227 SearchEventsWsResponse result = ws.newRequest()
228 .setParam(PARAM_PROJECTS, project.getKey())
229 .setParam(PARAM_FROM, formatDateTime(from))
230 .executeProtobuf(SearchEventsWsResponse.class);
232 assertThat(result.getEventsList())
233 .extracting(Event::getCategory, Event::getProject, Event::getMessage, Event::getLink, Event::getDate)
235 tuple("NEW_ISSUES", project.getKey(), format("You have 2 new issues on project '%s' on branch '%s'", project.name(), nonMainBranchName),
236 format("https://sonarcloud.io/project/issues?id=%s&createdAfter=%s&assignees=%s&resolved=false&branch=%s", nonMainBranch.getKey(), encode(formatDateTime(from + 1_000L)),
237 userSession.getLogin(), nonMainBranchName),
238 formatDateTime(nonMainBranchAnalysis.getCreatedAt())),
239 tuple("NEW_ISSUES", project.getKey(), format("You have 1 new issue on project '%s' on pull request '%s'", project.name(), pullRequestKey),
240 format("https://sonarcloud.io/project/issues?id=%s&createdAfter=%s&assignees=%s&resolved=false&pullRequest=%s", pullRequest.getKey(),
241 encode(formatDateTime(from + 1_000L)),
242 userSession.getLogin(), pullRequestKey),
243 formatDateTime(pullRequestAnalysis.getCreatedAt())));
247 public void encode_link() {
248 userSession.logIn("rågnar").setSystemAdministrator();
249 long from = 1_500_000_000_000L;
250 ComponentDto project = db.components().insertPrivateProject(p -> p.setKey("M&M's")).getMainBranchComponent();
251 userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
252 SnapshotDto analysis = insertAnalysis(project, from);
253 insertIssue(project, analysis);
254 issueIndexer.indexAllIssues();
255 when(server.getPublicRootUrl()).thenReturn("http://sonarcloud.io");
257 String fromDate = formatDateTime(from - 1_000L);
258 SearchEventsWsResponse result = ws.newRequest()
259 .setParam(PARAM_PROJECTS, project.getKey())
260 .setParam(PARAM_FROM, fromDate)
261 .executeProtobuf(SearchEventsWsResponse.class);
263 assertThat(result.getEventsList()).extracting(Event::getLink)
264 .containsExactly("http://sonarcloud.io/project/issues?id=M%26M%27s&createdAfter=" + encode(formatDateTime(from)) + "&assignees=r%C3%A5gnar&resolved=false");
267 private String encode(String text) {
269 return URLEncoder.encode(text, UTF_8.name());
270 } catch (UnsupportedEncodingException e) {
271 throw new IllegalStateException(format("Cannot encode %s", text), e);
275 private void insertIssue(ComponentDto component, SnapshotDto analysis) {
276 RuleDto rule = db.rules().insert(r -> r.setType(randomRuleTypeExceptHotspot()));
277 db.issues().insert(rule, component, component,
278 i -> i.setIssueCreationDate(new Date(analysis.getCreatedAt()))
279 .setAssigneeUuid(userSession.getUuid())
280 .setType(randomRuleTypeExceptHotspot()));
283 private void insertSecurityHotspot(ComponentDto component, SnapshotDto analysis) {
284 RuleDto rule = db.rules().insert(r -> r.setType(RuleType.SECURITY_HOTSPOT));
285 db.issues().insert(rule, component, component,
286 i -> i.setIssueCreationDate(new Date(analysis.getCreatedAt()))
287 .setAssigneeUuid(userSession.getUuid())
288 .setType(RuleType.SECURITY_HOTSPOT));
292 private SnapshotDto insertAnalysis(ComponentDto project, long analysisDate) {
293 SnapshotDto analysis = db.components().insertSnapshot(project, s -> s.setCreatedAt(analysisDate));
294 insertActivity(project.uuid(), analysis, CeActivityDto.Status.SUCCESS);
298 private SnapshotDto insertAnalysis(ComponentDto branch, String mainBranchUuid, long analysisDate) {
299 SnapshotDto analysis = db.components().insertSnapshot(branch, s -> s.setCreatedAt(analysisDate));
300 insertActivity(mainBranchUuid, analysis, CeActivityDto.Status.SUCCESS);
304 private CeActivityDto insertActivity(String mainBranchUuid, SnapshotDto analysis, CeActivityDto.Status status) {
305 CeQueueDto queueDto = new CeQueueDto();
306 queueDto.setTaskType(CeTaskTypes.REPORT);
307 queueDto.setComponentUuid(mainBranchUuid);
308 queueDto.setUuid(randomAlphanumeric(40));
309 queueDto.setCreatedAt(nextLong());
310 CeActivityDto activityDto = new CeActivityDto(queueDto);
311 activityDto.setStatus(status);
312 activityDto.setExecutionTimeMs(nextLong());
313 activityDto.setExecutedAt(nextLong());
314 activityDto.setAnalysisUuid(analysis.getUuid());
315 db.getDbClient().ceActivityDao().insert(db.getSession(), activityDto);
320 private RuleType randomRuleTypeExceptHotspot() {
321 return RULE_TYPES_EXCEPT_HOTSPOT[nextInt(RULE_TYPES_EXCEPT_HOTSPOT.length)];