From 5686fcc7cc310e5603c4620d74e8cf679a38eeb2 Mon Sep 17 00:00:00 2001 From: Teryk Bellahsene Date: Fri, 9 Dec 2016 09:27:24 +0100 Subject: [PATCH] SONAR-8466 Create WS api/project_analyses/update_event --- .../ProjectAnalysisModule.java | 2 + .../projectanalysis/ws/CreateEventAction.java | 2 +- .../projectanalysis/ws/DeleteEventAction.java | 11 +- .../projectanalysis/ws/EventValidator.java | 40 +++ .../projectanalysis/ws/UpdateEventAction.java | 178 ++++++++++++ .../ws/update_event-example.json | 9 + .../ProjectAnalysisModuleTest.java | 2 +- .../ws/CreateEventActionTest.java | 2 +- .../ws/DeleteEventActionTest.java | 2 +- .../ws/UpdateEventActionTest.java | 259 ++++++++++++++++++ .../java/org/sonar/db/event/EventDao.java | 5 + .../java/org/sonar/db/event/EventMapper.java | 4 + .../org/sonar/db/event/EventValidator.java | 9 +- .../org/sonar/db/event/EventMapper.xml | 7 + .../java/org/sonar/db/event/EventDaoTest.java | 12 + .../projectanalysis/UpdateEventRequest.java | 54 ++++ .../main/protobuf/ws-projectanalyses.proto | 5 + .../UpdateEventRequestTest.java | 77 ++++++ 18 files changed, 662 insertions(+), 18 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/EventValidator.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/UpdateEventAction.java create mode 100644 server/sonar-server/src/main/resources/org/sonar/server/projectanalysis/ws/update_event-example.json create mode 100644 server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/UpdateEventActionTest.java create mode 100644 sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/UpdateEventRequest.java create mode 100644 sonar-ws/src/test/java/org/sonarqube/ws/client/projectanalysis/UpdateEventRequestTest.java 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 61f5ecd3052..e4c43303e22 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.UpdateEventAction; public class ProjectAnalysisModule extends Module { @@ -33,6 +34,7 @@ public class ProjectAnalysisModule extends Module { ProjectAnalysesWs.class, // actions CreateEventAction.class, + UpdateEventAction.class, DeleteEventAction.class); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/CreateEventAction.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/CreateEventAction.java index 96d068e8c27..cc5a422051f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/CreateEventAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/CreateEventAction.java @@ -205,7 +205,7 @@ public class CreateEventAction implements ProjectAnalysesWsAction { }; case OTHER: return dbEvent -> { - throw new IllegalArgumentException(format("An 'other' event with the same name already exists on analysis '%s'", request.getAnalysis())); + throw new IllegalArgumentException(format("An '%s' event with the same name already exists on analysis '%s'", OTHER.getLabel(), request.getAnalysis())); }; default: throw new IllegalStateException("Event category not handled: " + request.getCategory()); diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/DeleteEventAction.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/DeleteEventAction.java index ac6be518af1..1fe95314717 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/DeleteEventAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/DeleteEventAction.java @@ -20,9 +20,6 @@ package org.sonar.server.projectanalysis.ws; -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableSet; -import java.util.Set; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; @@ -37,14 +34,11 @@ import org.sonarqube.ws.client.projectanalysis.DeleteEventRequest; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.String.format; -import static org.sonarqube.ws.client.projectanalysis.EventCategory.OTHER; +import static org.sonar.server.projectanalysis.ws.EventValidator.checkModifiable; import static org.sonarqube.ws.client.projectanalysis.EventCategory.VERSION; import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_EVENT; public class DeleteEventAction implements ProjectAnalysesWsAction { - private static final Set AUTHORIZED_CATEGORIES = ImmutableSet.of(VERSION.getLabel(), OTHER.getLabel()); - private static final String AUTHORIZED_CATEGORIES_INLINED = Joiner.on(", ").join(AUTHORIZED_CATEGORIES); - private final DbClient dbClient; private final UserSession userSession; @@ -83,8 +77,7 @@ public class DeleteEventAction implements ProjectAnalysesWsAction { EventDto dbEvent = dbClient.eventDao().selectByUuid(dbSession, request.getEvent()) .orElseThrow(() -> new NotFoundException(format("Event '%s' not found", request.getEvent()))); checkPermissions(dbEvent); - checkArgument(AUTHORIZED_CATEGORIES.contains(dbEvent.getCategory()), "Event of category '%s' cannot be deleted. Authorized categories: %s", dbEvent.getCategory(), - AUTHORIZED_CATEGORIES_INLINED); + checkModifiable().accept(dbEvent); deleteEvent(dbSession, dbEvent); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/EventValidator.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/EventValidator.java new file mode 100644 index 00000000000..52604c00cb6 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/EventValidator.java @@ -0,0 +1,40 @@ +/* + * 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.Joiner; +import com.google.common.collect.ImmutableSet; +import java.util.Set; +import java.util.function.Consumer; +import org.sonar.db.event.EventDto; + +import static com.google.common.base.Preconditions.checkArgument; + +class EventValidator { + private static final Set AUTHORIZED_CATEGORIES = ImmutableSet.of("Version", "Other"); + private static final String AUTHORIZED_CATEGORIES_INLINED = Joiner.on(", ").join(AUTHORIZED_CATEGORIES); + + static Consumer checkModifiable() { + return event -> checkArgument(AUTHORIZED_CATEGORIES.contains(event.getCategory()), + "Event of category '%s' cannot be modified. Authorized categories: %s", + event.getCategory(), AUTHORIZED_CATEGORIES_INLINED); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/UpdateEventAction.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/UpdateEventAction.java new file mode 100644 index 00000000000..823c057f60a --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/UpdateEventAction.java @@ -0,0 +1,178 @@ +/* + * 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.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +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.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.event.EventDto; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.ProjectAnalyses.Event; +import org.sonarqube.ws.ProjectAnalyses.UpdateEventResponse; +import org.sonarqube.ws.client.projectanalysis.UpdateEventRequest; + +import static java.lang.String.format; +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.server.projectanalysis.ws.EventValidator.checkModifiable; +import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.client.projectanalysis.EventCategory.VERSION; +import static org.sonarqube.ws.client.projectanalysis.EventCategory.fromLabel; +import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_DESCRIPTION; +import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_EVENT; +import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_NAME; + +public class UpdateEventAction implements ProjectAnalysesWsAction { + private final DbClient dbClient; + private final UserSession userSession; + + public UpdateEventAction(DbClient dbClient, UserSession userSession) { + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("update_event") + .setDescription("Update a project analysis event.
" + + "Requires one of the following permissions:" + + "
    " + + "
  • 'Administer System'
  • " + + "
  • 'Administer' rights on the specified project
  • " + + "
") + .setSince("6.3") + .setPost(true) + .setResponseExample(getClass().getResource("update_event-example.json")) + .setHandler(this); + + action.createParam(PARAM_EVENT) + .setDescription("Event key") + .setRequired(true); + + action.createParam(PARAM_NAME) + .setDescription("New name") + .setExampleValue("5.6"); + + action.createParam(PARAM_DESCRIPTION) + .setDescription("New description") + .setExampleValue("Version released"); + } + + @Override + public void handle(Request httpRequest, Response httpResponse) throws Exception { + Stream.of(httpRequest) + .map(toUpdateEventRequest()) + .map(this::doHandle) + .forEach(wsResponse -> writeProtobuf(wsResponse, httpRequest, httpResponse)); + } + + private UpdateEventResponse doHandle(UpdateEventRequest request) { + try (DbSession dbSession = dbClient.openSession(false)) { + return Stream + .of(getDbEvent(dbSession, request)) + .peek(checkPermissions()) + .peek(checkModifiable()) + .map(updateNameAndDescription(request)) + .peek(checkNonConflictingOtherEvents(dbSession)) + .peek(updateInDb(dbSession)) + .map(toWsResponse()) + .findAny() + .orElseThrow(() -> new IllegalStateException("Event not found")); + } + } + + private Consumer updateInDb(DbSession dbSession) { + return event -> { + dbClient.eventDao().update(dbSession, event.getUuid(), event.getName(), event.getDescription()); + if (VERSION.getLabel().equals(event.getCategory())) { + SnapshotDto analysis = getAnalysis(dbSession, event); + dbClient.snapshotDao().updateVersion(dbSession, analysis.getUuid(), event.getName()); + } + dbSession.commit(); + }; + } + + private EventDto getDbEvent(DbSession dbSession, UpdateEventRequest request) { + return dbClient.eventDao().selectByUuid(dbSession, request.getEvent()) + .orElseThrow(() -> new NotFoundException(format("Event '%s' not found", request.getEvent()))); + } + + private Consumer checkPermissions() { + return event -> userSession.checkComponentUuidPermission(UserRole.ADMIN, event.getComponentUuid()); + } + + private Consumer checkNonConflictingOtherEvents(DbSession dbSession) { + return candidateEvent -> { + List dbEvents = dbClient.eventDao().selectByAnalysisUuid(dbSession, candidateEvent.getAnalysisUuid()); + Predicate otherEventWithSameName = otherEvent -> !candidateEvent.getUuid().equals(otherEvent.getUuid()) && otherEvent.getName().equals(candidateEvent.getName()); + dbEvents.stream() + .filter(otherEventWithSameName) + .findAny() + .ifPresent(event -> { + throw new IllegalArgumentException(format("An '%s' event with the same name already exists on analysis '%s'", + candidateEvent.getCategory(), + candidateEvent.getAnalysisUuid())); + }); + }; + } + + private SnapshotDto getAnalysis(DbSession dbSession, EventDto event) { + return dbClient.snapshotDao().selectByUuid(dbSession, event.getAnalysisUuid()) + .orElseThrow(() -> new IllegalStateException(format("Analysis '%s' is not found", event.getAnalysisUuid()))); + } + + private static Function updateNameAndDescription(UpdateEventRequest request) { + return event -> { + setNullable(request.getName(), event::setName); + setNullable(request.getDescription(), event::setDescription); + return event; + }; + } + + private static Function toWsResponse() { + return dbEvent -> { + Event.Builder wsEvent = Event.newBuilder() + .setKey(dbEvent.getUuid()) + .setCategory(fromLabel(dbEvent.getCategory()).name()) + .setAnalysis(dbEvent.getAnalysisUuid()); + setNullable(dbEvent.getName(), wsEvent::setName); + setNullable(dbEvent.getDescription(), wsEvent::setDescription); + + return UpdateEventResponse.newBuilder().setEvent(wsEvent).build(); + }; + } + + private static Function toUpdateEventRequest() { + return request -> new UpdateEventRequest( + request.mandatoryParam(PARAM_EVENT), + request.param(PARAM_NAME), + request.param(PARAM_DESCRIPTION)); + } +} diff --git a/server/sonar-server/src/main/resources/org/sonar/server/projectanalysis/ws/update_event-example.json b/server/sonar-server/src/main/resources/org/sonar/server/projectanalysis/ws/update_event-example.json new file mode 100644 index 00000000000..14e486d5b60 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/projectanalysis/ws/update_event-example.json @@ -0,0 +1,9 @@ +{ + "event": { + "analysis": "A2", + "key": "E1", + "category": "OTHER", + "name": "My Custom Event", + "description": "event description" + } +} 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 1f81537fee8..bd1018524f6 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 + 3); + assertThat(container.size()).isEqualTo(2 + 4); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/CreateEventActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/CreateEventActionTest.java index dd6ecaad45d..3070200a401 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/CreateEventActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/CreateEventActionTest.java @@ -257,7 +257,7 @@ public class CreateEventActionTest { call(request); expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("An 'other' event with the same name already exists on analysis '" + analysis.getUuid() + "'"); + expectedException.expectMessage("An 'Other' event with the same name already exists on analysis '" + analysis.getUuid() + "'"); call(request.setName("Project Import")); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/DeleteEventActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/DeleteEventActionTest.java index dcd4ffd4f46..10cd80f445b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/DeleteEventActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/DeleteEventActionTest.java @@ -113,7 +113,7 @@ public class DeleteEventActionTest { db.events().insertEvent(newEvent(analysis).setUuid("E1").setCategory("Profile")); expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Event of category 'Profile' cannot be deleted. Authorized categories: Version, Other"); + expectedException.expectMessage("Event of category 'Profile' cannot be modified. Authorized categories: Version, Other"); call("E1"); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/UpdateEventActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/UpdateEventActionTest.java new file mode 100644 index 00000000000..f9ec5d17055 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/UpdateEventActionTest.java @@ -0,0 +1,259 @@ +/* + * 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 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.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +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.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.MediaTypes; +import org.sonarqube.ws.ProjectAnalyses; +import org.sonarqube.ws.ProjectAnalyses.UpdateEventResponse; + +import static org.assertj.core.api.Assertions.assertThat; +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.test.JsonAssert.assertJson; +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.VERSION; +import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_DESCRIPTION; +import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_EVENT; +import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_NAME; + +public class UpdateEventActionTest { + @Rule + public UserSessionRule userSession = UserSessionRule.standalone().setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + private DbClient dbClient = db.getDbClient(); + private DbSession dbSession = db.getSession(); + + private WsActionTester ws = new WsActionTester(new UpdateEventAction(dbClient, userSession)); + + @Test + public void json_example() { + ComponentDto project = db.components().insertProject(); + SnapshotDto analysis = db.components().insertSnapshot(newAnalysis(project).setUuid("A2")); + db.events().insertEvent(newEvent(analysis) + .setUuid("E1") + .setCategory(OTHER.getLabel()) + .setName("Original Name") + .setDescription("Original Description")); + + String result = ws.newRequest() + .setParam(PARAM_EVENT, "E1") + .setParam(PARAM_NAME, "My Custom Event") + .setParam(PARAM_DESCRIPTION, "event description") + .execute().getInput(); + + assertJson(result).isSimilarTo(getClass().getResource("update_event-example.json")); + } + + @Test + public void update_name_and_description_in_db() { + SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto()); + EventDto originalEvent = db.events().insertEvent(newEvent(analysis).setUuid("E1").setName("Original Name").setDescription("Original Description")); + + call("E1", "name", "description"); + + EventDto newEvent = dbClient.eventDao().selectByUuid(dbSession, "E1").get(); + assertThat(newEvent.getName()).isEqualTo("name"); + assertThat(newEvent.getDescription()).isEqualTo("description"); + assertThat(newEvent.getCategory()).isEqualTo(originalEvent.getCategory()); + assertThat(newEvent.getDate()).isEqualTo(originalEvent.getDate()); + assertThat(newEvent.getCreatedAt()).isEqualTo(originalEvent.getCreatedAt()); + } + + @Test + public void ws_response_with_updated_name_and_description() { + SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto()); + EventDto originalEvent = db.events().insertEvent(newEvent(analysis).setUuid("E1").setName("Original Name").setDescription("Original Description")); + + ProjectAnalyses.Event result = call("E1", "name", "description").getEvent(); + + assertThat(result.getName()).isEqualTo("name"); + assertThat(result.getDescription()).isEqualTo("description"); + assertThat(result.getCategory()).isEqualTo(OTHER.name()); + assertThat(result.getAnalysis()).isEqualTo(originalEvent.getAnalysisUuid()); + assertThat(result.getKey()).isEqualTo("E1"); + } + + @Test + public void update_VERSION_event_update_analysis_version() { + ComponentDto project = db.components().insertProject(); + SnapshotDto analysis = db.components().insertSnapshot(newAnalysis(project).setVersion("5.6")); + db.events().insertEvent(newEvent(analysis).setUuid("E1").setCategory(VERSION.getLabel())); + + call("E1", "6.3", null); + + SnapshotDto updatedAnalysis = dbClient.snapshotDao().selectByUuid(dbSession, analysis.getUuid()).get(); + assertThat(updatedAnalysis.getVersion()).isEqualTo("6.3"); + } + + @Test + public void update_OTHER_event_does_not_update_analysis_version() { + ComponentDto project = db.components().insertProject(); + SnapshotDto analysis = db.components().insertSnapshot(newAnalysis(project).setVersion("5.6")); + db.events().insertEvent(newEvent(analysis).setUuid("E1").setCategory(OTHER.getLabel())); + + call("E1", "6.3", null); + + SnapshotDto updatedAnalysis = dbClient.snapshotDao().selectByUuid(dbSession, analysis.getUuid()).get(); + assertThat(updatedAnalysis.getVersion()).isEqualTo("5.6"); + } + + @Test + public void update_name_only_in_db() { + SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto()); + EventDto originalEvent = db.events().insertEvent(newEvent(analysis).setUuid("E1").setName("Original Name").setDescription("Original Description")); + + call("E1", "name", null); + + EventDto newEvent = dbClient.eventDao().selectByUuid(dbSession, "E1").get(); + assertThat(newEvent.getName()).isEqualTo("name"); + assertThat(newEvent.getDescription()).isEqualTo(originalEvent.getDescription()); + } + + @Test + public void update_as_project_admin() { + SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto("P1")); + db.events().insertEvent(newEvent(analysis).setUuid("E1").setName("Original Name").setDescription("Original Description")); + userSession.anonymous().addProjectUuidPermissions(UserRole.ADMIN, "P1"); + + call("E1", "name", "description"); + + EventDto newEvent = dbClient.eventDao().selectByUuid(dbSession, "E1").get(); + assertThat(newEvent.getName()).isEqualTo("name"); + assertThat(newEvent.getDescription()).isEqualTo("description"); + } + + @Test + public void update_description_only_in_db() { + SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto()); + EventDto originalEvent = db.events().insertEvent(newEvent(analysis).setUuid("E1").setName("Original Name").setDescription("Original Description")); + + call("E1", null, "description"); + + EventDto newEvent = dbClient.eventDao().selectByUuid(dbSession, "E1").get(); + assertThat(newEvent.getName()).isEqualTo(originalEvent.getName()); + assertThat(newEvent.getDescription()).isEqualTo("description"); + } + + @Test + public void ws_definition() { + WebService.Action definition = ws.getDef(); + + assertThat(definition.key()).isEqualTo("update_event"); + assertThat(definition.responseExampleAsString()).isNotEmpty(); + assertThat(definition.isPost()).isTrue(); + assertThat(definition.since()).isEqualTo("6.3"); + assertThat(definition.param(PARAM_EVENT).isRequired()).isTrue(); + } + + @Test + public void fail_if_insufficient_permissions() { + SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto()); + db.events().insertEvent(newEvent(analysis).setUuid("E1")); + userSession.anonymous(); + + expectedException.expect(ForbiddenException.class); + + call("E1", "name", "description"); + } + + @Test + public void fail_if_event_is_not_found() { + expectedException.expect(NotFoundException.class); + expectedException.expectMessage("Event 'E42' not found"); + + call("E42", "name", "description"); + } + + @Test + public void fail_if_no_name_nor_description() { + SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto()); + db.events().insertEvent(newEvent(analysis).setUuid("E1")); + + expectedException.expect(IllegalArgumentException.class); + + call("E1", null, null); + } + + @Test + public void fail_if_category_other_than_other_or_version() { + SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto()); + db.events().insertEvent(newEvent(analysis).setUuid("E1").setCategory("Profile")); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Event of category 'Profile' cannot be modified. Authorized categories: Version, Other"); + + call("E1", "name", "description"); + } + + @Test + public void fail_if_other_event_with_same_name_on_same_analysis() { + SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto()); + db.events().insertEvent(newEvent(analysis).setUuid("E1").setCategory(OTHER.getLabel()).setName("E1 name")); + db.events().insertEvent(newEvent(analysis).setUuid("E2").setCategory(OTHER.getLabel()).setName("E2 name")); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("An 'Other' event with the same name already exists on analysis '" + analysis.getUuid() + "'"); + + call("E2", "E1 name", "description"); + } + + private UpdateEventResponse call(@Nullable String eventUuid, @Nullable String name, @Nullable String description) { + TestRequest request = ws.newRequest() + .setMethod(POST.name()) + .setMediaType(MediaTypes.PROTOBUF); + setNullable(eventUuid, e -> request.setParam(PARAM_EVENT, e)); + setNullable(name, n -> request.setParam(PARAM_NAME, n)); + setNullable(description, d -> request.setParam(PARAM_DESCRIPTION, d)); + + try { + return UpdateEventResponse.parseFrom(request.execute().getInputStream()); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/sonar-db/src/main/java/org/sonar/db/event/EventDao.java b/sonar-db/src/main/java/org/sonar/db/event/EventDao.java index 86a99504abd..b6241690718 100644 --- a/sonar-db/src/main/java/org/sonar/db/event/EventDao.java +++ b/sonar-db/src/main/java/org/sonar/db/event/EventDao.java @@ -21,6 +21,7 @@ package org.sonar.db.event; import java.util.List; import java.util.Optional; +import javax.annotation.Nullable; import org.sonar.db.Dao; import org.sonar.db.DbSession; @@ -44,6 +45,10 @@ public class EventDao implements Dao { return dto; } + public void update(DbSession dbSession, String uuid, @Nullable String name, @Nullable String description) { + mapper(dbSession).update(uuid, name, description); + } + public void delete(DbSession session, Long id) { mapper(session).deleteById(id); } diff --git a/sonar-db/src/main/java/org/sonar/db/event/EventMapper.java b/sonar-db/src/main/java/org/sonar/db/event/EventMapper.java index 5c2f5030356..3a6db52c352 100644 --- a/sonar-db/src/main/java/org/sonar/db/event/EventMapper.java +++ b/sonar-db/src/main/java/org/sonar/db/event/EventMapper.java @@ -20,6 +20,8 @@ package org.sonar.db.event; import java.util.List; +import javax.annotation.Nullable; +import org.apache.ibatis.annotations.Param; public interface EventMapper { @@ -31,6 +33,8 @@ public interface EventMapper { void insert(EventDto dto); + void update(@Param("uuid") String uuid, @Param("name") @Nullable String name, @Param("description") @Nullable String description); + void deleteById(long id); void deleteByUuid(String uuid); diff --git a/sonar-db/src/main/java/org/sonar/db/event/EventValidator.java b/sonar-db/src/main/java/org/sonar/db/event/EventValidator.java index d69684950db..0044f29aad1 100644 --- a/sonar-db/src/main/java/org/sonar/db/event/EventValidator.java +++ b/sonar-db/src/main/java/org/sonar/db/event/EventValidator.java @@ -25,7 +25,7 @@ import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; -public class EventValidator { +class EventValidator { private static final int MAX_NAME_LENGTH = 400; private static final int MAX_CATEGORY_LENGTH = 50; private static final int MAX_DESCRIPTION_LENGTH = 4000; @@ -35,7 +35,7 @@ public class EventValidator { } @CheckForNull - public static String checkEventName(@Nullable String name) { + static String checkEventName(@Nullable String name) { if (name == null) { return null; } @@ -45,7 +45,7 @@ public class EventValidator { } @CheckForNull - public static String checkEventCategory(@Nullable String category) { + static String checkEventCategory(@Nullable String category) { if (category == null) { return null; } @@ -55,7 +55,7 @@ public class EventValidator { } @CheckForNull - public static String checkEventDescription(@Nullable String description) { + static String checkEventDescription(@Nullable String description) { if (description == null) { return null; } @@ -63,5 +63,4 @@ public class EventValidator { description.length(), MAX_DESCRIPTION_LENGTH, description); return description; } - } diff --git a/sonar-db/src/main/resources/org/sonar/db/event/EventMapper.xml b/sonar-db/src/main/resources/org/sonar/db/event/EventMapper.xml index e9241e67c66..bffc0b4b240 100644 --- a/sonar-db/src/main/resources/org/sonar/db/event/EventMapper.xml +++ b/sonar-db/src/main/resources/org/sonar/db/event/EventMapper.xml @@ -56,6 +56,13 @@ #{createdAt, jdbcType=BIGINT}) + + update events + set name = #{name, jdbcType=VARCHAR}, + description = #{description, jdbcType=VARCHAR} + where uuid = #{uuid} + + DELETE FROM events WHERE id=#{id} diff --git a/sonar-db/src/test/java/org/sonar/db/event/EventDaoTest.java b/sonar-db/src/test/java/org/sonar/db/event/EventDaoTest.java index b5e2850b1ea..120d4493278 100644 --- a/sonar-db/src/test/java/org/sonar/db/event/EventDaoTest.java +++ b/sonar-db/src/test/java/org/sonar/db/event/EventDaoTest.java @@ -129,6 +129,18 @@ public class EventDaoTest { dbTester.assertDbUnit(getClass(), "insert-result.xml", new String[] {"id"}, "events"); } + @Test + public void update_name_and_description() { + SnapshotDto analysis = dbTester.components().insertProjectAndSnapshot(newProjectDto()); + dbTester.events().insertEvent(newEvent(analysis).setUuid("E1")); + + underTest.update(dbSession, "E1", "New Name", "New Description"); + + EventDto result = dbClient.eventDao().selectByUuid(dbSession, "E1").get(); + assertThat(result.getName()).isEqualTo("New Name"); + assertThat(result.getDescription()).isEqualTo("New Description"); + } + @Test public void delete_by_id() { dbTester.prepareDbUnit(getClass(), "delete.xml"); diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/UpdateEventRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/UpdateEventRequest.java new file mode 100644 index 00000000000..9afeb37c12d --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/UpdateEventRequest.java @@ -0,0 +1,54 @@ +/* + * 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.sonarqube.ws.client.projectanalysis; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +public class UpdateEventRequest { + private final String event; + private final String name; + private final String description; + + public UpdateEventRequest(String event, @Nullable String name, @Nullable String description) { + checkArgument(name != null || description != null, "Name or description is required"); + this.event = requireNonNull(event, "Event key is required"); + this.name = name; + this.description = description; + } + + public String getEvent() { + return event; + } + + @CheckForNull + public String getName() { + return name; + } + + @CheckForNull + public String getDescription() { + return description; + } +} diff --git a/sonar-ws/src/main/protobuf/ws-projectanalyses.proto b/sonar-ws/src/main/protobuf/ws-projectanalyses.proto index 686027ad229..b06aca07713 100644 --- a/sonar-ws/src/main/protobuf/ws-projectanalyses.proto +++ b/sonar-ws/src/main/protobuf/ws-projectanalyses.proto @@ -29,6 +29,11 @@ message CreateEventResponse { optional Event event = 1; } +// WS api/project_analyses/update_event +message UpdateEventResponse { + optional Event event = 1; +} + message Event { optional string key = 1; optional string analysis = 2; diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/projectanalysis/UpdateEventRequestTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/projectanalysis/UpdateEventRequestTest.java new file mode 100644 index 00000000000..43e05ad956d --- /dev/null +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/projectanalysis/UpdateEventRequestTest.java @@ -0,0 +1,77 @@ +/* + * 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.sonarqube.ws.client.projectanalysis; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UpdateEventRequestTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private UpdateEventRequest underTest; + + @Test + public void request_with_name_only() { + underTest = new UpdateEventRequest("E1", "name", null); + + assertThat(underTest.getEvent()).isEqualTo("E1"); + assertThat(underTest.getName()).isEqualTo("name"); + assertThat(underTest.getDescription()).isNull(); + } + + @Test + public void request_with_description_only() { + underTest = new UpdateEventRequest("E1", null , "description"); + + assertThat(underTest.getEvent()).isEqualTo("E1"); + assertThat(underTest.getName()).isNull(); + assertThat(underTest.getDescription()).isEqualTo("description"); + } + + @Test + public void request_with_all_parameters() { + underTest = new UpdateEventRequest("E1", "name", "description"); + + assertThat(underTest.getEvent()).isEqualTo("E1"); + assertThat(underTest.getName()).isEqualTo("name"); + assertThat(underTest.getDescription()).isEqualTo("description"); + } + + @Test + public void fail_if_null_event() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Event key is required"); + + new UpdateEventRequest(null, "name", "description"); + } + + @Test + public void fail_if_name_and_description_not_provided() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Name or description is required"); + + new UpdateEventRequest("E1", null, null); + } +} -- 2.39.5