]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8353 add WS api/webhooks/delivery
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Thu, 10 Nov 2016 19:23:27 +0000 (20:23 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Mon, 14 Nov 2016 11:18:51 +0000 (12:18 +0100)
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveriesAction.java
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveryAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java
server/sonar-server/src/main/resources/org/sonar/server/webhook/ws/example-delivery.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveryActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhooksWsModuleTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/WebhooksService.java
sonar-ws/src/main/protobuf/ws-webhooks.proto

index 8a4bc4f388d67e9d7551ee8d2ce7cea62b19404e..680a2e186f2c64857ece8b1a4521380b16479fc5 100644 (file)
@@ -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.<br/>" +
-        "Require 'Administer System' permission.<br/>" +
+        "Require 'Administer' permission on the related project.<br/>" +
         "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<WebhookDeliveryLiteDto> deliveries;
+    private final List<WebhookDeliveryLiteDto> deliveryDtos;
 
     Data(@Nullable ComponentDto component, List<WebhookDeliveryLiteDto> 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 (file)
index 0000000..cec1090
--- /dev/null
@@ -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.<br/>" +
+        "Require 'Administer System' permission.<br/>" +
+        "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<WebhookDeliveryDto> 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 (file)
index 0000000..c23df78
--- /dev/null
@@ -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;
+  }
+}
index 9ac075dfa6590607933f2d09587b9f4047d8a3af..adc31a60dd9e45f17486ef99dabee23c655a9b55 100644 (file)
@@ -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 (file)
index 0000000..d4da803
--- /dev/null
@@ -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 (file)
index 0000000..e7d9e78
--- /dev/null
@@ -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();
+  }
+}
index 24a143643919f9508cb6a56cf2e3ac24402c0efa..2a4d02f9a7ee62cd7b2c1c9384c198f03c98d265 100644 (file)
@@ -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);
   }
 
 }
index 67ee10aaf57739aab82a2b2ac0a814920622fe11..77cfa1eec9477ca26fc3feb858b82e2b60cb9ee3 100644 (file)
@@ -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.
    */
index 4ef519c3b9b7cf90c6e07a46c761fe4b9181a797..a7ef3a9ab2ba7f11de458d383470780295bc426a 100644 (file)
@@ -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;
 }