]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8353 add WS api/webhooks/deliveries
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Thu, 10 Nov 2016 17:28:57 +0000 (18:28 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Mon, 14 Nov 2016 11:18:51 +0000 (12:18 +0100)
20 files changed:
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveriesAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWs.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/webhook/ws/example-deliveries.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveriesActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhooksWsModuleTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhooksWsTest.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryDao.java
sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryMapper.java
sonar-db/src/main/resources/org/sonar/db/webhook/WebhookDeliveryMapper.xml
sonar-db/src/test/java/org/sonar/db/webhook/WebhookDeliveryDaoTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java
sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java
sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/DeliveriesRequest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/WebhooksService.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/package-info.java [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-webhooks.proto [new file with mode: 0644]

index 594d98a044edbf52537e6fa5cf7ddb9cc4686b9a..def3adbf166c3091f26991abc0b51580a81e52d3 100644 (file)
@@ -226,6 +226,7 @@ import org.sonar.server.util.TypeValidationModule;
 import org.sonar.server.view.index.ViewIndex;
 import org.sonar.server.view.index.ViewIndexDefinition;
 import org.sonar.server.view.index.ViewIndexer;
+import org.sonar.server.webhook.ws.WebhooksWsModule;
 import org.sonar.server.ws.WebServiceEngine;
 import org.sonar.server.ws.WebServiceFilter;
 import org.sonar.server.ws.WebServicesWs;
@@ -538,7 +539,10 @@ public class PlatformLevel4 extends PlatformLevel {
       NavigationWs.class,
 
       // root
-      RootWsModule.class);
+      RootWsModule.class,
+
+      // webhooks
+      WebhooksWsModule.class);
 
     addAll(level4AddedComponents);
   }
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
new file mode 100644 (file)
index 0000000..8a4bc4f
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * 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.List;
+import java.util.Optional;
+import javax.annotation.Nullable;
+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.WebhookDeliveryLiteDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.user.UserSession;
+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.ws.WsUtils.writeProtobuf;
+
+public class WebhookDeliveriesAction implements WebhooksWsAction {
+
+  private static final String COMPONENT_PARAM = "componentKey";
+  private static final String TASK_PARAM = "ceTaskId";
+
+  private final DbClient dbClient;
+  private final UserSession userSession;
+  private final ComponentFinder componentFinder;
+
+  public WebhookDeliveriesAction(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("deliveries")
+      .setSince("6.2")
+      .setDescription("Get the recent deliveries for a specified project or Compute Engine task.<br/>" +
+        "Require 'Administer System' permission.<br/>" +
+        "Note that additional information are returned by api/webhooks/delivery.")
+      .setResponseExample(Resources.getResource(this.getClass(), "example-deliveries.json"))
+      .setInternal(true)
+      .setHandler(this);
+
+    action.createParam(COMPONENT_PARAM)
+      .setDescription("Key of the project")
+      .setExampleValue("my-project");
+
+    action.createParam(TASK_PARAM)
+      .setDescription("Id of the Compute Engine task")
+      .setExampleValue(Uuids.UUID_EXAMPLE_01);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    // fail-fast if not logged in
+    userSession.checkLoggedIn();
+
+    String ceTaskId = request.param(TASK_PARAM);
+    String componentKey = request.param(COMPONENT_PARAM);
+    checkArgument(ceTaskId != null ^ componentKey != null, "Either parameter '%s' or '%s' must be defined", TASK_PARAM, COMPONENT_PARAM);
+
+    Data data = loadFromDatabase(ceTaskId, componentKey);
+    data.ensureAdminPermission(userSession);
+    data.writeTo(request, response);
+  }
+
+  private Data loadFromDatabase(@Nullable String ceTaskId, @Nullable String componentKey) {
+    ComponentDto component = null;
+    List<WebhookDeliveryLiteDto> deliveries;
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      if (componentKey != null) {
+        component = componentFinder.getByKey(dbSession, componentKey);
+        deliveries = dbClient.webhookDeliveryDao().selectOrderedByComponentUuid(dbSession, component.uuid());
+      } else {
+        deliveries = dbClient.webhookDeliveryDao().selectOrderedByCeTaskUuid(dbSession, ceTaskId);
+        Optional<String> deliveredComponentUuid = deliveries
+          .stream()
+          .map(WebhookDeliveryLiteDto::getComponentUuid)
+          .findFirst();
+        if (deliveredComponentUuid.isPresent()) {
+          component = componentFinder.getByUuid(dbSession, deliveredComponentUuid.get());
+        }
+      }
+    }
+    return new Data(component, deliveries);
+  }
+
+  private static class Data {
+    private final ComponentDto component;
+    private final List<WebhookDeliveryLiteDto> deliveries;
+
+    Data(@Nullable ComponentDto component, List<WebhookDeliveryLiteDto> deliveries) {
+      this.deliveries = deliveries;
+      if (deliveries.isEmpty()) {
+        this.component = null;
+      } else {
+        this.component = requireNonNull(component);
+      }
+    }
+
+    void ensureAdminPermission(UserSession userSession) {
+      if (component != null) {
+        userSession.checkComponentUuidPermission(UserRole.ADMIN, component.uuid());
+      }
+    }
+
+    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());
+        }
+        responseBuilder.addDeliveries(deliveryBuilder);
+      }
+      writeProtobuf(responseBuilder.build(), request, response);
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWs.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWs.java
new file mode 100644 (file)
index 0000000..adfae10
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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.api.server.ws.WebService;
+
+public class WebhooksWs implements WebService {
+
+  public static final String API_ENDPOINT = "api/webhooks";
+
+  private final WebhooksWsAction[] actions;
+
+  public WebhooksWs(WebhooksWsAction... actions) {
+    this.actions = actions;
+  }
+
+  @Override
+  public void define(Context context) {
+    NewController controller = context.createController(API_ENDPOINT);
+    controller.setDescription("Webhooks allow to notify external services when a project analysis is done");
+    controller.setSince("6.2");
+    for (WebhooksWsAction action : actions) {
+      action.define(controller);
+    }
+    controller.done();
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsAction.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsAction.java
new file mode 100644 (file)
index 0000000..3857707
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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.server.ws.WsAction;
+
+interface WebhooksWsAction extends WsAction {
+  // Marker interface
+}
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
new file mode 100644 (file)
index 0000000..9ac075d
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 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.core.platform.Module;
+
+public class WebhooksWsModule extends Module {
+  @Override
+  protected void configureModule() {
+    add(
+      WebhooksWs.class,
+      WebhookDeliveriesAction.class);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/package-info.java
new file mode 100644 (file)
index 0000000..faba587
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.webhook.ws;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/webhook/ws/example-deliveries.json b/server/sonar-server/src/main/resources/org/sonar/server/webhook/ws/example-deliveries.json
new file mode 100644 (file)
index 0000000..129d9db
--- /dev/null
@@ -0,0 +1,15 @@
+{
+  "deliveries": [
+    {
+      "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
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveriesActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveriesActionTest.java
new file mode 100644 (file)
index 0000000..e758995
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * 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.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 WebhookDeliveriesActionTest {
+
+  @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);
+    WebhookDeliveriesAction underTest = new WebhookDeliveriesAction(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("componentKey", "ceTaskId");
+    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 search_by_component_and_return_no_records() throws Exception {
+    userSession.login().addProjectUuidPermissions(project.uuid(), UserRole.ADMIN);
+
+    Webhooks.DeliveriesWsResponse response = Webhooks.DeliveriesWsResponse.parseFrom(ws.newRequest()
+      .setMediaType(MediaTypes.PROTOBUF)
+      .setParam("componentKey", project.getKey())
+      .execute()
+      .getInputStream());
+
+    assertThat(response.getDeliveriesCount()).isEqualTo(0);
+  }
+
+  @Test
+  public void search_by_task_and_return_no_records() throws Exception {
+    userSession.login().addProjectUuidPermissions(project.uuid(), UserRole.ADMIN);
+
+    Webhooks.DeliveriesWsResponse response = Webhooks.DeliveriesWsResponse.parseFrom(ws.newRequest()
+      .setMediaType(MediaTypes.PROTOBUF)
+      .setParam("ceTaskId", "t1")
+      .execute()
+      .getInputStream());
+
+    assertThat(response.getDeliveriesCount()).isEqualTo(0);
+  }
+
+  @Test
+  public void search_by_component_and_return_records_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);
+    dbClient.webhookDeliveryDao().insert(db.getSession(), dto);
+    db.commit();
+    userSession.login().addProjectUuidPermissions(UserRole.ADMIN, project.uuid());
+
+    String json = ws.newRequest()
+      .setParam("componentKey", project.getKey())
+      .execute()
+      .getInput();
+
+    assertJson(json).isSimilarTo(ws.getDef().responseExampleAsString());
+  }
+
+  @Test
+  public void search_by_task_and_return_records() throws Exception {
+    WebhookDeliveryDto dto1 = newWebhookDeliveryDto().setComponentUuid(project.uuid()).setCeTaskUuid("t1");
+    WebhookDeliveryDto dto2 = newWebhookDeliveryDto().setComponentUuid(project.uuid()).setCeTaskUuid("t1");
+    WebhookDeliveryDto dto3 = newWebhookDeliveryDto().setComponentUuid(project.uuid()).setCeTaskUuid("t2");
+    dbClient.webhookDeliveryDao().insert(db.getSession(), dto1);
+    dbClient.webhookDeliveryDao().insert(db.getSession(), dto2);
+    dbClient.webhookDeliveryDao().insert(db.getSession(), dto3);
+    db.commit();
+    userSession.login().addProjectUuidPermissions(UserRole.ADMIN, project.uuid());
+
+    Webhooks.DeliveriesWsResponse response = Webhooks.DeliveriesWsResponse.parseFrom(ws.newRequest()
+      .setMediaType(MediaTypes.PROTOBUF)
+      .setParam("ceTaskId", "t1")
+      .execute()
+      .getInputStream());
+    assertThat(response.getDeliveriesCount()).isEqualTo(2);
+    assertThat(response.getDeliveriesList()).extracting(Webhooks.Delivery::getId).containsOnly(dto1.getUuid(), dto2.getUuid());
+  }
+
+  @Test
+  public void search_by_component_and_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("componentKey", project.getKey())
+      .execute();
+  }
+
+  @Test
+  public void search_by_task_and_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("ceTaskId", dto.getCeTaskUuid())
+      .execute();
+  }
+
+  @Test
+  public void throw_IAE_if_both_component_and_task_parameters_are_set() throws Exception {
+    userSession.login();
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Either parameter 'ceTaskId' or 'componentKey' must be defined");
+
+    ws.newRequest()
+      .setParam("componentKey", project.getKey())
+      .setParam("ceTaskId", "t1")
+      .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
new file mode 100644 (file)
index 0000000..24a1436
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class WebhooksWsModuleTest {
+
+  @Test
+  public void verify_count_of_added_components() {
+    ComponentContainer container = new ComponentContainer();
+    new WebhooksWsModule().configure(container);
+    assertThat(container.size()).isEqualTo(2 + 2);
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhooksWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhooksWsTest.java
new file mode 100644 (file)
index 0000000..a49abbc
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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.Test;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.db.DbClient;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.user.UserSession;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class WebhooksWsTest {
+
+  @Test
+  public void test_definition() {
+    WebhooksWsAction action = newFakeAction();
+    WebhooksWs underTest = new WebhooksWs(action);
+
+    WebService.Context context = new WebService.Context();
+    underTest.define(context);
+
+    WebService.Controller controller = context.controller("api/webhooks");
+    assertThat(controller).isNotNull();
+    assertThat(controller.description()).isNotEmpty();
+    assertThat(controller.since()).isEqualTo("6.2");
+  }
+
+  private static WebhooksWsAction newFakeAction() {
+    return new WebhookDeliveriesAction(mock(DbClient.class), mock(UserSession.class), mock(ComponentFinder.class));
+  }
+
+}
index 69af1576d22a088af7ac105a41039a363975e11b..ec444d3cb6fe140b3268e5139b5f461d78eb67bd 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.db.webhook;
 
+import java.util.List;
 import java.util.Optional;
 import org.sonar.db.Dao;
 import org.sonar.db.DbSession;
@@ -28,6 +29,21 @@ public class WebhookDeliveryDao implements Dao {
   public Optional<WebhookDeliveryDto> selectByUuid(DbSession dbSession, String uuid) {
     return Optional.ofNullable(mapper(dbSession).selectByUuid(uuid));
   }
+
+  /**
+   * All the deliveries for the specified component. Results are ordered by descending date.
+   */
+  public List<WebhookDeliveryLiteDto> selectOrderedByComponentUuid(DbSession dbSession, String componentUuid) {
+    return mapper(dbSession).selectOrderedByComponentUuid(componentUuid);
+  }
+
+  /**
+   * All the deliveries for the specified CE task. Results are ordered by descending date.
+   */
+  public List<WebhookDeliveryLiteDto> selectOrderedByCeTaskUuid(DbSession dbSession, String ceTaskUuid) {
+    return mapper(dbSession).selectOrderedByCeTaskUuid(ceTaskUuid);
+  }
+
   public void insert(DbSession dbSession, WebhookDeliveryDto dto) {
     mapper(dbSession).insert(dto);
   }
index 0d39cc3bfe0e15d2d1d4d40d1548f672cfecadc6..c89dbc767d85d475aeca605d8c4faf953022059d 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.db.webhook;
 
+import java.util.List;
 import javax.annotation.CheckForNull;
 import org.apache.ibatis.annotations.Param;
 
@@ -27,6 +28,10 @@ public interface WebhookDeliveryMapper {
   @CheckForNull
   WebhookDeliveryDto selectByUuid(@Param("uuid") String uuid);
 
+  List<WebhookDeliveryLiteDto> selectOrderedByComponentUuid(@Param("componentUuid") String componentUuid);
+
+  List<WebhookDeliveryLiteDto> selectOrderedByCeTaskUuid(@Param("ceTaskUuid") String ceTaskUuid);
+
   void insert(WebhookDeliveryDto dto);
 
   void deleteComponentBeforeDate(@Param("componentUuid") String componentUuid, @Param("beforeDate") long beforeDate);
index ac90c3210450a500e193bdba29dd6b45b3e5822e..dc178b7d4d1bfec26adf6d79cbcbeb31dc681b83 100644 (file)
@@ -4,8 +4,7 @@
 
 <mapper namespace="org.sonar.db.webhook.WebhookDeliveryMapper">
 
-  <select id="selectByUuid" parameterType="String" resultType="org.sonar.db.webhook.WebhookDeliveryDto">
-    select
+  <sql id="sqlLiteColumns">
     uuid,
     component_uuid as componentUuid,
     ce_task_uuid as ceTaskUuid,
     success,
     http_status as httpStatus,
     duration_ms as durationMs,
-    payload,
-    error_stacktrace as errorStacktrace,
     created_at as createdAt
+  </sql>
+
+  <select id="selectByUuid" parameterType="String" resultType="org.sonar.db.webhook.WebhookDeliveryDto">
+    select
+    <include refid="sqlLiteColumns" />,
+    payload,
+    error_stacktrace as errorStacktrace
     from webhook_deliveries
     where uuid = #{uuid,jdbcType=VARCHAR}
   </select>
 
+  <select id="selectOrderedByComponentUuid" parameterType="String" resultType="org.sonar.db.webhook.WebhookDeliveryLiteDto">
+    select <include refid="sqlLiteColumns" />
+    from webhook_deliveries
+    where component_uuid = #{componentUuid,jdbcType=VARCHAR}
+    order by created_at desc
+  </select>
+
+  <select id="selectOrderedByCeTaskUuid" parameterType="String" resultType="org.sonar.db.webhook.WebhookDeliveryLiteDto">
+    select <include refid="sqlLiteColumns" />
+    from webhook_deliveries
+    where ce_task_uuid = #{ceTaskUuid,jdbcType=VARCHAR}
+    order by created_at desc
+  </select>
+
   <insert id="insert" parameterType="org.sonar.db.webhook.WebhookDeliveryDto" useGeneratedKeys="false">
     insert into webhook_deliveries (
     uuid,
index 8445f7f69b6b748a18c60585a6ed18b0f6476bee..17cc45b046a2c52e802dbc95ad2e5d9868508629 100644 (file)
@@ -35,7 +35,8 @@ import static org.sonar.db.webhook.WebhookDbTesting.newWebhookDeliveryDto;
 
 public class WebhookDeliveryDaoTest {
 
-  private static final long DATE_1 = 1_999_000L;
+  private static final long NOW = 1_500_000_000L;
+  private static final long BEFORE = NOW - 1_000L;
 
   @Rule
   public final DbTester dbTester = DbTester.create(System2.INSTANCE).setDisableDefaultOrganization(true);
@@ -52,6 +53,52 @@ public class WebhookDeliveryDaoTest {
     assertThat(underTest.selectByUuid(dbSession, "missing")).isEmpty();
   }
 
+  @Test
+  public void selectOrderedByComponentUuid_returns_empty_if_no_records() {
+    underTest.insert(dbSession, newDto("D1", "COMPONENT_1", "TASK_1"));
+
+    List<WebhookDeliveryLiteDto> deliveries = underTest.selectOrderedByComponentUuid(dbSession, "ANOTHER_COMPONENT");
+
+    assertThat(deliveries).isEmpty();
+  }
+
+  @Test
+  public void selectOrderedByComponentUuid_returns_records_ordered_by_date() {
+    WebhookDeliveryDto dto1 = newDto("D1", "COMPONENT_1", "TASK_1").setCreatedAt(BEFORE);
+    WebhookDeliveryDto dto2 = newDto("D2", "COMPONENT_1", "TASK_1").setCreatedAt(NOW);
+    WebhookDeliveryDto dto3 = newDto("D3", "COMPONENT_2", "TASK_1").setCreatedAt(NOW);
+    underTest.insert(dbSession, dto3);
+    underTest.insert(dbSession, dto2);
+    underTest.insert(dbSession, dto1);
+
+    List<WebhookDeliveryLiteDto> deliveries = underTest.selectOrderedByComponentUuid(dbSession, "COMPONENT_1");
+
+    assertThat(deliveries).extracting(WebhookDeliveryLiteDto::getUuid).containsExactly("D2", "D1");
+  }
+
+  @Test
+  public void selectOrderedByCeTaskUuid_returns_empty_if_no_records() {
+    underTest.insert(dbSession, newDto("D1", "COMPONENT_1", "TASK_1"));
+
+    List<WebhookDeliveryLiteDto> deliveries = underTest.selectOrderedByCeTaskUuid(dbSession, "ANOTHER_TASK");
+
+    assertThat(deliveries).isEmpty();
+  }
+
+  @Test
+  public void selectOrderedByCeTaskUuid_returns_records_ordered_by_date() {
+    WebhookDeliveryDto dto1 = newDto("D1", "COMPONENT_1", "TASK_1").setCreatedAt(BEFORE);
+    WebhookDeliveryDto dto2 = newDto("D2", "COMPONENT_1", "TASK_1").setCreatedAt(NOW);
+    WebhookDeliveryDto dto3 = newDto("D3", "COMPONENT_2", "TASK_2").setCreatedAt(NOW);
+    underTest.insert(dbSession, dto3);
+    underTest.insert(dbSession, dto2);
+    underTest.insert(dbSession, dto1);
+
+    List<WebhookDeliveryLiteDto> deliveries = underTest.selectOrderedByCeTaskUuid(dbSession, "TASK_1");
+
+    assertThat(deliveries).extracting(WebhookDeliveryLiteDto::getUuid).containsExactly("D2", "D1");
+  }
+
   @Test
   public void insert_row_with_only_mandatory_columns() {
     WebhookDeliveryDto dto = newDto("DELIVERY_1", "COMPONENT_1", "TASK_1")
index eff30072e0df4339835baed378442613ac6d0460..e75239fd506cf615375c6a4c3a1625c0add9b6df 100644 (file)
@@ -34,6 +34,7 @@ import org.sonarqube.ws.client.rule.RulesService;
 import org.sonarqube.ws.client.setting.SettingsService;
 import org.sonarqube.ws.client.system.SystemService;
 import org.sonarqube.ws.client.usertoken.UserTokensService;
+import org.sonarqube.ws.client.webhook.WebhooksService;
 
 /**
  * This class is not public anymore since version 5.5. It is
@@ -59,6 +60,7 @@ class DefaultWsClient implements WsClient {
   private final ProjectLinksService projectLinksService;
   private final SettingsService settingsService;
   private final RootsService rootsService;
+  private final WebhooksService webhooksService;
 
   DefaultWsClient(WsConnector wsConnector) {
     this.wsConnector = wsConnector;
@@ -77,6 +79,7 @@ class DefaultWsClient implements WsClient {
     this.projectLinksService = new ProjectLinksService(wsConnector);
     this.settingsService = new SettingsService(wsConnector);
     this.rootsService = new RootsService(wsConnector);
+    this.webhooksService = new WebhooksService(wsConnector);
   }
 
   @Override
@@ -158,4 +161,9 @@ class DefaultWsClient implements WsClient {
   public RootsService rootService() {
     return rootsService;
   }
+
+  @Override
+  public WebhooksService webhooks() {
+    return webhooksService;
+  }
 }
index add8512bca2aebce5c749000b0501a9a9bab4e41..d2317c1627047b55557ccdae2ca243befcee552d 100644 (file)
@@ -34,6 +34,7 @@ import org.sonarqube.ws.client.rule.RulesService;
 import org.sonarqube.ws.client.setting.SettingsService;
 import org.sonarqube.ws.client.system.SystemService;
 import org.sonarqube.ws.client.usertoken.UserTokensService;
+import org.sonarqube.ws.client.webhook.WebhooksService;
 
 /**
  * Allows to request the web services of SonarQube server. Instance is provided by
@@ -97,4 +98,9 @@ public interface WsClient {
    * @since 6.2
    */
   RootsService rootService();
+
+  /**
+   * @since 6.2
+   */
+  WebhooksService webhooks();
 }
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/DeliveriesRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/DeliveriesRequest.java
new file mode 100644 (file)
index 0000000..af9ad99
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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.webhook;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public class DeliveriesRequest {
+
+  private final String componentKey;
+  private final String ceTaskId;
+
+  private DeliveriesRequest(Builder builder) {
+    this.componentKey = builder.componentKey;
+    this.ceTaskId = builder.ceTaskId;
+  }
+
+  @CheckForNull
+  public String getComponentKey() {
+    return componentKey;
+  }
+
+  @CheckForNull
+  public String getCeTaskId() {
+    return ceTaskId;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private String componentKey;
+    private String ceTaskId;
+
+    /**
+     * @see #builder()
+     */
+    private Builder() {
+    }
+
+    public Builder setComponentKey(@Nullable String s) {
+      this.componentKey = s;
+      return this;
+    }
+
+    public Builder setCeTaskId(@Nullable String s) {
+      this.ceTaskId = s;
+      return this;
+    }
+
+    public DeliveriesRequest build() {
+      return new DeliveriesRequest(this);
+    }
+  }
+}
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
new file mode 100644 (file)
index 0000000..67ee10a
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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.webhook;
+
+import org.sonarqube.ws.Webhooks;
+import org.sonarqube.ws.client.BaseService;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsConnector;
+
+public class WebhooksService extends BaseService {
+
+  public WebhooksService(WsConnector wsConnector) {
+    super(wsConnector, "api/webhooks");
+  }
+
+  /**
+   * @throws org.sonarqube.ws.client.HttpException if HTTP status code is not 2xx.
+   */
+  public Webhooks.DeliveriesWsResponse deliveries(DeliveriesRequest request) {
+    GetRequest httpRequest = new GetRequest(path("deliveries"))
+      .setParam("componentKey", request.getComponentKey())
+      .setParam("ceTaskId", request.getCeTaskId());
+    return call(httpRequest, Webhooks.DeliveriesWsResponse.parser());
+  }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/package-info.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/package-info.java
new file mode 100644 (file)
index 0000000..c613c4a
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonarqube.ws.client.webhook;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-ws/src/main/protobuf/ws-webhooks.proto b/sonar-ws/src/main/protobuf/ws-webhooks.proto
new file mode 100644 (file)
index 0000000..4ef519c
--- /dev/null
@@ -0,0 +1,42 @@
+// SonarQube, open source software quality management tool.
+// Copyright (C) 2008-2016 SonarSource
+// mailto:contact AT sonarsource DOT com
+//
+// SonarQube 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.
+//
+// SonarQube 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.
+
+syntax = "proto2";
+
+package sonarqube.ws.webhooks;
+
+option java_package = "org.sonarqube.ws";
+option java_outer_classname = "Webhooks";
+option optimize_for = SPEED;
+
+// WS api/webhooks/deliveries
+message DeliveriesWsResponse {
+  repeated Delivery deliveries = 1;
+}
+
+message Delivery {
+  optional string id = 1;
+  optional string componentKey = 2;
+  optional string ceTaskId = 3;
+  optional string name = 4;
+  optional string url = 5;
+  optional string at = 6;
+  optional bool success = 7;
+  optional int32 httpStatus = 8;
+  optional int32 durationMs = 9;
+}