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.util.Date;
23 import java.util.stream.IntStream;
24 import java.util.stream.Stream;
25 import org.junit.Rule;
26 import org.junit.Test;
27 import org.sonar.api.platform.Server;
28 import org.sonar.api.rules.RuleType;
29 import org.sonar.api.server.ws.WebService;
30 import org.sonar.api.server.ws.WebService.Param;
31 import org.sonar.api.web.UserRole;
32 import org.sonar.db.DbTester;
33 import org.sonar.db.ce.CeActivityDto;
34 import org.sonar.db.ce.CeQueueDto;
35 import org.sonar.db.ce.CeTaskTypes;
36 import org.sonar.db.component.ComponentDto;
37 import org.sonar.db.component.SnapshotDto;
38 import org.sonar.db.event.EventDto;
39 import org.sonar.server.es.EsTester;
40 import org.sonar.server.exceptions.UnauthorizedException;
41 import org.sonar.server.issue.index.IssueIndex;
42 import org.sonar.server.issue.index.IssueIndexSyncProgressChecker;
43 import org.sonar.server.issue.index.IssueIndexer;
44 import org.sonar.server.issue.index.IssueIteratorFactory;
45 import org.sonar.server.projectanalysis.ws.EventCategory;
46 import org.sonar.server.tester.UserSessionRule;
47 import org.sonar.server.ws.KeyExamples;
48 import org.sonar.server.ws.WsActionTester;
49 import org.sonarqube.ws.Developers.SearchEventsWsResponse;
50 import org.sonarqube.ws.Developers.SearchEventsWsResponse.Event;
52 import static java.lang.String.format;
53 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
54 import static org.apache.commons.lang.math.RandomUtils.nextInt;
55 import static org.apache.commons.lang.math.RandomUtils.nextLong;
56 import static org.assertj.core.api.Assertions.assertThat;
57 import static org.assertj.core.api.Assertions.assertThatThrownBy;
58 import static org.assertj.core.api.Assertions.tuple;
59 import static org.mockito.ArgumentMatchers.any;
60 import static org.mockito.ArgumentMatchers.argThat;
61 import static org.mockito.Mockito.mock;
62 import static org.mockito.Mockito.verify;
63 import static org.mockito.Mockito.when;
64 import static org.sonar.api.utils.DateUtils.formatDateTime;
65 import static org.sonar.api.web.UserRole.USER;
66 import static org.sonar.db.event.EventTesting.newEvent;
67 import static org.sonar.server.developers.ws.SearchEventsAction.PARAM_FROM;
68 import static org.sonar.server.developers.ws.SearchEventsAction.PARAM_PROJECTS;
69 import static org.sonar.test.JsonAssert.assertJson;
71 public class SearchEventsActionTest {
73 private static final RuleType[] RULE_TYPES_EXCEPT_HOTSPOT = Stream.of(RuleType.values())
74 .filter(r -> r != RuleType.SECURITY_HOTSPOT)
75 .toArray(RuleType[]::new);
78 public DbTester db = DbTester.create();
80 public EsTester es = EsTester.create();
82 public UserSessionRule userSession = UserSessionRule.standalone().logIn();
83 private Server server = mock(Server.class);
84 private IssueIndex issueIndex = new IssueIndex(es.client(), null, null, null);
85 private IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class);
86 private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()), null);
87 private WsActionTester ws = new WsActionTester(new SearchEventsAction(db.getDbClient(), userSession, server, issueIndex,
88 issueIndexSyncProgressChecker));
91 public void definition() {
92 WebService.Action definition = ws.getDef();
94 assertThat(definition.key()).isEqualTo("search_events");
95 assertThat(definition.description()).isNotEmpty();
96 assertThat(definition.isPost()).isFalse();
97 assertThat(definition.isInternal()).isTrue();
98 assertThat(definition.since()).isEqualTo("1.0");
99 assertThat(definition.description()).isNotEmpty();
100 assertThat(definition.responseExampleAsString()).isNotEmpty();
101 assertThat(definition.params()).extracting(Param::key).containsOnly("projects", "from");
102 Param projects = definition.param("projects");
103 assertThat(projects.isRequired()).isTrue();
104 assertThat(projects.exampleValue()).isEqualTo("my_project,another_project");
105 assertThat(definition.param("from").isRequired()).isTrue();
109 public void json_example() {
110 ComponentDto project = db.components().insertPrivateProject(p -> p.setName("My Project").setKey(KeyExamples.KEY_PROJECT_EXAMPLE_001));
111 userSession.addProjectPermission(USER, project);
112 SnapshotDto analysis = insertAnalysis(project, 1_500_000_000_000L);
113 EventDto e1 = db.events().insertEvent(newQualityGateEvent(analysis).setName("Failed").setDate(analysis.getCreatedAt()));
114 IntStream.range(0, 15).forEach(x -> insertIssue(project, analysis));
115 issueIndexer.indexAllIssues();
116 when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
118 String result = ws.newRequest()
119 .setParam(PARAM_PROJECTS, project.getKey())
120 .setParam(PARAM_FROM, formatDateTime(analysis.getCreatedAt() - 1_000L))
121 .execute().getInput();
123 assertJson(result).ignoreFields("date", "link").isSimilarTo(ws.getDef().responseExampleAsString());
127 public void events() {
128 when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
129 ComponentDto project = db.components().insertPrivateProject();
130 userSession.addProjectPermission(USER, project);
131 ComponentDto branch = db.components().insertProjectBranch(project);
132 SnapshotDto projectAnalysis = insertAnalysis(project, 1_500_000_000_000L);
133 db.events().insertEvent(newQualityGateEvent(projectAnalysis).setDate(projectAnalysis.getCreatedAt()).setName("Passed"));
134 insertIssue(project, projectAnalysis);
135 insertIssue(project, projectAnalysis);
136 SnapshotDto branchAnalysis = insertAnalysis(branch, 1_501_000_000_000L);
137 db.events().insertEvent(newQualityGateEvent(branchAnalysis).setDate(branchAnalysis.getCreatedAt()).setName("Failed"));
138 insertIssue(branch, branchAnalysis);
139 issueIndexer.indexAllIssues();
141 SearchEventsWsResponse result = ws.newRequest()
142 .setParam(PARAM_PROJECTS, project.getKey())
143 .setParam(PARAM_FROM, formatDateTime(1_499_000_000_000L))
144 .executeProtobuf(SearchEventsWsResponse.class);
146 assertThat(result.getEventsList())
147 .extracting(Event::getCategory, Event::getProject, Event::getMessage)
149 tuple("QUALITY_GATE", project.getKey(), format("Quality Gate status of project '%s' changed to 'Passed'", project.name())),
150 tuple("QUALITY_GATE", project.getKey(), format("Quality Gate status of project '%s' on branch '%s' changed to 'Failed'", project.name(), branch.getBranch())),
151 tuple("NEW_ISSUES", project.getKey(), format("You have 2 new issues on project '%s'", project.name())),
152 tuple("NEW_ISSUES", project.getKey(), format("You have 1 new issue on project '%s' on branch '%s'", project.name(), branch.getBranch())));
153 verify(issueIndexSyncProgressChecker).checkIfAnyComponentsNeedIssueSync(any(), argThat(arg -> arg.contains(project.getKey())));
157 public void does_not_return_old_events() {
158 ComponentDto project = db.components().insertPrivateProject();
159 userSession.addProjectPermission(USER, project);
160 SnapshotDto analysis = insertAnalysis(project, 1_500_000_000_000L);
161 insertIssue(project, analysis);
162 db.events().insertEvent(newQualityGateEvent(analysis).setDate(analysis.getCreatedAt()).setName("Passed"));
163 SnapshotDto oldAnalysis = insertAnalysis(project, 1_400_000_000_000L);
164 insertIssue(project, oldAnalysis);
165 db.events().insertEvent(newQualityGateEvent(oldAnalysis).setDate(oldAnalysis.getCreatedAt()).setName("Failed"));
166 issueIndexer.indexAllIssues();
168 SearchEventsWsResponse result = ws.newRequest()
169 .setParam(PARAM_PROJECTS, project.getKey())
170 .setParam(PARAM_FROM, formatDateTime(analysis.getCreatedAt() - 1450_000_000_000L))
171 .executeProtobuf(SearchEventsWsResponse.class);
173 assertThat(result.getEventsList())
174 .extracting(Event::getCategory, Event::getDate)
176 tuple("NEW_ISSUES", formatDateTime(analysis.getCreatedAt())),
177 tuple("QUALITY_GATE", formatDateTime(analysis.getCreatedAt())));
181 public void empty_response_for_empty_list_of_projects() {
182 SearchEventsWsResponse result = ws.newRequest()
183 .setParam(PARAM_PROJECTS, "")
184 .setParam(PARAM_FROM, "")
185 .executeProtobuf(SearchEventsWsResponse.class);
187 assertThat(result.getEventsList()).isEmpty();
191 public void does_not_return_events_of_project_for_which_the_current_user_has_no_browse_permission() {
192 ComponentDto project1 = db.components().insertPrivateProject();
193 userSession.addProjectPermission(UserRole.CODEVIEWER, project1);
194 userSession.addProjectPermission(UserRole.ISSUE_ADMIN, project1);
196 ComponentDto project2 = db.components().insertPrivateProject();
197 userSession.addProjectPermission(USER, project2);
199 SnapshotDto a1 = insertAnalysis(project1, 1_500_000_000_000L);
200 EventDto e1 = db.events().insertEvent(newQualityGateEvent(a1).setDate(a1.getCreatedAt()));
201 insertIssue(project1, a1);
202 SnapshotDto a2 = insertAnalysis(project2, 1_500_000_000_000L);
203 EventDto e2 = db.events().insertEvent(newQualityGateEvent(a2).setDate(a2.getCreatedAt()));
204 insertIssue(project2, a2);
205 issueIndexer.indexAllIssues();
207 String stringFrom = formatDateTime(a1.getCreatedAt() - 1_000L);
208 SearchEventsWsResponse result = ws.newRequest()
209 .setParam(PARAM_PROJECTS, String.join(",", project1.getKey(), project2.getKey()))
210 .setParam(PARAM_FROM, String.join(",", stringFrom, stringFrom))
211 .executeProtobuf(SearchEventsWsResponse.class);
213 assertThat(result.getEventsList())
214 .extracting(Event::getCategory, Event::getProject)
216 tuple("NEW_ISSUES", project2.getKey()),
217 tuple(EventCategory.QUALITY_GATE.name(), project2.getKey()));
221 public void empty_response_if_project_key_is_unknown() {
222 long from = 1_500_000_000_000L;
223 SearchEventsWsResponse result = ws.newRequest()
224 .setParam(PARAM_PROJECTS, "unknown")
225 .setParam(PARAM_FROM, formatDateTime(from - 1_000L))
226 .executeProtobuf(SearchEventsWsResponse.class);
228 assertThat(result.getEventsList()).isEmpty();
232 public void fail_when_not_loggued() {
233 userSession.anonymous();
234 ComponentDto project = db.components().insertPrivateProject();
236 assertThatThrownBy(() -> {
238 .setParam(PARAM_PROJECTS, project.getKey())
239 .setParam(PARAM_FROM, formatDateTime(1_000L))
242 .isInstanceOf(UnauthorizedException.class);
246 public void fail_if_date_format_is_not_valid() {
247 assertThatThrownBy(() -> {
249 .setParam(PARAM_PROJECTS, "foo")
250 .setParam(PARAM_FROM, "wat")
251 .executeProtobuf(SearchEventsWsResponse.class);
253 .isInstanceOf(IllegalArgumentException.class)
254 .hasMessage("'wat' cannot be parsed as either a date or date+time");
257 private static EventDto newQualityGateEvent(SnapshotDto analysis) {
258 return newEvent(analysis).setCategory(EventCategory.QUALITY_GATE.getLabel());
261 private CeActivityDto insertActivity(ComponentDto project, SnapshotDto analysis, CeActivityDto.Status status) {
262 CeQueueDto queueDto = new CeQueueDto();
263 queueDto.setTaskType(CeTaskTypes.REPORT);
264 String mainBranchProjectUuid = project.getMainBranchProjectUuid();
265 queueDto.setComponentUuid(mainBranchProjectUuid == null ? project.uuid() : mainBranchProjectUuid);
266 queueDto.setUuid(randomAlphanumeric(40));
267 queueDto.setCreatedAt(nextLong());
268 CeActivityDto activityDto = new CeActivityDto(queueDto);
269 activityDto.setStatus(status);
270 activityDto.setExecutionTimeMs(nextLong());
271 activityDto.setExecutedAt(nextLong());
272 activityDto.setAnalysisUuid(analysis.getUuid());
273 db.getDbClient().ceActivityDao().insert(db.getSession(), activityDto);
278 private void insertIssue(ComponentDto component, SnapshotDto analysis) {
279 db.issues().insert(db.rules().insert(), component, component,
280 i -> i.setIssueCreationDate(new Date(analysis.getCreatedAt()))
281 .setAssigneeUuid(userSession.getUuid())
282 .setType(randomRuleTypeExceptHotspot()));
285 private SnapshotDto insertAnalysis(ComponentDto project, long analysisDate) {
286 SnapshotDto analysis = db.components().insertSnapshot(project, s -> s.setCreatedAt(analysisDate));
287 insertActivity(project, analysis, CeActivityDto.Status.SUCCESS);
291 private RuleType randomRuleTypeExceptHotspot() {
292 return RULE_TYPES_EXCEPT_HOTSPOT[nextInt(RULE_TYPES_EXCEPT_HOTSPOT.length)];