return rootSession.qGates();
}
+ @Override
+ public WebhookTester webhooks() {
+ return rootSession.webhooks();
+ }
+
private static class TesterSessionImpl implements TesterSession {
private final WsClient client;
public QGateTester qGates() {
return new QGateTester(this);
}
+
+ @Override
+ public WebhookTester webhooks() {
+ return new WebhookTester(this);
+ }
}
}
QGateTester qGates();
+ WebhookTester webhooks();
+
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.qa.util;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+import org.sonarqube.ws.Organizations.Organization;
+import org.sonarqube.ws.Projects.CreateWsResponse.Project;
+import org.sonarqube.ws.Webhooks.CreateWsResponse.Webhook;
+import org.sonarqube.ws.Webhooks.Delivery;
+import org.sonarqube.ws.client.webhooks.CreateRequest;
+import org.sonarqube.ws.client.webhooks.DeleteRequest;
+import org.sonarqube.ws.client.webhooks.DeliveriesRequest;
+import org.sonarqube.ws.client.webhooks.DeliveryRequest;
+import org.sonarqube.ws.client.webhooks.ListRequest;
+import org.sonarqube.ws.client.webhooks.WebhooksService;
+
+import static java.util.Arrays.stream;
+import static java.util.Objects.requireNonNull;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class WebhookTester {
+ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
+
+ private final TesterSession session;
+
+ WebhookTester(TesterSession session) {
+ this.session = session;
+ }
+
+ public WebhooksService service() {
+ return session.wsClient().webhooks();
+ }
+
+ public Webhook generate(Consumer<CreateRequest>... populators) {
+ return generate(null, null, populators);
+ }
+
+ public Webhook generate(Organization organization, Consumer<CreateRequest>... populators) {
+ return generate(organization, null, populators);
+ }
+
+ public Webhook generate(Project project, Consumer<CreateRequest>... populators) {
+ return generate(null, project, populators);
+ }
+
+ @SafeVarargs
+ public final Webhook generate(
+ @Nullable Organization organization,
+ @Nullable Project project,
+ Consumer<CreateRequest>... populators
+ ) {
+ int id = ID_GENERATOR.getAndIncrement();
+ CreateRequest request = new CreateRequest()
+ .setName("Webhook " + id)
+ .setUrl("https://webhook-" + id)
+ .setProject(project != null ? project.getKey(): null)
+ .setOrganization(organization != null ? organization.getKey() : null);
+ stream(populators).forEach(p -> p.accept(request));
+ return service().create(request).getWebhook();
+ }
+
+ public void deleteAllGlobal() {
+ service().list(new ListRequest()).getWebhooksList().forEach(p ->
+ service().delete(new DeleteRequest().setWebhook(p.getKey()))
+ );
+ }
+
+ public List<Delivery> getPersistedDeliveries(Project project) {
+ DeliveriesRequest deliveriesReq = new DeliveriesRequest().setComponentKey(project.getKey());
+ return service().deliveries(deliveriesReq).getDeliveriesList();
+ }
+
+ public Delivery getPersistedDeliveryByName(Project project, String webhookName) {
+ List<Delivery> deliveries = getPersistedDeliveries(project);
+ Optional<Delivery> delivery = deliveries.stream().filter(d -> d.getName().equals(webhookName)).findFirst();
+ assertThat(delivery).isPresent();
+ return delivery.get();
+ }
+
+ public Delivery getDetailOfPersistedDelivery(Delivery delivery) {
+ Delivery detail = service().delivery(new DeliveryRequest().setDeliveryId(delivery.getId())).getDelivery();
+ return requireNonNull(detail);
+ }
+
+ public void assertThatPersistedDeliveryIsValid(Delivery delivery, @Nullable Project project, @Nullable String url) {
+ 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();
+ if (project != null) {
+ assertThat(delivery.getComponentKey()).isEqualTo(project.getKey());
+ }
+ if (url != null) {
+ assertThat(delivery.getUrl()).startsWith(url);
+ }
+ }
+}
package org.sonarqube.qa.util.pageobjects;
import com.codeborne.selenide.ElementsCollection;
+import com.codeborne.selenide.SelenideElement;
+import static com.codeborne.selenide.Condition.cssClass;
+import static com.codeborne.selenide.Condition.enabled;
import static com.codeborne.selenide.Condition.exist;
import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.$$;
return this;
}
+ public WebhooksPage hasNoWebhooks() {
+ $(".boxed-group").shouldHave(text("No webhook defined"));
+ return this;
+ }
+
public WebhooksPage countWebhooks(Integer number) {
getWebhooks().shouldHaveSize(number);
return this;
}
+ public WebhooksPage createWebhook(String name, String url) {
+ $(".js-webhook-create").shouldBe(visible).shouldBe(enabled).shouldNotHave(cssClass("disabled")).click();
+ modalShouldBeOpen("Create Webhook");
+ $("#webhook-name").shouldBe(visible).sendKeys(name);
+ $("#webhook-url").shouldBe(visible).sendKeys(url);
+ $("button[type='submit']").shouldBe(visible).click();
+ modalShouldBeClosed();
+ return this;
+ }
+
+ public WebhooksPage createIsDisabled() {
+ $(".js-webhook-create").shouldBe(visible).shouldHave(cssClass("disabled")).click();
+ modalShouldBeClosed();
+ return this;
+ }
+
+ public WebhooksPage deleteWebhook(String webhookName) {
+ SelenideElement webhook = getWebhook(webhookName);
+ webhook.$(".dropdown-toggle").shouldBe(visible).click();
+ webhook.$(".js-webhook-delete").shouldBe(visible).click();
+ modalShouldBeOpen("Delete Webhook");
+ $("button.button-red").shouldBe(visible).click();
+ modalShouldBeClosed();
+ return this;
+ }
+
+ private static SelenideElement getWebhook(String webhookName) {
+ return getWebhooks().find(text(webhookName)).should(exist);
+ }
+
private static ElementsCollection getWebhooks() {
return $$(".boxed-group tbody tr");
}
+
+ private static void modalShouldBeOpen(String title) {
+ $(".modal-head").shouldBe(visible).shouldHave(text(title));
+ }
+
+ private static void modalShouldBeClosed() {
+ $(".modal-head").shouldNot(exist);
+ }
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.webhooks;
+
+import java.util.List;
+import javax.annotation.Generated;
+
+/**
+ * This is part of the internal API.
+ * This is a POST request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/webhooks/create">Further information about this action online (including a response example)</a>
+ * @since 7.1
+ */
+@Generated("sonar-ws-generator")
+public class CreateRequest {
+
+ private String name;
+ private String organization;
+ private String project;
+ private String url;
+
+ /**
+ * This is a mandatory parameter.
+ * Example value: "My Webhook"
+ */
+ public CreateRequest setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * This is part of the internal API.
+ * Example value: "my-org"
+ */
+ public CreateRequest setOrganization(String organization) {
+ this.organization = organization;
+ return this;
+ }
+
+ public String getOrganization() {
+ return organization;
+ }
+
+ /**
+ * Example value: "my_project"
+ */
+ public CreateRequest setProject(String project) {
+ this.project = project;
+ return this;
+ }
+
+ public String getProject() {
+ return project;
+ }
+
+ /**
+ * This is a mandatory parameter.
+ * Example value: "https://www.my-webhook-listener.com/sonar"
+ */
+ public CreateRequest setUrl(String url) {
+ this.url = url;
+ return this;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.webhooks;
+
+import java.util.List;
+import javax.annotation.Generated;
+
+/**
+ * This is part of the internal API.
+ * This is a POST request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/webhooks/delete">Further information about this action online (including a response example)</a>
+ * @since 7.1
+ */
+@Generated("sonar-ws-generator")
+public class DeleteRequest {
+
+ private String webhook;
+
+ /**
+ * This is a mandatory parameter.
+ * Example value: "my_project"
+ */
+ public DeleteRequest setWebhook(String webhook) {
+ this.webhook = webhook;
+ return this;
+ }
+
+ public String getWebhook() {
+ return webhook;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.webhooks;
+
+import java.util.List;
+import javax.annotation.Generated;
+
+/**
+ * This is part of the internal API.
+ * This is a POST request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/webhooks/list">Further information about this action online (including a response example)</a>
+ * @since 7.1
+ */
+@Generated("sonar-ws-generator")
+public class ListRequest {
+
+ private String organization;
+ private String project;
+
+ /**
+ * This is part of the internal API.
+ * Example value: "my-org"
+ */
+ public ListRequest setOrganization(String organization) {
+ this.organization = organization;
+ return this;
+ }
+
+ public String getOrganization() {
+ return organization;
+ }
+
+ /**
+ * Example value: "my_project"
+ */
+ public ListRequest setProject(String project) {
+ this.project = project;
+ return this;
+ }
+
+ public String getProject() {
+ return project;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.webhooks;
+
+import java.util.List;
+import javax.annotation.Generated;
+
+/**
+ * This is part of the internal API.
+ * This is a POST request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/webhooks/update">Further information about this action online (including a response example)</a>
+ * @since 7.1
+ */
+@Generated("sonar-ws-generator")
+public class UpdateRequest {
+
+ private String name;
+ private String url;
+ private String webhook;
+
+ /**
+ * This is a mandatory parameter.
+ * Example value: "My Webhook"
+ */
+ public UpdateRequest setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * This is a mandatory parameter.
+ * Example value: "https://www.my-webhook-listener.com/sonar"
+ */
+ public UpdateRequest setUrl(String url) {
+ this.url = url;
+ return this;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ /**
+ * This is a mandatory parameter.
+ * Example value: "my_project"
+ */
+ public UpdateRequest setWebhook(String webhook) {
+ this.webhook = webhook;
+ return this;
+ }
+
+ public String getWebhook() {
+ return webhook;
+ }
+}
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.PostRequest;
import org.sonarqube.ws.client.WsConnector;
+import org.sonarqube.ws.Webhooks.CreateWsResponse;
import org.sonarqube.ws.Webhooks.DeliveriesWsResponse;
import org.sonarqube.ws.Webhooks.DeliveryWsResponse;
+import org.sonarqube.ws.Webhooks.ListWsResponse;
/**
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/webhooks">Further information about this web service online</a>
super(wsConnector, "api/webhooks");
}
+ /**
+ *
+ * This is part of the internal API.
+ * This is a POST request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/webhooks/create">Further information about this action online (including a response example)</a>
+ * @since 7.1
+ */
+ public CreateWsResponse create(CreateRequest request) {
+ return call(
+ new PostRequest(path("create"))
+ .setParam("name", request.getName())
+ .setParam("organization", request.getOrganization())
+ .setParam("project", request.getProject())
+ .setParam("url", request.getUrl()),
+ CreateWsResponse.parser());
+ }
+
+ /**
+ *
+ * This is part of the internal API.
+ * This is a POST request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/webhooks/delete">Further information about this action online (including a response example)</a>
+ * @since 7.1
+ */
+ public void delete(DeleteRequest request) {
+ call(
+ new PostRequest(path("delete"))
+ .setParam("webhook", request.getWebhook())
+ .setMediaType(MediaTypes.JSON)
+ ).content();
+ }
+
/**
*
* This is part of the internal API.
.setParam("deliveryId", request.getDeliveryId()),
DeliveryWsResponse.parser());
}
+
+ /**
+ *
+ * This is part of the internal API.
+ * This is a GET request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/webhooks/list">Further information about this action online (including a response example)</a>
+ * @since 7.1
+ */
+ public ListWsResponse list(ListRequest request) {
+ return call(
+ new GetRequest(path("list"))
+ .setParam("organization", request.getOrganization())
+ .setParam("project", request.getProject()),
+ ListWsResponse.parser());
+ }
+
+ /**
+ *
+ * This is part of the internal API.
+ * This is a POST request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/webhooks/update">Further information about this action online (including a response example)</a>
+ * @since 7.1
+ */
+ public void update(UpdateRequest request) {
+ call(
+ new PostRequest(path("update"))
+ .setParam("name", request.getName())
+ .setParam("url", request.getUrl())
+ .setParam("webhook", request.getWebhook())
+ .setMediaType(MediaTypes.JSON)
+ ).content();
+ }
}
}
private void shouldHaveUrl(SelenideElement badgesModal, String url) {
- badgesModal.$(".badge-snippet pre")
+ badgesModal.$(".code-snippet pre")
.shouldBe(Condition.visible)
.shouldHave(Condition.text(url));
}
return jetty.getURI().resolve(path).toString();
}
+ void waitUntilAllWebHooksCalled(int expectedNumberOfRequests) throws InterruptedException {
+ // Wait up to 30 seconds max
+ for (int i = 0; i < 60; i++) {
+ if (getPayloadRequests().size() == expectedNumberOfRequests) {
+ break;
+ }
+ Thread.sleep(500);
+ }
+ }
+
void clear() {
payloads.clear();
}
+
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.tests.webhook;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.qa.util.Tester;
+import org.sonarqube.qa.util.pageobjects.WebhooksPage;
+import org.sonarqube.ws.Projects.CreateWsResponse.Project;
+import org.sonarqube.ws.Users.CreateWsResponse.User;
+import org.sonarqube.ws.Webhooks.CreateWsResponse.Webhook;
+
+import static util.ItUtils.runProjectAnalysis;
+
+public class WebhooksPageTest
+{
+ @ClassRule
+ public static Orchestrator orchestrator = WebhooksSuite.ORCHESTRATOR;
+
+ @Rule
+ public Tester tester = new Tester(orchestrator);
+
+ @Before
+ @After
+ public void reset() {
+ tester.webhooks().deleteAllGlobal();
+ }
+
+ @Test
+ public void list_global_webhooks() {
+ tester.webhooks().generate();
+ Webhook webhook = tester.webhooks().generate();
+ tester.wsClient().users().skipOnboardingTutorial();
+ WebhooksPage webhooksPage = tester.openBrowser().logIn().submitCredentials("admin").openWebhooks();
+ webhooksPage
+ .countWebhooks(2)
+ .hasWebhook(webhook.getUrl())
+ .hasWebhook(webhook.getName());
+ }
+
+ @Test
+ public void list_project_webhooks() {
+ User user = tester.users().generateAdministratorOnDefaultOrganization();
+ tester.wsClient().users().skipOnboardingTutorial();
+
+ Project project = tester.projects().provision();
+ analyseProject(project);
+
+ Webhook webhook1 = tester.webhooks().generate(project);
+ Webhook webhook2 = tester.webhooks().generate(project);
+
+ WebhooksPage webhooksPage = tester.openBrowser().logIn().submitCredentials(user.getLogin()).openProjectWebhooks(project.getKey());
+ webhooksPage
+ .countWebhooks(2)
+ .hasWebhook(webhook1.getUrl())
+ .hasWebhook(webhook2.getUrl());
+ }
+
+ @Test
+ public void create_new_webhook() {
+ User user = tester.users().generateAdministratorOnDefaultOrganization();
+ tester.wsClient().users().skipOnboardingTutorial();
+
+ Project project = tester.projects().provision();
+ analyseProject(project);
+
+ WebhooksPage webhooksPage = tester.openBrowser().logIn().submitCredentials(user.getLogin()).openProjectWebhooks(project.getKey());
+ webhooksPage
+ .hasNoWebhooks()
+ .createWebhook("my-webhook", "http://greg:pass@test.com")
+ .countWebhooks(1)
+ .hasWebhook("my-webhook")
+ .hasWebhook("http://greg:pass@test.com");
+ }
+
+ @Test
+ public void prevent_webhook_creation() {
+ tester.wsClient().users().skipOnboardingTutorial();
+
+ Webhook webhook = tester.webhooks().generate();
+ for (int i = 0; i < 9; i++) {
+ tester.webhooks().generate();
+ }
+
+ WebhooksPage webhooksPage = tester.openBrowser().logIn().submitCredentials("admin").openWebhooks();
+ webhooksPage
+ .countWebhooks(10)
+ .createIsDisabled()
+ .deleteWebhook(webhook.getName())
+ .countWebhooks(9)
+ .createWebhook("my-new-webhook", "http://my-new-webhook.com");
+ }
+
+ @Test
+ public void delete_webhook() {
+ User user = tester.users().generateAdministratorOnDefaultOrganization();
+ tester.wsClient().users().skipOnboardingTutorial();
+
+ Project project = tester.projects().provision();
+ analyseProject(project);
+
+ tester.webhooks().generate(project);
+ tester.webhooks().generate(project);
+ Webhook webhook = tester.webhooks().generate(project);
+
+ WebhooksPage webhooksPage = tester.openBrowser().logIn().submitCredentials(user.getLogin()).openProjectWebhooks(project.getKey());
+ webhooksPage
+ .countWebhooks(3)
+ .deleteWebhook(webhook.getName())
+ .countWebhooks(2);
+ }
+
+ private void analyseProject(Project project) {
+ runProjectAnalysis(orchestrator, "shared/xoo-sample",
+ "sonar.projectKey", project.getKey(),
+ "sonar.projectName", project.getName());
+ }
+}
@RunWith(Suite.class)
@Suite.SuiteClasses({
+ WebhooksPageTest.class,
WebhooksTest.class
})
public class WebhooksSuite {
package org.sonarqube.tests.webhook;
import com.sonar.orchestrator.Orchestrator;
-import java.util.ArrayList;
-import java.util.Collections;
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.Rule;
import org.junit.Test;
import org.sonarqube.qa.util.Tester;
-import org.sonarqube.qa.util.pageobjects.WebhooksPage;
import org.sonarqube.ws.Issues.Issue;
import org.sonarqube.ws.Organizations.Organization;
import org.sonarqube.ws.Projects.CreateWsResponse.Project;
import org.sonarqube.ws.Qualitygates;
import org.sonarqube.ws.Qualityprofiles.CreateWsResponse.QualityProfile;
-import org.sonarqube.ws.Users;
-import org.sonarqube.ws.Users.CreateWsResponse.User;
import org.sonarqube.ws.Webhooks;
-import org.sonarqube.ws.client.HttpException;
-import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.Webhooks.CreateWsResponse.Webhook;
import org.sonarqube.ws.client.issues.BulkChangeRequest;
import org.sonarqube.ws.client.issues.SearchRequest;
-import org.sonarqube.ws.client.projects.DeleteRequest;
import org.sonarqube.ws.client.qualitygates.CreateConditionRequest;
-import org.sonarqube.ws.client.settings.ResetRequest;
-import org.sonarqube.ws.client.settings.SetRequest;
-import org.sonarqube.ws.client.webhooks.DeliveriesRequest;
-import org.sonarqube.ws.client.webhooks.DeliveryRequest;
-import util.ItUtils;
import static java.util.Collections.singletonList;
-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 = WebhooksSuite.ORCHESTRATOR;
+
@ClassRule
public static ExternalServer externalServer = new ExternalServer();
@Rule
public Tester tester = new Tester(orchestrator);
- private WsClient adminWs = ItUtils.newAdminWsClient(orchestrator);
-
@Before
public void setUp() {
externalServer.clear();
@Before
@After
public void reset() {
- disableGlobalWebhooks();
- try {
- // delete project and related properties/webhook deliveries
- adminWs.projects().delete(new DeleteRequest().setProject(PROJECT_KEY));
- } catch (HttpException e) {
- // ignore because project may not exist
- }
+ tester.webhooks().deleteAllGlobal();
}
@Test
public void call_multiple_global_and_project_webhooks_when_analysis_is_done() throws InterruptedException {
- 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")));
+ Project project = tester.projects().provision();
+ Webhook jenkins = tester.webhooks().generate(p -> p.setName("Jenkins").setUrl(externalServer.urlFor("/jenkins")));
+ Webhook hipchat = tester.webhooks().generate(p -> p.setName("HipChat").setUrl(externalServer.urlFor("/hipchat")));
+ Webhook burgr = tester.webhooks().generate(project, p -> p.setName("Burgr").setUrl(externalServer.urlFor("/burgr")));
- analyseProject();
+ analyseProject(project);
// the same payload has been sent to three servers
- waitUntilAllWebHooksCalled(3);
+ externalServer.waitUntilAllWebHooksCalled(3);
assertThat(externalServer.getPayloadRequests()).hasSize(3);
PayloadRequest request = externalServer.getPayloadRequests().get(0);
for (int i = 1; i < 3; i++) {
}
// verify HTTP headers
- assertThat(request.getHttpHeaders().get("X-SonarQube-Project")).isEqualTo(PROJECT_KEY);
+ assertThat(request.getHttpHeaders().get("X-SonarQube-Project")).isEqualTo(project.getKey());
// 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);
- assertThat(project.get("url")).isEqualTo(orchestrator.getServer().getUrl() + "/dashboard?id=" + PROJECT_KEY);
+ Map<String, String> projectPayload = (Map<String, String>) payload.get("project");
+ assertThat(projectPayload.get("key")).isEqualTo(project.getKey());
+ assertThat(projectPayload.get("name")).isEqualTo(project.getName());
+ assertThat(projectPayload.get("url")).isEqualTo(orchestrator.getServer().getUrl() + "/dashboard?id=" + project.getKey());
Map<String, Object> gate = (Map<String, Object>) payload.get("qualityGate");
assertThat(gate.get("name")).isEqualTo("Sonar 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();
+ List<Webhooks.Delivery> deliveries = tester.webhooks().getPersistedDeliveries(project);
assertThat(deliveries).hasSize(3);
for (Webhooks.Delivery delivery : deliveries) {
- assertThatPersistedDeliveryIsValid(delivery);
+ tester.webhooks().assertThatPersistedDeliveryIsValid(delivery, project, externalServer.urlFor("/"));
assertThat(delivery.getSuccess()).isTrue();
assertThat(delivery.getHttpStatus()).isEqualTo(200);
- assertThat(delivery.getName()).isIn("Jenkins", "HipChat", "Burgr");
+ assertThat(delivery.getName()).isIn(jenkins.getName(), hipchat.getName(), burgr.getName());
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);
+ Webhooks.Delivery detail = tester.webhooks().getDetailOfPersistedDelivery(deliveries.get(0));
+ tester.webhooks().assertThatPersistedDeliveryIsValid(detail, project, externalServer.urlFor("/"));
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")));
+ Project project = tester.projects().provision();
+ Webhook fail = tester.webhooks().generate(p -> p.setName("Fail").setUrl(externalServer.urlFor("/fail")));
+ Webhook hipchat = tester.webhooks().generate(p -> p.setName("HipChat").setUrl(externalServer.urlFor("/hipchat")));
- analyseProject();
+ analyseProject(project);
// 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);
+ Webhooks.Delivery failedDelivery = tester.webhooks().getPersistedDeliveryByName(project, fail.getName());
+ tester.webhooks().assertThatPersistedDeliveryIsValid(failedDelivery, project, fail.getUrl());
assertThat(failedDelivery.getSuccess()).isFalse();
assertThat(failedDelivery.getHttpStatus()).isEqualTo(500);
- Webhooks.Delivery successfulDelivery = getPersistedDeliveryByName("HipChat");
- assertThatPersistedDeliveryIsValid(successfulDelivery);
+ Webhooks.Delivery successfulDelivery = tester.webhooks().getPersistedDeliveryByName(project, hipchat.getName());
+ tester.webhooks().assertThatPersistedDeliveryIsValid(successfulDelivery, project, hipchat.getUrl());
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() throws InterruptedException {
- 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
- waitUntilAllWebHooksCalled(10 + 10);
- 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"));
+ public void persist_delivery_as_failed_if_webhook_is_not_reachable() {
+ Project project = tester.projects().provision();
+ Webhook badUrl = tester.webhooks().generate(p -> p.setUrl("http://does_not_exist"));
- analyseProject();
+ analyseProject(project);
assertThat(externalServer.getPayloadRequests()).isEmpty();
// verify persisted deliveries
- Webhooks.Delivery delivery = getPersistedDeliveryByName("Jenkins");
- Webhooks.Delivery detail = getDetailOfPersistedDelivery(delivery);
+ Webhooks.Delivery delivery = tester.webhooks().getPersistedDeliveryByName(project, badUrl.getName());
+ Webhooks.Delivery detail = tester.webhooks().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("Webhook URL is not valid: 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();
+ .contains("java.net.UnknownHostException")
+ .contains("Name or service not known");
}
@Test
public void send_webhook_on_issue_change() throws InterruptedException {
Organization defaultOrganization = tester.organizations().getDefaultOrganization();
- Project wsProject = tester.projects().provision(r -> r.setProject(PROJECT_KEY).setName(PROJECT_NAME));
- enableProjectWebhooks(PROJECT_KEY, new Webhook("Burgr", externalServer.urlFor("/burgr")));
+ Project project = tester.projects().provision();
+ Webhook burgr = tester.webhooks().generate(project, p -> p.setName("Burgr").setUrl(externalServer.urlFor("/burgr")));
+
// quality profile with one issue per line
QualityProfile qualityProfile = tester.qProfiles().createXooProfile(defaultOrganization);
tester.qProfiles().activateRule(qualityProfile, "xoo:OneIssuePerLine");
- tester.qProfiles().assignQProfileToProject(qualityProfile, wsProject);
+ tester.qProfiles().assignQProfileToProject(qualityProfile, project);
// quality gate definition
Qualitygates.CreateResponse qGate = tester.qGates().generate();
tester.qGates().service().createCondition(new CreateConditionRequest().setGateId(String.valueOf(qGate.getId()))
.setMetric("reliability_rating").setOp("GT").setError("1"));
- tester.qGates().associateProject(qGate, wsProject);
+ tester.qGates().associateProject(qGate, project);
// analyze project and clear first webhook
- analyseProject();
- waitUntilAllWebHooksCalled(1);
+
+ analyseProject(project);
+ externalServer.waitUntilAllWebHooksCalled(1);
externalServer.clear();
// change an issue to blocker bug, QG status goes from OK to ERROR, so webhook is called
tester.wsClient().issues().bulkChange(new BulkChangeRequest().setIssues(singletonList(firstIssue.getKey()))
.setSetSeverity(singletonList("BLOCKER"))
.setSetType(singletonList("BUG")));
- waitUntilAllWebHooksCalled(1);
+ externalServer.waitUntilAllWebHooksCalled(1);
PayloadRequest request = externalServer.getPayloadRequests().get(0);
- assertThat(request.getHttpHeaders().get("X-SonarQube-Project")).isEqualTo(PROJECT_KEY);
+ assertThat(request.getHttpHeaders().get("X-SonarQube-Project")).isEqualTo(project.getKey());
// 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);
- assertThat(project.get("url")).isEqualTo(orchestrator.getServer().getUrl() + "/dashboard?id=" + PROJECT_KEY);
+ Map<String, String> projectPayload = (Map<String, String>) payload.get("project");
+ assertThat(projectPayload.get("key")).isEqualTo(project.getKey());
+ assertThat(projectPayload.get("name")).isEqualTo(project.getName());
+ assertThat(projectPayload.get("url")).isEqualTo(orchestrator.getServer().getUrl() + "/dashboard?id=" + project.getKey());
Map<String, Object> gate = (Map<String, Object>) payload.get("qualityGate");
assertThat(gate.get("name")).isEqualTo(qGate.getName());
assertThat(gate.get("status")).isEqualTo("ERROR");
// change severity of issue, won't change the QG status, so no webhook called
tester.wsClient().issues().bulkChange(new BulkChangeRequest().setIssues(singletonList(firstIssue.getKey()))
.setSetSeverity(singletonList("MINOR")));
- waitUntilAllWebHooksCalled(1);
+ externalServer.waitUntilAllWebHooksCalled(1);
assertThat(externalServer.getPayloadRequests()).isEmpty();
// resolve issue as won't fix, QG status goes to OK, so webhook called
tester.wsClient().issues().bulkChange(new BulkChangeRequest().setIssues(singletonList(firstIssue.getKey()))
.setDoTransition("wontfix"));
- waitUntilAllWebHooksCalled(1);
+ externalServer.waitUntilAllWebHooksCalled(1);
request = externalServer.getPayloadRequests().get(0);
payload = jsonToMap(request.getJson());
gate = (Map<String, Object>) payload.get("qualityGate");
assertThat(gate.get("status")).isEqualTo("OK");
}
- @Test
- public void list_global_webhooks() {
- enableGlobalWebhooks(new Webhook("foo", "http://foo.bar"), new Webhook("bar", "https://bar.baz/test"));
- tester.wsClient().users().skipOnboardingTutorial();
- WebhooksPage webhooksPage = tester.openBrowser().logIn().submitCredentials("admin").openWebhooks();
- webhooksPage
- .countWebhooks(2)
- .hasWebhook("http://foo.bar");
- }
-
- @Test
- public void list_project_webhooks() {
- analyseProject();
- enableProjectWebhooks(PROJECT_KEY, new Webhook("foo", "http://foo.bar"), new Webhook("bar", "https://bar.baz/test"));
- User user = tester.users().generateAdministratorOnDefaultOrganization();
- tester.wsClient().users().skipOnboardingTutorial();
- WebhooksPage webhooksPage = tester.openBrowser().logIn().submitCredentials(user.getLogin()).openProjectWebhooks(PROJECT_KEY);
- webhooksPage
- .countWebhooks(2)
- .hasWebhook("http://foo.bar");
- }
-
- private void analyseProject() {
+ private void analyseProject(Project project) {
runProjectAnalysis(orchestrator, "shared/xoo-sample",
- "sonar.projectKey", PROJECT_KEY,
- "sonar.projectName", PROJECT_NAME);
- }
-
- private List<Webhooks.Delivery> getPersistedDeliveries() {
- DeliveriesRequest deliveriesReq = new DeliveriesRequest().setComponentKey(PROJECT_KEY);
- 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(new DeliveryRequest().setDeliveryId(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, ","));
+ "sonar.projectKey", project.getKey(),
+ "sonar.projectName", project.getName());
}
-
- 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 = new ResetRequest().setKeys(Collections.singletonList(key)).setComponent(componentKey);
- adminWs.settings().reset(req);
- } else {
- SetRequest req = new SetRequest().setKey(key).setValue(value).setComponent(componentKey);
- adminWs.settings().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;
- }
- }
-
- /**
- * Wait up to 30 seconds
- */
- private static void waitUntilAllWebHooksCalled(int expectedNumberOfRequests) throws InterruptedException {
- for (int i = 0; i < 60; i++) {
- if (externalServer.getPayloadRequests().size() == expectedNumberOfRequests) {
- break;
- }
- Thread.sleep(500);
- }
- }
-
}