3 * Copyright (C) 2009-2022 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.SnapshotDto;
37 import org.sonar.db.rule.RuleDefinitionDto;
38 import org.sonar.server.es.EsTester;
39 import org.sonar.server.issue.index.IssueIndex;
40 import org.sonar.server.issue.index.IssueIndexSyncProgressChecker;
41 import org.sonar.server.issue.index.IssueIndexer;
42 import org.sonar.server.issue.index.IssueIteratorFactory;
43 import org.sonar.server.tester.UserSessionRule;
44 import org.sonar.server.ws.WsActionTester;
45 import org.sonarqube.ws.Developers.SearchEventsWsResponse;
46 import org.sonarqube.ws.Developers.SearchEventsWsResponse.Event;
48 import static java.lang.String.format;
49 import static java.nio.charset.StandardCharsets.UTF_8;
50 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
51 import static org.apache.commons.lang.math.RandomUtils.nextInt;
52 import static org.apache.commons.lang.math.RandomUtils.nextLong;
53 import static org.assertj.core.api.Assertions.assertThat;
54 import static org.assertj.core.api.Assertions.tuple;
55 import static org.mockito.Mockito.mock;
56 import static org.mockito.Mockito.when;
57 import static org.sonar.api.utils.DateUtils.formatDateTime;
58 import static org.sonar.db.component.BranchType.BRANCH;
59 import static org.sonar.server.developers.ws.SearchEventsAction.PARAM_FROM;
60 import static org.sonar.server.developers.ws.SearchEventsAction.PARAM_PROJECTS;
62 public class SearchEventsActionNewIssuesTest {
64 private static final RuleType[] RULE_TYPES_EXCEPT_HOTSPOT = Stream.of(RuleType.values())
65 .filter(r -> r != RuleType.SECURITY_HOTSPOT)
66 .toArray(RuleType[]::new);
69 public DbTester db = DbTester.create();
71 public EsTester es = EsTester.create();
73 public UserSessionRule userSession = UserSessionRule.standalone();
75 private Server server = mock(Server.class);
77 private IssueIndex issueIndex = new IssueIndex(es.client(), null, null, null);
78 private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()), null);
79 private IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class);
80 private WsActionTester ws = new WsActionTester(new SearchEventsAction(db.getDbClient(), userSession, server, issueIndex,
81 issueIndexSyncProgressChecker));
84 public void issue_event() {
85 userSession.logIn().setRoot();
86 when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
87 ComponentDto project = db.components().insertPrivateProject();
88 SnapshotDto analysis = insertAnalysis(project, 1_500_000_000_000L);
89 insertIssue(project, analysis);
90 insertIssue(project, analysis);
92 insertSecurityHotspot(project, analysis);
93 issueIndexer.indexAllIssues();
95 long from = analysis.getCreatedAt() - 1_000_000L;
96 SearchEventsWsResponse result = ws.newRequest()
97 .setParam(PARAM_PROJECTS, project.getKey())
98 .setParam(PARAM_FROM, formatDateTime(from))
99 .executeProtobuf(SearchEventsWsResponse.class);
101 assertThat(result.getEventsList())
102 .extracting(Event::getCategory, Event::getProject, Event::getMessage, Event::getLink, Event::getDate)
104 tuple("NEW_ISSUES", project.getKey(), format("You have 2 new issues on project '%s'", project.name()),
105 format("https://sonarcloud.io/project/issues?id=%s&createdAfter=%s&assignees=%s&resolved=false", project.getKey(), encode(formatDateTime(from + 1_000L)),
106 userSession.getLogin()),
107 formatDateTime(analysis.getCreatedAt())));
111 public void many_issues_events() {
112 userSession.logIn().setRoot();
113 long from = 1_500_000_000_000L;
114 ComponentDto project = db.components().insertPrivateProject(p -> p.setName("SonarQube"));
115 SnapshotDto analysis = insertAnalysis(project, from);
116 insertIssue(project, analysis);
117 insertIssue(project, analysis);
118 issueIndexer.indexAllIssues();
119 String fromDate = formatDateTime(from - 1_000L);
121 SearchEventsWsResponse result = ws.newRequest()
122 .setParam(PARAM_PROJECTS, project.getKey())
123 .setParam(PARAM_FROM, fromDate)
124 .executeProtobuf(SearchEventsWsResponse.class);
126 assertThat(result.getEventsList()).extracting(Event::getCategory, Event::getMessage, Event::getProject, Event::getDate)
127 .containsExactly(tuple("NEW_ISSUES", "You have 2 new issues on project 'SonarQube'", project.getKey(),
128 formatDateTime(from)));
132 public void does_not_return_old_issue() {
133 userSession.logIn().setRoot();
134 ComponentDto project = db.components().insertPrivateProject();
135 SnapshotDto analysis = insertAnalysis(project, 1_500_000_000_000L);
136 db.issues().insert(db.rules().insert(), project, project, i -> i.setIssueCreationDate(new Date(analysis.getCreatedAt() - 10_000L)));
137 issueIndexer.indexAllIssues();
139 SearchEventsWsResponse result = ws.newRequest()
140 .setParam(PARAM_PROJECTS, project.getKey())
141 .setParam(PARAM_FROM, formatDateTime(analysis.getCreatedAt() - 1_000L))
142 .executeProtobuf(SearchEventsWsResponse.class);
144 assertThat(result.getEventsList()).isEmpty();
148 public void return_link_to_issue_search_for_new_issues_event() {
149 userSession.logIn("my_login").setRoot();
150 ComponentDto project = db.components().insertPrivateProject(p -> p.setDbKey("my_project"));
151 SnapshotDto analysis = insertAnalysis(project, 1_400_000_000_000L);
152 insertIssue(project, analysis);
153 issueIndexer.indexAllIssues();
154 when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
156 SearchEventsWsResponse result = ws.newRequest()
157 .setParam(PARAM_PROJECTS, project.getKey())
158 .setParam(PARAM_FROM, formatDateTime(analysis.getCreatedAt() - 1_000L))
159 .executeProtobuf(SearchEventsWsResponse.class);
161 assertThat(result.getEventsList()).extracting(Event::getLink)
162 .containsExactly("https://sonarcloud.io/project/issues?id=my_project&createdAfter=" + encode(formatDateTime(analysis.getCreatedAt())) + "&assignees=my_login&resolved=false");
166 public void branch_issues_events() {
167 userSession.logIn().setRoot();
168 when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
169 ComponentDto project = db.components().insertPrivateProject();
170 ComponentDto branch1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH).setKey("branch1"));
171 SnapshotDto branch1Analysis = insertAnalysis(branch1, 1_500_000_000_000L);
172 insertIssue(branch1, branch1Analysis);
173 insertIssue(branch1, branch1Analysis);
174 ComponentDto branch2 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH).setKey("branch"));
175 SnapshotDto branch2Analysis = insertAnalysis(branch2, 1_300_000_000_000L);
176 insertIssue(branch2, branch2Analysis);
177 issueIndexer.indexAllIssues();
179 long from = 1_000_000_000_000L;
180 SearchEventsWsResponse result = ws.newRequest()
181 .setParam(PARAM_PROJECTS, project.getKey())
182 .setParam(PARAM_FROM, formatDateTime(from))
183 .executeProtobuf(SearchEventsWsResponse.class);
185 assertThat(result.getEventsList())
186 .extracting(Event::getCategory, Event::getProject, Event::getMessage, Event::getLink, Event::getDate)
188 tuple("NEW_ISSUES", project.getKey(), format("You have 2 new issues on project '%s' on branch '%s'", project.name(), branch1.getBranch()),
189 format("https://sonarcloud.io/project/issues?id=%s&createdAfter=%s&assignees=%s&resolved=false&branch=%s", branch1.getKey(), encode(formatDateTime(from + 1_000L)),
190 userSession.getLogin(), branch1.getBranch()),
191 formatDateTime(branch1Analysis.getCreatedAt())),
192 tuple("NEW_ISSUES", project.getKey(), format("You have 1 new issue on project '%s' on branch '%s'", project.name(), branch2.getBranch()),
193 format("https://sonarcloud.io/project/issues?id=%s&createdAfter=%s&assignees=%s&resolved=false&branch=%s", branch2.getKey(), encode(formatDateTime(from + 1_000L)),
194 userSession.getLogin(), branch2.getBranch()),
195 formatDateTime(branch2Analysis.getCreatedAt())));
199 public void pull_request_issues_events() {
200 userSession.logIn().setRoot();
201 when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
202 ComponentDto project = db.components().insertPrivateProject();
203 ComponentDto nonMainBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH).setKey("nonMain"));
204 SnapshotDto nonMainBranchAnalysis = insertAnalysis(nonMainBranch, 1_500_000_000_000L);
205 insertIssue(nonMainBranch, nonMainBranchAnalysis);
206 insertIssue(nonMainBranch, nonMainBranchAnalysis);
207 ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.PULL_REQUEST).setKey("42"));
208 SnapshotDto pullRequestAnalysis = insertAnalysis(pullRequest, 1_300_000_000_000L);
209 insertIssue(pullRequest, pullRequestAnalysis);
210 issueIndexer.indexAllIssues();
212 long from = 1_000_000_000_000L;
213 SearchEventsWsResponse result = ws.newRequest()
214 .setParam(PARAM_PROJECTS, project.getKey())
215 .setParam(PARAM_FROM, formatDateTime(from))
216 .executeProtobuf(SearchEventsWsResponse.class);
218 assertThat(result.getEventsList())
219 .extracting(Event::getCategory, Event::getProject, Event::getMessage, Event::getLink, Event::getDate)
221 tuple("NEW_ISSUES", project.getKey(), format("You have 2 new issues on project '%s' on branch '%s'", project.name(), nonMainBranch.getBranch()),
222 format("https://sonarcloud.io/project/issues?id=%s&createdAfter=%s&assignees=%s&resolved=false&branch=%s", nonMainBranch.getKey(), encode(formatDateTime(from + 1_000L)),
223 userSession.getLogin(), nonMainBranch.getBranch()),
224 formatDateTime(nonMainBranchAnalysis.getCreatedAt())),
225 tuple("NEW_ISSUES", project.getKey(), format("You have 1 new issue on project '%s' on pull request '%s'", project.name(), pullRequest.getPullRequest()),
226 format("https://sonarcloud.io/project/issues?id=%s&createdAfter=%s&assignees=%s&resolved=false&pullRequest=%s", pullRequest.getKey(),
227 encode(formatDateTime(from + 1_000L)),
228 userSession.getLogin(), pullRequest.getPullRequest()),
229 formatDateTime(pullRequestAnalysis.getCreatedAt())));
233 public void encode_link() {
234 userSession.logIn("rågnar").setRoot();
235 long from = 1_500_000_000_000L;
236 ComponentDto project = db.components().insertPrivateProject(p -> p.setDbKey("M&M's"));
237 SnapshotDto analysis = insertAnalysis(project, from);
238 insertIssue(project, analysis);
239 issueIndexer.indexAllIssues();
240 when(server.getPublicRootUrl()).thenReturn("http://sonarcloud.io");
242 String fromDate = formatDateTime(from - 1_000L);
243 SearchEventsWsResponse result = ws.newRequest()
244 .setParam(PARAM_PROJECTS, project.getKey())
245 .setParam(PARAM_FROM, fromDate)
246 .executeProtobuf(SearchEventsWsResponse.class);
248 assertThat(result.getEventsList()).extracting(Event::getLink)
249 .containsExactly("http://sonarcloud.io/project/issues?id=M%26M%27s&createdAfter=" + encode(formatDateTime(from)) + "&assignees=r%C3%A5gnar&resolved=false");
252 private String encode(String text) {
254 return URLEncoder.encode(text, UTF_8.name());
255 } catch (UnsupportedEncodingException e) {
256 throw new IllegalStateException(format("Cannot encode %s", text), e);
260 private void insertIssue(ComponentDto component, SnapshotDto analysis) {
261 RuleDefinitionDto rule = db.rules().insert(r -> r.setType(randomRuleTypeExceptHotspot()));
262 db.issues().insert(rule, component, component,
263 i -> i.setIssueCreationDate(new Date(analysis.getCreatedAt()))
264 .setAssigneeUuid(userSession.getUuid())
265 .setType(randomRuleTypeExceptHotspot()));
268 private void insertSecurityHotspot(ComponentDto component, SnapshotDto analysis) {
269 RuleDefinitionDto rule = db.rules().insert(r -> r.setType(RuleType.SECURITY_HOTSPOT));
270 db.issues().insert(rule, component, component,
271 i -> i.setIssueCreationDate(new Date(analysis.getCreatedAt()))
272 .setAssigneeUuid(userSession.getUuid())
273 .setType(RuleType.SECURITY_HOTSPOT));
276 private SnapshotDto insertAnalysis(ComponentDto project, long analysisDate) {
277 SnapshotDto analysis = db.components().insertSnapshot(project, s -> s.setCreatedAt(analysisDate));
278 insertActivity(project, analysis, CeActivityDto.Status.SUCCESS);
282 private CeActivityDto insertActivity(ComponentDto project, SnapshotDto analysis, CeActivityDto.Status status) {
283 CeQueueDto queueDto = new CeQueueDto();
284 queueDto.setTaskType(CeTaskTypes.REPORT);
285 String mainBranchProjectUuid = project.getMainBranchProjectUuid();
286 queueDto.setComponentUuid(mainBranchProjectUuid == null ? project.uuid() : mainBranchProjectUuid);
287 queueDto.setUuid(randomAlphanumeric(40));
288 queueDto.setCreatedAt(nextLong());
289 CeActivityDto activityDto = new CeActivityDto(queueDto);
290 activityDto.setStatus(status);
291 activityDto.setExecutionTimeMs(nextLong());
292 activityDto.setExecutedAt(nextLong());
293 activityDto.setAnalysisUuid(analysis.getUuid());
294 db.getDbClient().ceActivityDao().insert(db.getSession(), activityDto);
299 private RuleType randomRuleTypeExceptHotspot() {
300 return RULE_TYPES_EXCEPT_HOTSPOT[nextInt(RULE_TYPES_EXCEPT_HOTSPOT.length)];