From 564fca44405012a51c4b2ae01e3732e71c70ccff Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Thu, 10 Nov 2016 20:23:27 +0100 Subject: [PATCH] SONAR-8353 add WS api/webhooks/delivery --- .../webhook/ws/WebhookDeliveriesAction.java | 26 +-- .../webhook/ws/WebhookDeliveryAction.java | 113 ++++++++++++ .../server/webhook/ws/WebhookWsSupport.java | 61 +++++++ .../server/webhook/ws/WebhooksWsModule.java | 1 + .../server/webhook/ws/example-delivery.json | 14 ++ .../webhook/ws/WebhookDeliveryActionTest.java | 161 ++++++++++++++++++ .../webhook/ws/WebhooksWsModuleTest.java | 6 +- .../ws/client/webhook/WebhooksService.java | 9 + sonar-ws/src/main/protobuf/ws-webhooks.proto | 7 + 9 files changed, 376 insertions(+), 22 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveryAction.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java create mode 100644 server/sonar-server/src/main/resources/org/sonar/server/webhook/ws/example-delivery.json create mode 100644 server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveryActionTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveriesAction.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveriesAction.java index 8a4bc4f388d..680a2e186f2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveriesAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveriesAction.java @@ -38,7 +38,7 @@ import org.sonarqube.ws.Webhooks; import static com.google.common.base.Preconditions.checkArgument; import static java.util.Objects.requireNonNull; -import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.server.webhook.ws.WebhookWsSupport.copyDtoToProtobuf; import static org.sonar.server.ws.WsUtils.writeProtobuf; public class WebhookDeliveriesAction implements WebhooksWsAction { @@ -61,7 +61,7 @@ public class WebhookDeliveriesAction implements WebhooksWsAction { WebService.NewAction action = controller.createAction("deliveries") .setSince("6.2") .setDescription("Get the recent deliveries for a specified project or Compute Engine task.
" + - "Require 'Administer System' permission.
" + + "Require 'Administer' permission on the related project.
" + "Note that additional information are returned by api/webhooks/delivery.") .setResponseExample(Resources.getResource(this.getClass(), "example-deliveries.json")) .setInternal(true) @@ -113,10 +113,10 @@ public class WebhookDeliveriesAction implements WebhooksWsAction { private static class Data { private final ComponentDto component; - private final List deliveries; + private final List deliveryDtos; Data(@Nullable ComponentDto component, List deliveries) { - this.deliveries = deliveries; + this.deliveryDtos = deliveries; if (deliveries.isEmpty()) { this.component = null; } else { @@ -133,22 +133,8 @@ public class WebhookDeliveriesAction implements WebhooksWsAction { void writeTo(Request request, Response response) { Webhooks.DeliveriesWsResponse.Builder responseBuilder = Webhooks.DeliveriesWsResponse.newBuilder(); Webhooks.Delivery.Builder deliveryBuilder = Webhooks.Delivery.newBuilder(); - for (WebhookDeliveryLiteDto dto : deliveries) { - deliveryBuilder - .clear() - .setId(dto.getUuid()) - .setAt(formatDateTime(dto.getCreatedAt())) - .setName(dto.getName()) - .setUrl(dto.getUrl()) - .setSuccess(dto.isSuccess()) - .setCeTaskId(dto.getCeTaskUuid()) - .setComponentKey(component.getKey()); - if (dto.getHttpStatus() != null) { - deliveryBuilder.setHttpStatus(dto.getHttpStatus()); - } - if (dto.getDurationMs() != null) { - deliveryBuilder.setDurationMs(dto.getDurationMs()); - } + for (WebhookDeliveryLiteDto dto : deliveryDtos) { + copyDtoToProtobuf(component, dto, deliveryBuilder); responseBuilder.addDeliveries(deliveryBuilder); } writeProtobuf(responseBuilder.build(), request, response); diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveryAction.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveryAction.java new file mode 100644 index 00000000000..cec10909c95 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveryAction.java @@ -0,0 +1,113 @@ +/* + * 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.webhook.ws; + +import com.google.common.io.Resources; +import java.util.Optional; +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.core.util.Uuids; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.webhook.WebhookDeliveryDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.Webhooks; + +import static java.util.Objects.requireNonNull; +import static org.sonar.server.webhook.ws.WebhookWsSupport.copyDtoToProtobuf; +import static org.sonar.server.ws.WsUtils.checkFoundWithOptional; +import static org.sonar.server.ws.WsUtils.writeProtobuf; + +public class WebhookDeliveryAction implements WebhooksWsAction { + + private static final String PARAM_ID = "deliveryId"; + + private final DbClient dbClient; + private final UserSession userSession; + private final ComponentFinder componentFinder; + + public WebhookDeliveryAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder) { + this.dbClient = dbClient; + this.userSession = userSession; + this.componentFinder = componentFinder; + } + + @Override + public void define(WebService.NewController controller) { + WebService.NewAction action = controller.createAction("delivery") + .setSince("6.2") + .setDescription("Get a webhook delivery by its id.
" + + "Require 'Administer System' permission.
" + + "Note that additional information are returned by api/webhooks/delivery.") + .setResponseExample(Resources.getResource(this.getClass(), "example-delivery.json")) + .setInternal(true) + .setHandler(this); + + action.createParam(PARAM_ID) + .setDescription("Id of delivery") + .setExampleValue(Uuids.UUID_EXAMPLE_06); + } + + @Override + public void handle(Request request, Response response) throws Exception { + // fail-fast if not logged in + userSession.checkLoggedIn(); + + Data data = loadFromDatabase(request.mandatoryParam(PARAM_ID)); + data.ensureAdminPermission(userSession); + data.writeTo(request, response); + } + + private Data loadFromDatabase(String deliveryUuid) { + try (DbSession dbSession = dbClient.openSession(false)) { + Optional delivery = dbClient.webhookDeliveryDao().selectByUuid(dbSession, deliveryUuid); + checkFoundWithOptional(delivery, "Webhook delivery not found"); + ComponentDto component = componentFinder.getByUuid(dbSession, delivery.get().getComponentUuid()); + return new Data(component, delivery.get()); + } + } + + private static class Data { + private final ComponentDto component; + private final WebhookDeliveryDto deliveryDto; + + Data(ComponentDto component, WebhookDeliveryDto delivery) { + this.deliveryDto = requireNonNull(delivery); + this.component = requireNonNull(component); + } + + void ensureAdminPermission(UserSession userSession) { + userSession.checkComponentUuidPermission(UserRole.ADMIN, component.uuid()); + } + + void writeTo(Request request, Response response) { + Webhooks.DeliveryWsResponse.Builder responseBuilder = Webhooks.DeliveryWsResponse.newBuilder(); + Webhooks.Delivery.Builder deliveryBuilder = Webhooks.Delivery.newBuilder(); + copyDtoToProtobuf(component, deliveryDto, deliveryBuilder); + responseBuilder.setDelivery(deliveryBuilder); + + writeProtobuf(responseBuilder.build(), request, response); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java new file mode 100644 index 00000000000..c23df78cf4d --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java @@ -0,0 +1,61 @@ +/* + * 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.webhook.ws; + +import org.sonar.db.component.ComponentDto; +import org.sonar.db.webhook.WebhookDeliveryDto; +import org.sonar.db.webhook.WebhookDeliveryLiteDto; +import org.sonarqube.ws.Webhooks; + +import static org.sonar.api.utils.DateUtils.formatDateTime; + +class WebhookWsSupport { + private WebhookWsSupport() { + // only statics + } + + static Webhooks.Delivery.Builder copyDtoToProtobuf(ComponentDto component, WebhookDeliveryLiteDto dto, Webhooks.Delivery.Builder builder) { + builder + .clear() + .setId(dto.getUuid()) + .setAt(formatDateTime(dto.getCreatedAt())) + .setName(dto.getName()) + .setUrl(dto.getUrl()) + .setSuccess(dto.isSuccess()) + .setCeTaskId(dto.getCeTaskUuid()) + .setComponentKey(component.getKey()); + if (dto.getHttpStatus() != null) { + builder.setHttpStatus(dto.getHttpStatus()); + } + if (dto.getDurationMs() != null) { + builder.setDurationMs(dto.getDurationMs()); + } + return builder; + } + + static Webhooks.Delivery.Builder copyDtoToProtobuf(ComponentDto component, WebhookDeliveryDto dto, Webhooks.Delivery.Builder builder) { + copyDtoToProtobuf(component, (WebhookDeliveryLiteDto) dto, builder); + builder.setPayload(dto.getPayload()); + if (dto.getErrorStacktrace() != null) { + builder.setErrorStacktrace(dto.getErrorStacktrace()); + } + return builder; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java index 9ac075dfa65..adc31a60dd9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java @@ -26,6 +26,7 @@ public class WebhooksWsModule extends Module { protected void configureModule() { add( WebhooksWs.class, + WebhookDeliveryAction.class, WebhookDeliveriesAction.class); } } diff --git a/server/sonar-server/src/main/resources/org/sonar/server/webhook/ws/example-delivery.json b/server/sonar-server/src/main/resources/org/sonar/server/webhook/ws/example-delivery.json new file mode 100644 index 00000000000..d4da8033df5 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/webhook/ws/example-delivery.json @@ -0,0 +1,14 @@ +{ + "delivery": { + "id": "d1", + "componentKey": "my-project", + "ceTaskId": "task-1", + "name": "Jenkins", + "url": "http://jenkins", + "at": "2017-07-14T04:40:00+0200", + "success": true, + "httpStatus": 200, + "durationMs": 10, + "payload": "{\"status\"=\"SUCCESS\"}" + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveryActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveryActionTest.java new file mode 100644 index 00000000000..e7d9e788ffa --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveryActionTest.java @@ -0,0 +1,161 @@ +/* + * 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.webhook.ws; + +import org.junit.Before; +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.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.webhook.WebhookDeliveryDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.MediaTypes; +import org.sonarqube.ws.Webhooks; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.db.webhook.WebhookDbTesting.newWebhookDeliveryDto; +import static org.sonar.test.JsonAssert.assertJson; + +public class WebhookDeliveryActionTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + + private DbClient dbClient = db.getDbClient(); + private WsActionTester ws; + private ComponentDto project; + + @Before + public void setUp() { + ComponentFinder componentFinder = new ComponentFinder(dbClient); + WebhookDeliveryAction underTest = new WebhookDeliveryAction(dbClient, userSession, componentFinder); + ws = new WsActionTester(underTest); + project = db.components().insertComponent(newProjectDto().setKey("my-project")); + } + + @Test + public void test_definition() { + assertThat(ws.getDef().params()).extracting(WebService.Param::key).containsOnly("deliveryId"); + assertThat(ws.getDef().isPost()).isFalse(); + assertThat(ws.getDef().isInternal()).isTrue(); + assertThat(ws.getDef().responseExampleAsString()).isNotEmpty(); + } + + @Test + public void throw_UnauthorizedException_if_anonymous() { + expectedException.expect(UnauthorizedException.class); + + ws.newRequest().execute(); + } + + @Test + public void return_404_if_delivery_does_not_exist() throws Exception { + userSession.login(); + + expectedException.expect(NotFoundException.class); + + ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setParam("deliveryId", "does_not_exist") + .execute(); + } + + @Test + public void load_the_delivery_of_example() throws Exception { + WebhookDeliveryDto dto = newWebhookDeliveryDto() + .setUuid("d1") + .setComponentUuid(project.uuid()) + .setCeTaskUuid("task-1") + .setName("Jenkins") + .setUrl("http://jenkins") + .setCreatedAt(1_500_000_000_000L) + .setSuccess(true) + .setDurationMs(10) + .setHttpStatus(200) + .setPayload("{\"status\"=\"SUCCESS\"}"); + dbClient.webhookDeliveryDao().insert(db.getSession(), dto); + db.commit(); + userSession.login().addProjectUuidPermissions(UserRole.ADMIN, project.uuid()); + + String json = ws.newRequest() + .setParam("deliveryId", dto.getUuid()) + .execute() + .getInput(); + + assertJson(json).isSimilarTo(ws.getDef().responseExampleAsString()); + } + + @Test + public void return_delivery_that_failed_to_be_sent() throws Exception { + WebhookDeliveryDto dto = newWebhookDeliveryDto() + .setComponentUuid(project.uuid()) + .setSuccess(false) + .setHttpStatus(null) + .setDurationMs(null) + .setErrorStacktrace("IOException -> can not connect"); + dbClient.webhookDeliveryDao().insert(db.getSession(), dto); + db.commit(); + userSession.login().addProjectUuidPermissions(UserRole.ADMIN, project.uuid()); + + Webhooks.DeliveryWsResponse response = Webhooks.DeliveryWsResponse.parseFrom(ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setParam("deliveryId", dto.getUuid()) + .execute() + .getInputStream()); + + Webhooks.Delivery actual = response.getDelivery(); + assertThat(actual.hasHttpStatus()).isFalse(); + assertThat(actual.hasDurationMs()).isFalse(); + assertThat(actual.getErrorStacktrace()).isEqualTo(dto.getErrorStacktrace()); + } + + @Test + public void throw_ForbiddenException_if_not_admin_of_project() throws Exception { + WebhookDeliveryDto dto = newWebhookDeliveryDto() + .setComponentUuid(project.uuid()); + dbClient.webhookDeliveryDao().insert(db.getSession(), dto); + db.commit(); + userSession.login().addProjectUuidPermissions(UserRole.USER, project.uuid()); + + expectedException.expect(ForbiddenException.class); + expectedException.expectMessage("Insufficient privileges"); + + ws.newRequest() + .setParam("deliveryId", dto.getUuid()) + .execute(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhooksWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhooksWsModuleTest.java index 24a14364391..2a4d02f9a7e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhooksWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhooksWsModuleTest.java @@ -26,11 +26,13 @@ import static org.assertj.core.api.Assertions.assertThat; public class WebhooksWsModuleTest { + private WebhooksWsModule underTest = new WebhooksWsModule(); + @Test public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); - new WebhooksWsModule().configure(container); - assertThat(container.size()).isEqualTo(2 + 2); + underTest.configure(container); + assertThat(container.size()).isEqualTo(2 + 3); } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/WebhooksService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/WebhooksService.java index 67ee10aaf57..77cfa1eec94 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/WebhooksService.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/WebhooksService.java @@ -24,12 +24,21 @@ import org.sonarqube.ws.client.BaseService; import org.sonarqube.ws.client.GetRequest; import org.sonarqube.ws.client.WsConnector; +/** + * @since 6.2 + */ public class WebhooksService extends BaseService { public WebhooksService(WsConnector wsConnector) { super(wsConnector, "api/webhooks"); } + public Webhooks.DeliveryWsResponse delivery(String deliveryId) { + GetRequest httpRequest = new GetRequest(path("delivery")) + .setParam("deliveryId", deliveryId); + return call(httpRequest, Webhooks.DeliveryWsResponse.parser()); + } + /** * @throws org.sonarqube.ws.client.HttpException if HTTP status code is not 2xx. */ diff --git a/sonar-ws/src/main/protobuf/ws-webhooks.proto b/sonar-ws/src/main/protobuf/ws-webhooks.proto index 4ef519c3b9b..a7ef3a9ab2b 100644 --- a/sonar-ws/src/main/protobuf/ws-webhooks.proto +++ b/sonar-ws/src/main/protobuf/ws-webhooks.proto @@ -29,6 +29,11 @@ message DeliveriesWsResponse { repeated Delivery deliveries = 1; } +// WS api/webhooks/delivery +message DeliveryWsResponse { + optional Delivery delivery = 1; +} + message Delivery { optional string id = 1; optional string componentKey = 2; @@ -39,4 +44,6 @@ message Delivery { optional bool success = 7; optional int32 httpStatus = 8; optional int32 durationMs = 9; + optional string payload = 10; + optional string errorStacktrace = 11; } -- 2.39.5