diff options
author | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2016-12-12 17:16:05 +0100 |
---|---|---|
committer | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2016-12-13 14:48:31 +0100 |
commit | a01c99bb547c5f9318eb536f03e473e8cb6f1126 (patch) | |
tree | b830158f84979d9ba83b8bd43617326949be9775 /server | |
parent | fb9f4f8314cc44d45aef2f41a82520d969080444 (diff) | |
download | sonarqube-a01c99bb547c5f9318eb536f03e473e8cb6f1126.tar.gz sonarqube-a01c99bb547c5f9318eb536f03e473e8cb6f1126.zip |
SONAR-8467 Create WS api/project_analyses/search
Diffstat (limited to 'server')
7 files changed, 691 insertions, 2 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java index e4c43303e22..08de37424ed 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java @@ -24,6 +24,7 @@ import org.sonar.core.platform.Module; import org.sonar.server.projectanalysis.ws.CreateEventAction; import org.sonar.server.projectanalysis.ws.DeleteEventAction; import org.sonar.server.projectanalysis.ws.ProjectAnalysesWs; +import org.sonar.server.projectanalysis.ws.SearchAction; import org.sonar.server.projectanalysis.ws.UpdateEventAction; public class ProjectAnalysisModule extends Module { @@ -35,7 +36,8 @@ public class ProjectAnalysisModule extends Module { // actions CreateEventAction.class, UpdateEventAction.class, - DeleteEventAction.class); + DeleteEventAction.class, + SearchAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SearchAction.java new file mode 100644 index 00000000000..39235dd8e56 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SearchAction.java @@ -0,0 +1,154 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.projectanalysis.ws; + +import java.util.EnumSet; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Scopes; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.server.ws.WebService.Param; +import org.sonar.api.web.UserRole; +import org.sonar.core.util.stream.Collectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.component.SnapshotQuery; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; +import org.sonar.server.ws.KeyExamples; +import org.sonarqube.ws.ProjectAnalyses; +import org.sonarqube.ws.client.projectanalysis.EventCategory; +import org.sonarqube.ws.client.projectanalysis.SearchRequest; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.sonar.core.util.stream.Collectors.toOneElement; +import static org.sonar.db.component.SnapshotQuery.SORT_FIELD.BY_DATE; +import static org.sonar.db.component.SnapshotQuery.SORT_ORDER.DESC; +import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.client.projectanalysis.EventCategory.OTHER; +import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_CATEGORY; +import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_PROJECT; +import static org.sonarqube.ws.client.projectanalysis.SearchRequest.DEFAULT_PAGE_SIZE; + +public class SearchAction implements ProjectAnalysesWsAction { + private final DbClient dbClient; + private final ComponentFinder componentFinder; + private final UserSession userSession; + + public SearchAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + this.dbClient = dbClient; + this.componentFinder = componentFinder; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("search") + .setDescription("Search a project analyses and attached events.<br>" + + "Requires the following permission: 'Browse' on the specified project") + .setSince("6.3") + .setResponseExample(getClass().getResource("search-example.json")) + .setHandler(this); + + action.addPagingParams(DEFAULT_PAGE_SIZE, 500); + + action.createParam(PARAM_PROJECT) + .setDescription("Project key") + .setRequired(true) + .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001); + + action.createParam(PARAM_CATEGORY) + .setDescription("Event category. Filter analyses that have at least one event of the category specified.") + .setPossibleValues(EnumSet.allOf(EventCategory.class)) + .setExampleValue(OTHER.name()); + } + + @Override + public void handle(Request request, Response response) throws Exception { + ProjectAnalyses.SearchResponse searchResponse = Stream.of(request) + .map(toWsRequest()) + .map(this::search) + .map(new SearchResponseBuilder().buildWsResponse()) + .collect(toOneElement()); + writeProtobuf(searchResponse, request, response); + } + + private static Function<Request, SearchRequest> toWsRequest() { + return request -> { + String category = request.param(PARAM_CATEGORY); + return SearchRequest.builder() + .setProject(request.mandatoryParam(PARAM_PROJECT)) + .setCategory(category == null ? null : EventCategory.valueOf(category)) + .setPage(request.mandatoryParamAsInt(Param.PAGE)) + .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE)) + .build(); + }; + } + + private SearchResults search(SearchRequest request) { + try (DbSession dbSession = dbClient.openSession(false)) { + return Stream.of(SearchResults.builder(dbSession, request)) + .peek(addProject()) + .peek(checkPermission()) + .peek(addAnalyses()) + .peek(addEvents()) + .map(SearchResults.Builder::build) + .collect(toOneElement()); + } + } + + private Consumer<SearchResults.Builder> addAnalyses() { + return data -> { + SnapshotQuery dbQuery = new SnapshotQuery() + .setComponentUuid(data.getProjectUuid()) + .setStatus(SnapshotDto.STATUS_PROCESSED) + .setSort(BY_DATE, DESC); + data.setAnalyses(dbClient.snapshotDao().selectAnalysesByQuery(data.getDbSession(), dbQuery)); + }; + } + + private Consumer<SearchResults.Builder> addEvents() { + return data -> { + List<String> analyses = data.getAnalyses().stream().map(SnapshotDto::getUuid).collect(Collectors.toList()); + data.setEvents(dbClient.eventDao().selectByAnalysisUuids(data.getDbSession(), analyses)); + }; + } + + private Consumer<SearchResults.Builder> checkPermission() { + return data -> userSession.checkComponentUuidPermission(UserRole.USER, data.getProjectUuid()); + } + + private Consumer<SearchResults.Builder> addProject() { + return data -> { + ComponentDto project = componentFinder.getByKey(data.getDbSession(), data.getRequest().getProject()); + checkArgument(Scopes.PROJECT.equals(project.scope()) && Qualifiers.PROJECT.equals(project.qualifier()), "A project is required"); + data.setProjectUuid(project.uuid()); + }; + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SearchResponseBuilder.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SearchResponseBuilder.java new file mode 100644 index 00000000000..0dcd59deb19 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SearchResponseBuilder.java @@ -0,0 +1,92 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.projectanalysis.ws; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import org.sonar.core.util.stream.Collectors; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.event.EventDto; +import org.sonarqube.ws.ProjectAnalyses.Analysis; +import org.sonarqube.ws.ProjectAnalyses.Event; +import org.sonarqube.ws.ProjectAnalyses.SearchResponse; + +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonarqube.ws.client.projectanalysis.EventCategory.fromLabel; + +class SearchResponseBuilder { + private final Analysis.Builder analysisBuilder; + private final Event.Builder eventBuilder; + + SearchResponseBuilder() { + this.analysisBuilder = Analysis.newBuilder(); + this.eventBuilder = Event.newBuilder(); + } + + Function<SearchResults, SearchResponse> buildWsResponse() { + return searchResults -> Stream.of(SearchResponse.newBuilder()) + .peek(addAnalyses(searchResults)) + .peek(addPagination(searchResults)) + .map(SearchResponse.Builder::build) + .collect(Collectors.toOneElement()); + } + + private Consumer<SearchResponse.Builder> addAnalyses(SearchResults searchResults) { + return response -> searchResults.analyses.stream() + .map(dbToWsAnalysis()) + .peek(addEvents(searchResults)) + .forEach(response::addAnalyses); + } + + private Function<SnapshotDto, Analysis.Builder> dbToWsAnalysis() { + return dbAnalysis -> analysisBuilder.clear() + .setKey(dbAnalysis.getUuid()) + .setDate(formatDateTime(dbAnalysis.getCreatedAt())); + } + + private Consumer<Analysis.Builder> addEvents(SearchResults searchResults) { + return wsAnalysis -> searchResults.eventsByAnalysis.get(wsAnalysis.getKey()).stream() + .map(dbToWsEvent()) + .forEach(wsAnalysis::addEvents); + } + + private Function<EventDto, Event.Builder> dbToWsEvent() { + return dbEvent -> { + Event.Builder wsEvent = eventBuilder.clear() + .setKey(dbEvent.getUuid()); + setNullable(dbEvent.getName(), wsEvent::setName); + setNullable(dbEvent.getDescription(), wsEvent::setDescription); + setNullable(dbEvent.getCategory(), cat -> wsEvent.setCategory(fromLabel(cat).name())); + return wsEvent; + }; + } + + private static Consumer<SearchResponse.Builder> addPagination(SearchResults searchResults) { + return response -> response.getPagingBuilder() + .setPageIndex(searchResults.paging.pageIndex()) + .setPageSize(searchResults.paging.pageSize()) + .setTotal(searchResults.paging.total()) + .build(); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SearchResults.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SearchResults.java new file mode 100644 index 00000000000..b70f023a775 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SearchResults.java @@ -0,0 +1,129 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.projectanalysis.ws; + +import com.google.common.collect.ListMultimap; +import java.util.List; +import java.util.stream.Stream; +import org.sonar.api.utils.Paging; +import org.sonar.core.util.stream.Collectors; +import org.sonar.db.DbSession; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.event.EventDto; +import org.sonarqube.ws.client.projectanalysis.SearchRequest; + +import static java.util.Objects.requireNonNull; + +class SearchResults { + final List<SnapshotDto> analyses; + final ListMultimap<String, EventDto> eventsByAnalysis; + final Paging paging; + + private SearchResults(Builder builder) { + this.analyses = builder.analyses; + this.eventsByAnalysis = buildEvents(builder.events); + this.paging = Paging + .forPageIndex(builder.getRequest().getPage()) + .withPageSize(builder.getRequest().getPageSize()) + .andTotal(builder.countAnalyses); + } + + private ListMultimap<String, EventDto> buildEvents(List<EventDto> events) { + return events.stream().collect(Collectors.index(EventDto::getAnalysisUuid)); + } + + static Builder builder(DbSession dbSession, SearchRequest request) { + return new Builder(dbSession, request); + } + + static class Builder { + private final DbSession dbSession; + private final SearchRequest request; + private String projectUuid; + private List<SnapshotDto> analyses; + private int countAnalyses; + private List<EventDto> events; + + private Builder(DbSession dbSession, SearchRequest request) { + this.dbSession = dbSession; + this.request = request; + } + + Builder setProjectUuid(String projectUuid) { + this.projectUuid = projectUuid; + return this; + } + + Builder setAnalyses(List<SnapshotDto> analyses) { + Stream<SnapshotDto> stream = analyses.stream(); + // no filter by category, the pagination can be applied + if (request.getCategory() == null) { + stream = stream + .skip(Paging.offset(request.getPage(), request.getPageSize())) + .limit(request.getPageSize()); + } + + this.analyses = stream.collect(Collectors.toList()); + this.countAnalyses = analyses.size(); + return this; + } + + Builder setEvents(List<EventDto> events) { + this.events = events; + return this; + } + + DbSession getDbSession() { + return dbSession; + } + + SearchRequest getRequest() { + return request; + } + + String getProjectUuid() { + return projectUuid; + } + + List<SnapshotDto> getAnalyses() { + return analyses; + } + + private void filterByCategory() { + ListMultimap<String, String> eventCategoriesByAnalysisUuid = events.stream() + .collect(Collectors.index(EventDto::getAnalysisUuid, EventDto::getCategory)); + this.analyses = analyses.stream() + .filter(a -> eventCategoriesByAnalysisUuid.get(a.getUuid()).contains(request.getCategory().getLabel())) + .skip(Paging.offset(request.getPage(), request.getPageSize())) + .limit(request.getPageSize()) + .collect(Collectors.toList()); + } + + SearchResults build() { + requireNonNull(analyses); + requireNonNull(events); + if (request.getCategory() != null) { + filterByCategory(); + } + return new SearchResults(this); + } + } +} diff --git a/server/sonar-server/src/main/resources/org/sonar/server/projectanalysis/ws/search-example.json b/server/sonar-server/src/main/resources/org/sonar/server/projectanalysis/ws/search-example.json new file mode 100644 index 00000000000..7d602081749 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/projectanalysis/ws/search-example.json @@ -0,0 +1,42 @@ +{ + "paging": { + "pageIndex": 1, + "pageSize": 100, + "total": 2 + }, + "analyses": [ + { + "key": "A2", + "date": "2016-12-12T17:12:45+0100", + "events": [ + { + "key": "E21", + "category": "QUALITY_PROFILE", + "name": "Quality Profile changed to Sonar Way", + }, + { + "key": "E22", + "category": "OTHER", + "name": "6.3", + } + ] + }, + { + "key": "A1", + "date": "2016-12-11T17:12:45+0100", + "events": [ + { + "key": "E11", + "category": "QUALITY_GATE", + "name": "Quality Gate is Red (was Orange)", + "description": "Coverage is \u003c 80%" + }, + { + "key": "E12", + "category": "VERSION", + "name": "6.3", + } + ] + } + ] +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java index bd1018524f6..b7841ce9ec2 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java @@ -31,6 +31,6 @@ public class ProjectAnalysisModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new ProjectAnalysisModule().configure(container); - assertThat(container.size()).isEqualTo(2 + 4); + assertThat(container.size()).isEqualTo(2 + 5); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/SearchActionTest.java new file mode 100644 index 00000000000..35d641e2ef0 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/SearchActionTest.java @@ -0,0 +1,270 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.projectanalysis.ws; + +import com.google.common.base.Throwables; +import java.io.IOException; +import java.util.List; +import java.util.function.Function; +import java.util.stream.IntStream; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.server.ws.WebService.Param; +import org.sonar.api.web.UserRole; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.event.EventDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.ProjectAnalyses.Analysis; +import org.sonarqube.ws.ProjectAnalyses.Event; +import org.sonarqube.ws.ProjectAnalyses.SearchResponse; +import org.sonarqube.ws.client.projectanalysis.EventCategory; +import org.sonarqube.ws.client.projectanalysis.SearchRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.sonar.api.utils.DateUtils.parseDateTime; +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.db.component.SnapshotTesting.newAnalysis; +import static org.sonar.db.event.EventTesting.newEvent; +import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; +import static org.sonar.test.JsonAssert.assertJson; +import static org.sonarqube.ws.MediaTypes.PROTOBUF; +import static org.sonarqube.ws.client.WsRequest.Method.POST; +import static org.sonarqube.ws.client.projectanalysis.EventCategory.OTHER; +import static org.sonarqube.ws.client.projectanalysis.EventCategory.QUALITY_GATE; +import static org.sonarqube.ws.client.projectanalysis.EventCategory.VERSION; +import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_CATEGORY; +import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_PROJECT; + +public class SearchActionTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone().setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + @Rule + public DbTester db = DbTester.create(); + private DbClient dbClient = db.getDbClient(); + + private WsActionTester ws = new WsActionTester(new SearchAction(dbClient, new ComponentFinder(dbClient), userSession)); + + @Test + public void json_example() { + ComponentDto project = db.components().insertComponent(newProjectDto().setKey(KEY_PROJECT_EXAMPLE_001)); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + SnapshotDto a1 = db.components().insertSnapshot(newAnalysis(project).setUuid("A1").setCreatedAt(parseDateTime("2016-12-11T17:12:45+0100").getTime())); + SnapshotDto a2 = db.components().insertSnapshot(newAnalysis(project).setUuid("A2").setCreatedAt(parseDateTime("2016-12-12T17:12:45+0100").getTime())); + db.events().insertEvent(newEvent(a1).setUuid("E11") + .setName("Quality Gate is Red (was Orange)") + .setCategory(EventCategory.QUALITY_GATE.getLabel()) + .setDescription("Coverage is < 80%")); + db.events().insertEvent(newEvent(a1).setUuid("E12") + .setName("6.3").setCategory(VERSION.getLabel())); + db.events().insertEvent(newEvent(a2).setUuid("E21") + .setName("Quality Profile changed to Sonar Way") + .setCategory(EventCategory.QUALITY_PROFILE.getLabel())); + db.events().insertEvent(newEvent(a2).setUuid("E22") + .setName("6.3").setCategory(OTHER.getLabel())); + + String result = ws.newRequest() + .setParam(PARAM_PROJECT, KEY_PROJECT_EXAMPLE_001) + .execute().getInput(); + + assertJson(result).isSimilarTo(getClass().getResource("search-example.json")); + } + + @Test + public void return_analyses_ordered_by_analysis_date() { + ComponentDto project = db.components().insertComponent(newProjectDto().setKey("P1")); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + db.components().insertSnapshot(newAnalysis(project).setUuid("A1").setCreatedAt(1_000_000L)); + db.components().insertSnapshot(newAnalysis(project).setUuid("A2").setCreatedAt(2_000_000L)); + db.components().insertSnapshot(newAnalysis(project).setUuid("A3").setCreatedAt(3_000_000L)); + + List<Analysis> result = call("P1").getAnalysesList(); + + assertThat(result).hasSize(3); + assertThat(result).extracting(Analysis::getKey, a -> parseDateTime(a.getDate()).getTime()).containsExactly( + tuple("A3", 3_000_000L), + tuple("A2", 2_000_000L), + tuple("A1", 1_000_000L)); + } + + @Test + public void return_only_processed_analyses() { + ComponentDto project = db.components().insertComponent(newProjectDto().setKey("P1")); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + db.components().insertSnapshot(newAnalysis(project).setUuid("A1")); + db.components().insertSnapshot(newAnalysis(project).setUuid("A2").setStatus(SnapshotDto.STATUS_UNPROCESSED)); + + List<Analysis> result = call("P1").getAnalysesList(); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getKey()).isEqualTo("A1"); + } + + @Test + public void return_events() { + ComponentDto project = db.components().insertComponent(newProjectDto().setKey("P1")); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + SnapshotDto a1 = db.components().insertSnapshot(newAnalysis(project).setUuid("A1")); + SnapshotDto a42 = db.components().insertSnapshot(newAnalysis(newProjectDto()).setUuid("A42")); + EventDto e1 = db.events().insertEvent(newEvent(a1).setUuid("E1").setName("N1").setCategory(EventCategory.QUALITY_GATE.getLabel()).setDescription("D1")); + EventDto e2 = db.events().insertEvent(newEvent(a1).setUuid("E2").setName("N2").setCategory(VERSION.getLabel()).setDescription("D2")); + db.events().insertEvent(newEvent(a42)); + + List<Analysis> result = call("P1").getAnalysesList(); + + assertThat(result).hasSize(1); + List<Event> events = result.get(0).getEventsList(); + assertThat(events).hasSize(2); + assertThat(events).extracting(Event::getKey, wsToDbCategory(), Event::getName, Event::getDescription).containsOnly( + tuple(e1.getUuid(), e1.getCategory(), e1.getName(), e1.getDescription()), + tuple(e2.getUuid(), e2.getCategory(), e2.getName(), e2.getDescription())); + } + + @Test + public void paginate_analyses() { + ComponentDto project = db.components().insertProject(); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + IntStream.rangeClosed(1, 9).forEach(i -> db.components().insertSnapshot(newAnalysis(project).setCreatedAt(1_000_000L * i).setUuid("A" + i))); + + SearchResponse result = call(SearchRequest.builder() + .setProject(project.key()) + .setPage(2) + .setPageSize(3) + .build()); + + assertThat(result.getAnalysesList()).extracting(Analysis::getKey) + .containsExactly("A6", "A5", "A4"); + } + + @Test + public void filter_by_category() { + ComponentDto project = db.components().insertComponent(newProjectDto().setKey("P1")); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + SnapshotDto a1 = db.components().insertSnapshot(newAnalysis(project).setUuid("A1")); + SnapshotDto a2 = db.components().insertSnapshot(newAnalysis(project).setUuid("A2")); + SnapshotDto a42 = db.components().insertSnapshot(newAnalysis(project).setUuid("A42")); + db.events().insertEvent(newEvent(a1).setUuid("E11").setCategory(VERSION.getLabel())); + db.events().insertEvent(newEvent(a1).setUuid("E12").setCategory(QUALITY_GATE.getLabel())); + db.events().insertEvent(newEvent(a2).setUuid("E21").setCategory(QUALITY_GATE.getLabel())); + // Analysis A42 doesn't have a quality gate event + db.events().insertEvent(newEvent(a42).setCategory(OTHER.getLabel())); + + List<Analysis> result = call(SearchRequest.builder() + .setProject("P1") + .setCategory(QUALITY_GATE) + .build()).getAnalysesList(); + + assertThat(result).extracting(Analysis::getKey).containsOnly("A1", "A2"); + } + + @Test + public void paginate_with_filter_on_category() { + ComponentDto project = db.components().insertComponent(newProjectDto().setKey("P1")); + userSession.addProjectUuidPermissions(UserRole.USER, project.uuid()); + SnapshotDto a1 = db.components().insertSnapshot(newAnalysis(project).setUuid("A1").setCreatedAt(1_000_000L)); + SnapshotDto a2 = db.components().insertSnapshot(newAnalysis(project).setUuid("A2").setCreatedAt(2_000_000L)); + SnapshotDto a42 = db.components().insertSnapshot(newAnalysis(project).setUuid("A42")); + db.events().insertEvent(newEvent(a1).setUuid("E11").setCategory(VERSION.getLabel())); + db.events().insertEvent(newEvent(a1).setUuid("E12").setCategory(QUALITY_GATE.getLabel())); + db.events().insertEvent(newEvent(a2).setUuid("E21").setCategory(QUALITY_GATE.getLabel())); + // Analysis A42 doesn't have a quality gate event + db.events().insertEvent(newEvent(a42).setCategory(OTHER.getLabel())); + + List<Analysis> result = call(SearchRequest.builder() + .setProject("P1") + .setCategory(QUALITY_GATE) + .setPage(1) + .setPageSize(1) + .build()).getAnalysesList(); + + assertThat(result).extracting(Analysis::getKey).containsOnly("A2"); + } + + @Test + public void definition() { + WebService.Action definition = ws.getDef(); + + assertThat(definition.key()).isEqualTo("search"); + assertThat(definition.since()).isEqualTo("6.3"); + assertThat(definition.responseExampleAsString()).isNotEmpty(); + assertThat(definition.param(PARAM_PROJECT).isRequired()).isTrue(); + assertThat(definition.param(PARAM_CATEGORY)).isNotNull(); + } + + @Test + public void fail_if_not_enough_permissions() { + userSession.anonymous(); + ComponentDto project = db.components().insertProject(); + + expectedException.expect(ForbiddenException.class); + + call(project.key()); + } + + @Test + public void fail_if_project_does_not_exist() { + expectedException.expect(NotFoundException.class); + + call("P1"); + } + + private static Function<Event, String> wsToDbCategory() { + return e -> e == null ? null : EventCategory.valueOf(e.getCategory()).getLabel(); + } + + private SearchResponse call(@Nullable String project) { + SearchRequest.Builder request = SearchRequest.builder(); + setNullable(project, request::setProject); + return call(request.build()); + } + + private SearchResponse call(SearchRequest wsRequest) { + TestRequest request = ws.newRequest() + .setMediaType(PROTOBUF) + .setMethod(POST.name()); + setNullable(wsRequest.getProject(), project -> request.setParam(PARAM_PROJECT, project)); + setNullable(wsRequest.getCategory(), category -> request.setParam(PARAM_CATEGORY, category.name())); + setNullable(wsRequest.getPage(), page -> request.setParam(Param.PAGE, String.valueOf(page))); + setNullable(wsRequest.getPageSize(), pageSize -> request.setParam(Param.PAGE_SIZE, String.valueOf(pageSize))); + + try { + return SearchResponse.parseFrom(request.execute().getInputStream()); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } +} |