From: Simon Brandhof Date: Thu, 10 Nov 2016 17:28:57 +0000 (+0100) Subject: SONAR-8353 add WS api/webhooks/deliveries X-Git-Tag: 6.2-RC1~104 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=976aa63a8ee9bf1df31eb23d5085a67074f0d5e7;p=sonarqube.git SONAR-8353 add WS api/webhooks/deliveries --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 594d98a044e..def3adbf166 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -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 index 00000000000..8a4bc4f388d --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookDeliveriesAction.java @@ -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.
" + + "Require 'Administer System' permission.
" + + "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 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 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 deliveries; + + Data(@Nullable ComponentDto component, List 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 index 00000000000..adfae105259 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWs.java @@ -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 index 00000000000..3857707f995 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsAction.java @@ -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 index 00000000000..9ac075dfa65 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java @@ -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 index 00000000000..faba58796ed --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/package-info.java @@ -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 index 00000000000..129d9dbfc21 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/webhook/ws/example-deliveries.json @@ -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 index 00000000000..e75899558df --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveriesActionTest.java @@ -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 index 00000000000..24a14364391 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhooksWsModuleTest.java @@ -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 index 00000000000..a49abbc1179 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhooksWsTest.java @@ -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)); + } + +} diff --git a/sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryDao.java b/sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryDao.java index 69af1576d22..ec444d3cb6f 100644 --- a/sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryDao.java +++ b/sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryDao.java @@ -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 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 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 selectOrderedByCeTaskUuid(DbSession dbSession, String ceTaskUuid) { + return mapper(dbSession).selectOrderedByCeTaskUuid(ceTaskUuid); + } + public void insert(DbSession dbSession, WebhookDeliveryDto dto) { mapper(dbSession).insert(dto); } diff --git a/sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryMapper.java b/sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryMapper.java index 0d39cc3bfe0..c89dbc767d8 100644 --- a/sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryMapper.java +++ b/sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryMapper.java @@ -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 selectOrderedByComponentUuid(@Param("componentUuid") String componentUuid); + + List selectOrderedByCeTaskUuid(@Param("ceTaskUuid") String ceTaskUuid); + void insert(WebhookDeliveryDto dto); void deleteComponentBeforeDate(@Param("componentUuid") String componentUuid, @Param("beforeDate") long beforeDate); diff --git a/sonar-db/src/main/resources/org/sonar/db/webhook/WebhookDeliveryMapper.xml b/sonar-db/src/main/resources/org/sonar/db/webhook/WebhookDeliveryMapper.xml index ac90c321045..dc178b7d4d1 100644 --- a/sonar-db/src/main/resources/org/sonar/db/webhook/WebhookDeliveryMapper.xml +++ b/sonar-db/src/main/resources/org/sonar/db/webhook/WebhookDeliveryMapper.xml @@ -4,8 +4,7 @@ - + select + , + payload, + error_stacktrace as errorStacktrace from webhook_deliveries where uuid = #{uuid,jdbcType=VARCHAR} + + + + insert into webhook_deliveries ( uuid, diff --git a/sonar-db/src/test/java/org/sonar/db/webhook/WebhookDeliveryDaoTest.java b/sonar-db/src/test/java/org/sonar/db/webhook/WebhookDeliveryDaoTest.java index 8445f7f69b6..17cc45b046a 100644 --- a/sonar-db/src/test/java/org/sonar/db/webhook/WebhookDeliveryDaoTest.java +++ b/sonar-db/src/test/java/org/sonar/db/webhook/WebhookDeliveryDaoTest.java @@ -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 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 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 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 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") diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java index eff30072e0d..e75239fd506 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java @@ -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; + } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java index add8512bca2..d2317c16270 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java @@ -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 index 00000000000..af9ad99940c --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/DeliveriesRequest.java @@ -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 index 00000000000..67ee10aaf57 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/WebhooksService.java @@ -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 index 00000000000..c613c4a612f --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/webhook/package-info.java @@ -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 index 00000000000..4ef519c3b9b --- /dev/null +++ b/sonar-ws/src/main/protobuf/ws-webhooks.proto @@ -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; +}