Browse Source

SONAR-10345 Add IT's for webhooks management

tags/7.5
Grégoire Aubert 6 years ago
parent
commit
bad30545b5

+ 10
- 0
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/Tester.java View File

@@ -207,6 +207,11 @@ public class Tester extends ExternalResource implements TesterSession {
return rootSession.qGates();
}

@Override
public WebhookTester webhooks() {
return rootSession.webhooks();
}

private static class TesterSessionImpl implements TesterSession {
private final WsClient client;

@@ -262,5 +267,10 @@ public class Tester extends ExternalResource implements TesterSession {
public QGateTester qGates() {
return new QGateTester(this);
}

@Override
public WebhookTester webhooks() {
return new WebhookTester(this);
}
}
}

+ 2
- 0
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/TesterSession.java View File

@@ -41,4 +41,6 @@ public interface TesterSession {

QGateTester qGates();

WebhookTester webhooks();

}

+ 120
- 0
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/WebhookTester.java View File

@@ -0,0 +1,120 @@
/*
* 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);
}
}
}

+ 47
- 0
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/WebhooksPage.java View File

@@ -20,9 +20,13 @@
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.$$;

@@ -37,12 +41,55 @@ public class WebhooksPage {
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);
}
}

+ 89
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/webhooks/CreateRequest.java View File

@@ -0,0 +1,89 @@
/*
* 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;
}
}

+ 48
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/webhooks/DeleteRequest.java View File

@@ -0,0 +1,48 @@
/*
* 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;
}
}

+ 61
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/webhooks/ListRequest.java View File

@@ -0,0 +1,61 @@
/*
* 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;
}
}

+ 76
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/webhooks/UpdateRequest.java View File

@@ -0,0 +1,76 @@
/*
* 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;
}
}

+ 66
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/webhooks/WebhooksService.java View File

@@ -26,8 +26,10 @@ import org.sonarqube.ws.client.BaseService;
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>
@@ -39,6 +41,38 @@ public class WebhooksService extends BaseService {
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.
@@ -67,4 +101,36 @@ public class WebhooksService extends BaseService {
.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();
}
}

+ 1
- 1
tests/src/test/java/org/sonarqube/tests/project/ProjectBadgesTest.java View File

@@ -115,7 +115,7 @@ public class ProjectBadgesTest {
}

private void shouldHaveUrl(SelenideElement badgesModal, String url) {
badgesModal.$(".badge-snippet pre")
badgesModal.$(".code-snippet pre")
.shouldBe(Condition.visible)
.shouldHave(Condition.text(url));
}

+ 11
- 0
tests/src/test/java/org/sonarqube/tests/webhook/ExternalServer.java View File

@@ -93,7 +93,18 @@ class ExternalServer extends ExternalResource {
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();
}

}

+ 139
- 0
tests/src/test/java/org/sonarqube/tests/webhook/WebhooksPageTest.java View File

@@ -0,0 +1,139 @@
/*
* 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());
}
}

+ 1
- 0
tests/src/test/java/org/sonarqube/tests/webhook/WebhooksSuite.java View File

@@ -28,6 +28,7 @@ import static util.ItUtils.xooPlugin;

@RunWith(Suite.class)
@Suite.SuiteClasses({
WebhooksPageTest.class,
WebhooksTest.class
})
public class WebhooksSuite {

+ 54
- 223
tests/src/test/java/org/sonarqube/tests/webhook/WebhooksTest.java View File

@@ -20,64 +20,41 @@
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();
@@ -86,28 +63,20 @@ public class WebhooksTest {
@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++) {
@@ -116,137 +85,104 @@ public class WebhooksTest {
}

// 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
@@ -255,18 +191,18 @@ public class WebhooksTest {
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");
@@ -276,127 +212,22 @@ public class WebhooksTest {
// 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);
}
}

}

Loading…
Cancel
Save