]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8349 add integration tests
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Fri, 11 Nov 2016 10:48:23 +0000 (11:48 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Mon, 14 Nov 2016 11:18:51 +0000 (12:18 +0100)
it/it-tests/src/test/java/it/Category3Suite.java
it/it-tests/src/test/java/it/webhook/ExternalServer.java [new file with mode: 0644]
it/it-tests/src/test/java/it/webhook/PayloadRequest.java [new file with mode: 0644]
it/it-tests/src/test/java/it/webhook/WebhooksTest.java [new file with mode: 0644]

index 99f74cc3909c932a208d3090aa65661113ab6db7..198153235d7215dc5e2802f82ad014efdf400795 100644 (file)
@@ -36,6 +36,7 @@ import it.analysis.TempFolderTest;
 import it.measure.DecimalScaleMetricTest;
 import it.organization.OrganizationIt;
 import it.root.RootIt;
+import it.webhook.WebhooksTest;
 import org.junit.ClassRule;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
@@ -64,7 +65,8 @@ import static util.ItUtils.xooPlugin;
   // organization
   OrganizationIt.class,
   // root users
-  RootIt.class
+  RootIt.class,
+  WebhooksTest.class
 })
 public class Category3Suite {
 
diff --git a/it/it-tests/src/test/java/it/webhook/ExternalServer.java b/it/it-tests/src/test/java/it/webhook/ExternalServer.java
new file mode 100644 (file)
index 0000000..2029631
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * 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 it.webhook;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.rules.ExternalResource;
+
+/**
+ * This web server listens to requests sent by webhooks
+ */
+class ExternalServer extends ExternalResource {
+  private final Server jetty;
+  private final List<PayloadRequest> payloads = new ArrayList<>();
+
+  ExternalServer() {
+    jetty = new Server(0);
+    jetty.setHandler(new AbstractHandler() {
+      @Override
+      public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+        throws IOException, ServletException {
+
+        if ("POST".equalsIgnoreCase(request.getMethod())) {
+          String json = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
+          Map<String, String> httpHeaders = new HashMap<>();
+          Enumeration<String> headerNames = request.getHeaderNames();
+          while (headerNames.hasMoreElements()) {
+            String key = headerNames.nextElement();
+            httpHeaders.put(key, request.getHeader(key));
+          }
+          payloads.add(new PayloadRequest(target, httpHeaders, json));
+        }
+
+        response.setStatus(target.equals("/fail") ? 500 : 200);
+        baseRequest.setHandled(true);
+      }
+    });
+  }
+
+  @Override
+  protected void before() throws Throwable {
+    jetty.start();
+  }
+
+  @Override
+  protected void after() {
+    try {
+      jetty.stop();
+    } catch (Exception e) {
+      throw new IllegalStateException("Cannot stop Jetty");
+    }
+  }
+
+  List<PayloadRequest> getPayloadRequests() {
+    return payloads;
+  }
+
+  List<PayloadRequest> getPayloadRequestsOnPath(String path) {
+    return payloads.stream().filter(p -> p.getPath().equals(path)).collect(Collectors.toList());
+  }
+
+  String urlFor(String path) {
+    return jetty.getURI().resolve(path).toString();
+  }
+
+  void clear() {
+    payloads.clear();
+  }
+}
diff --git a/it/it-tests/src/test/java/it/webhook/PayloadRequest.java b/it/it-tests/src/test/java/it/webhook/PayloadRequest.java
new file mode 100644 (file)
index 0000000..e0a1e9c
--- /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 it.webhook;
+
+import java.util.Map;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Request received by {@link ExternalServer}
+ */
+class PayloadRequest {
+  private final String path;
+  private final Map<String, String> httpHeaders;
+  private final String json;
+
+  PayloadRequest(String path, Map<String, String> httpHeaders, String json) {
+    this.path = requireNonNull(path);
+    this.httpHeaders = requireNonNull(httpHeaders);
+    this.json = requireNonNull(json);
+  }
+
+  Map<String, String> getHttpHeaders() {
+    return httpHeaders;
+  }
+
+  String getJson() {
+    return json;
+  }
+
+  String getPath() {
+    return path;
+  }
+}
diff --git a/it/it-tests/src/test/java/it/webhook/WebhooksTest.java b/it/it-tests/src/test/java/it/webhook/WebhooksTest.java
new file mode 100644 (file)
index 0000000..e55390f
--- /dev/null
@@ -0,0 +1,290 @@
+/*
+ * 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 it.webhook;
+
+import com.sonar.orchestrator.Orchestrator;
+import it.Category3Suite;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonarqube.ws.Webhooks;
+import org.sonarqube.ws.client.HttpException;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.project.DeleteRequest;
+import org.sonarqube.ws.client.setting.ResetRequest;
+import org.sonarqube.ws.client.setting.SetRequest;
+import org.sonarqube.ws.client.webhook.DeliveriesRequest;
+import util.ItUtils;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.IntStream.range;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.jsonToMap;
+import static util.ItUtils.runProjectAnalysis;
+
+public class WebhooksTest {
+
+  private static final String PROJECT_KEY = "my-project";
+  private static final String PROJECT_NAME = "My Project";
+  private static final String GLOBAL_WEBHOOK_PROPERTY = "sonar.webhooks.global";
+  private static final String PROJECT_WEBHOOK_PROPERTY = "sonar.webhooks.project";
+
+  @ClassRule
+  public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+  @ClassRule
+  public static ExternalServer externalServer = new ExternalServer();
+
+  private WsClient adminWs = ItUtils.newAdminWsClient(orchestrator);
+
+  @Before
+  public void setUp() throws Exception {
+    externalServer.clear();
+  }
+
+  @Before
+  @After
+  public void reset() throws Exception {
+    disableGlobalWebhooks();
+    try {
+      // delete project and related properties/webhook deliveries
+      adminWs.projects().delete(DeleteRequest.builder().setKey(PROJECT_KEY).build());
+    } catch (HttpException e) {
+      // ignore because project may not exist
+    }
+  }
+
+  @Test
+  public void call_multiple_global_and_project_webhooks_when_analysis_is_done() {
+    orchestrator.getServer().provisionProject(PROJECT_KEY, PROJECT_NAME);
+    enableGlobalWebhooks(
+      new Webhook("Jenkins", externalServer.urlFor("/jenkins")),
+      new Webhook("HipChat", externalServer.urlFor("/hipchat")));
+    enableProjectWebhooks(PROJECT_KEY,
+      new Webhook("Burgr", externalServer.urlFor("/burgr")));
+
+    analyseProject();
+
+    // the same payload has been sent to three servers
+    assertThat(externalServer.getPayloadRequests()).hasSize(3);
+    PayloadRequest request = externalServer.getPayloadRequests().get(0);
+    for (int i = 1; i < 3; i++) {
+      PayloadRequest r = externalServer.getPayloadRequests().get(i);
+      assertThat(request.getJson()).isEqualTo(r.getJson());
+    }
+
+    // verify HTTP headers
+    assertThat(request.getHttpHeaders().get("X-SonarQube-Project")).isEqualTo(PROJECT_KEY);
+
+    // verify content of payload
+    Map<String, Object> payload = jsonToMap(request.getJson());
+    assertThat(payload.get("status")).isEqualTo("SUCCESS");
+    assertThat(payload.get("analysedAt")).isNotNull();
+    Map<String, String> project = (Map<String, String>) payload.get("project");
+    assertThat(project.get("key")).isEqualTo(PROJECT_KEY);
+    assertThat(project.get("name")).isEqualTo(PROJECT_NAME);
+    Map<String, Object> gate = (Map<String, Object>) payload.get("qualityGate");
+    assertThat(gate.get("name")).isEqualTo("SonarQube way");
+    assertThat(gate.get("status")).isEqualTo("OK");
+    assertThat(gate.get("conditions")).isNotNull();
+
+    // verify list of persisted deliveries (api/webhooks/deliveries)
+    List<Webhooks.Delivery> deliveries = getPersistedDeliveries();
+    assertThat(deliveries).hasSize(3);
+    for (Webhooks.Delivery delivery : deliveries) {
+      assertThatPersistedDeliveryIsValid(delivery);
+      assertThat(delivery.getSuccess()).isTrue();
+      assertThat(delivery.getHttpStatus()).isEqualTo(200);
+      assertThat(delivery.getName()).isIn("Jenkins", "HipChat", "Burgr");
+      assertThat(delivery.hasErrorStacktrace()).isFalse();
+      // payload is available only in api/webhooks/delivery to avoid loading multiple DB CLOBs
+      assertThat(delivery.hasPayload()).isFalse();
+    }
+
+    // verify detail of persisted delivery (api/webhooks/delivery)
+    Webhooks.Delivery detail = getDetailOfPersistedDelivery(deliveries.get(0));
+    assertThatPersistedDeliveryIsValid(detail);
+    assertThat(detail.getPayload()).isEqualTo(request.getJson());
+  }
+
+  @Test
+  public void persist_delivery_as_failed_if_external_server_returns_an_error_code() {
+    enableGlobalWebhooks(
+      new Webhook("Fail", externalServer.urlFor("/fail")),
+      new Webhook("HipChat", externalServer.urlFor("/hipchat")));
+
+    analyseProject();
+
+    // all webhooks are called, even if one returns an error code
+    assertThat(externalServer.getPayloadRequests()).hasSize(2);
+
+    // verify persisted deliveries
+    Webhooks.Delivery failedDelivery = getPersistedDeliveryByName("Fail");
+    assertThatPersistedDeliveryIsValid(failedDelivery);
+    assertThat(failedDelivery.getSuccess()).isFalse();
+    assertThat(failedDelivery.getHttpStatus()).isEqualTo(500);
+
+    Webhooks.Delivery successfulDelivery = getPersistedDeliveryByName("HipChat");
+    assertThatPersistedDeliveryIsValid(successfulDelivery);
+    assertThat(successfulDelivery.getSuccess()).isTrue();
+    assertThat(successfulDelivery.getHttpStatus()).isEqualTo(200);
+  }
+
+  /**
+   * Restrict calls to ten webhooks per type (global or project)
+   */
+  @Test
+  public void do_not_become_a_denial_of_service_attacker() {
+    orchestrator.getServer().provisionProject(PROJECT_KEY, PROJECT_NAME);
+
+    List<Webhook> globalWebhooks = range(0, 15).mapToObj(i -> new Webhook("G" + i, externalServer.urlFor("/global"))).collect(Collectors.toList());
+    enableGlobalWebhooks(globalWebhooks.toArray(new Webhook[globalWebhooks.size()]));
+    List<Webhook> projectWebhooks = range(0, 15).mapToObj(i -> new Webhook("P" + i, externalServer.urlFor("/project"))).collect(Collectors.toList());
+    enableProjectWebhooks(PROJECT_KEY, projectWebhooks.toArray(new Webhook[projectWebhooks.size()]));
+
+    analyseProject();
+
+    // only the first ten global webhooks and ten project webhooks are called
+    assertThat(externalServer.getPayloadRequests()).hasSize(10 + 10);
+    assertThat(externalServer.getPayloadRequestsOnPath("/global")).hasSize(10);
+    assertThat(externalServer.getPayloadRequestsOnPath("/project")).hasSize(10);
+
+    // verify persisted deliveries
+    assertThat(getPersistedDeliveries()).hasSize(10 + 10);
+  }
+
+  @Test
+  public void persist_delivery_as_failed_if_webhook_url_is_malformed() {
+    enableGlobalWebhooks(new Webhook("Jenkins", "this_is_not_an_url"));
+
+    analyseProject();
+
+    assertThat(externalServer.getPayloadRequests()).isEmpty();
+
+    // verify persisted deliveries
+    Webhooks.Delivery delivery = getPersistedDeliveryByName("Jenkins");
+    Webhooks.Delivery detail = getDetailOfPersistedDelivery(delivery);
+
+    assertThat(detail.getSuccess()).isFalse();
+    assertThat(detail.hasHttpStatus()).isFalse();
+    assertThat(detail.hasDurationMs()).isFalse();
+    assertThat(detail.getPayload()).isNotEmpty();
+    assertThat(detail.getErrorStacktrace())
+      .contains("java.lang.IllegalArgumentException")
+      .contains("unexpected url")
+      .contains("this_is_not_an_url");
+  }
+
+  @Test
+  public void ignore_webhook_if_url_is_missing() {
+    // property sets, as used to define webhooks, do
+    // not allow to validate values yet
+    enableGlobalWebhooks(new Webhook("Jenkins", null));
+
+    analyseProject();
+
+    assertThat(externalServer.getPayloadRequests()).isEmpty();
+    assertThat(getPersistedDeliveries()).isEmpty();
+  }
+
+  private void analyseProject() {
+    runProjectAnalysis(orchestrator, "shared/xoo-sample",
+      "sonar.projectKey", PROJECT_KEY,
+      "sonar.projectName", PROJECT_NAME);
+  }
+
+  private List<Webhooks.Delivery> getPersistedDeliveries() {
+    DeliveriesRequest deliveriesReq = DeliveriesRequest.builder().setComponentKey(PROJECT_KEY).build();
+    return adminWs.webhooks().deliveries(deliveriesReq).getDeliveriesList();
+  }
+
+  private Webhooks.Delivery getPersistedDeliveryByName(String webhookName) {
+    List<Webhooks.Delivery> deliveries = getPersistedDeliveries();
+    return deliveries.stream().filter(d -> d.getName().equals(webhookName)).findFirst().get();
+  }
+
+  private Webhooks.Delivery getDetailOfPersistedDelivery(Webhooks.Delivery delivery) {
+    Webhooks.Delivery detail = adminWs.webhooks().delivery(delivery.getId()).getDelivery();
+    return requireNonNull(detail);
+  }
+
+  private void assertThatPersistedDeliveryIsValid(Webhooks.Delivery delivery) {
+    assertThat(delivery.getId()).isNotEmpty();
+    assertThat(delivery.getName()).isNotEmpty();
+    assertThat(delivery.hasSuccess()).isTrue();
+    assertThat(delivery.getHttpStatus()).isGreaterThanOrEqualTo(200);
+    assertThat(delivery.getDurationMs()).isGreaterThanOrEqualTo(0);
+    assertThat(delivery.getAt()).isNotEmpty();
+    assertThat(delivery.getComponentKey()).isEqualTo(PROJECT_KEY);
+    assertThat(delivery.getUrl()).startsWith(externalServer.urlFor("/"));
+  }
+
+  private void enableGlobalWebhooks(Webhook... webhooks) {
+    enableWebhooks(null, GLOBAL_WEBHOOK_PROPERTY, webhooks);
+  }
+
+  private void enableProjectWebhooks(String projectKey, Webhook... webhooks) {
+    enableWebhooks(projectKey, PROJECT_WEBHOOK_PROPERTY, webhooks);
+  }
+
+  private void enableWebhooks(@Nullable String projectKey, String property, Webhook... webhooks) {
+    List<String> webhookIds = new ArrayList<>();
+    for (int i = 0; i < webhooks.length; i++) {
+      Webhook webhook = webhooks[i];
+      String id = String.valueOf(i + 1);
+      webhookIds.add(id);
+      setProperty(projectKey, property + "." + id + ".name", webhook.name);
+      setProperty(projectKey, property + "." + id + ".url", webhook.url);
+    }
+    setProperty(projectKey, property, StringUtils.join(webhookIds, ","));
+  }
+
+  private void disableGlobalWebhooks() {
+    setProperty(null, GLOBAL_WEBHOOK_PROPERTY, null);
+  }
+
+  private void setProperty(@Nullable String componentKey, String key, @Nullable String value) {
+    if (value == null) {
+      ResetRequest req = ResetRequest.builder().setKeys(key).setComponentKey(componentKey).build();
+      adminWs.settingsService().reset(req);
+    } else {
+      SetRequest req = SetRequest.builder().setKey(key).setValue(value).setComponentKey(componentKey).build();
+      adminWs.settingsService().set(req);
+    }
+  }
+
+  private static class Webhook {
+    private final String name;
+    private final String url;
+
+    Webhook(@Nullable String name, @Nullable String url) {
+      this.name = name;
+      this.url = url;
+    }
+  }
+
+}