aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@sonarsource.com>2016-11-08 20:43:56 +0100
committerSimon Brandhof <simon.brandhof@sonarsource.com>2016-11-09 21:34:38 +0100
commit8301fe8e2841fdb94f12349134f23939e2e057c0 (patch)
tree82d55a9870185186e724667be6ce3f993103f6d6 /server
parentf050424587272bb89868aded19c5b6225d235348 (diff)
downloadsonarqube-8301fe8e2841fdb94f12349134f23939e2e057c0.tar.gz
sonarqube-8301fe8e2841fdb94f12349134f23939e2e057c0.zip
SONAR-8351 Send JSON payload over HTTP when project analysis is complete
Diffstat (limited to 'server')
-rw-r--r--server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java2
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java6
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java44
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java34
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java69
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java131
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java31
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java107
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java96
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java23
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java40
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java75
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java92
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java142
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java107
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json8
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json23
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json24
18 files changed, 1052 insertions, 2 deletions
diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
index b9b59c7dc48..b9d1c12bfef 100644
--- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
+++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
@@ -108,7 +108,7 @@ public class ComputeEngineContainerImplTest {
+ 25 // level 1
+ 46 // content of DaoModule
+ 2 // content of EsSearchModule
- + 62 // content of CorePropertyDefinitions
+ + 64 // content of CorePropertyDefinitions
+ 1 // content of CePropertyDefinitions
);
assertThat(picoContainer.getParent().getParent().getParent().getParent()).isNull();
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
index 0431c0983ae..3c32129608d 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
@@ -101,6 +101,7 @@ import org.sonar.server.computation.task.projectanalysis.source.LastCommitVisito
import org.sonar.server.computation.task.projectanalysis.source.SourceHashRepositoryImpl;
import org.sonar.server.computation.task.projectanalysis.source.SourceLinesRepositoryImpl;
import org.sonar.server.computation.task.projectanalysis.step.ReportComputationSteps;
+import org.sonar.server.computation.task.projectanalysis.webhook.WebhookModule;
import org.sonar.server.computation.task.step.ComputationStepExecutor;
import org.sonar.server.computation.task.step.ComputationSteps;
import org.sonar.server.computation.taskprocessor.MutableTaskResultHolderImpl;
@@ -236,7 +237,10 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
// views
ViewIndex.class,
- MeasureToMeasureDto.class);
+ MeasureToMeasureDto.class,
+
+ // webhooks
+ WebhookModule.class);
}
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java
new file mode 100644
index 00000000000..18c1de9ff67
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import javax.annotation.concurrent.Immutable;
+
+import static java.util.Objects.requireNonNull;
+
+@Immutable
+public class Webhook {
+
+ private final String name;
+ private final String url;
+
+ public Webhook(String name, String url) {
+ this.name = requireNonNull(name);
+ this.url = requireNonNull(url);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java
new file mode 100644
index 00000000000..b9316735ed5
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+public interface WebhookCaller {
+
+ /**
+ * Call webhook by sending a HTTP(S) POST request containing
+ * the JSON payload.
+ * <br/>
+ * Errors are silently ignored. They don't generate logs or
+ * throw exceptions. The error status is stored in the
+ * returned {@link WebhookDelivery}.
+ */
+ WebhookDelivery call(Webhook webhook, WebhookPayload payload);
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java
new file mode 100644
index 00000000000..26f618bb2bd
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import java.io.IOException;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.utils.System2;
+
+@ComputeEngineSide
+public class WebhookCallerImpl implements WebhookCaller {
+
+ private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+ private static final String PROJECT_KEY_HEADER = "X-SonarQube-Project";
+
+ private final System2 system;
+ private final OkHttpClient okHttpClient;
+
+ public WebhookCallerImpl(System2 system, OkHttpClient okHttpClient) {
+ this.system = system;
+ this.okHttpClient = okHttpClient;
+ }
+
+ @Override
+ public WebhookDelivery call(Webhook webhook, WebhookPayload payload) {
+ Request.Builder request = new Request.Builder();
+ request.url(webhook.getUrl());
+ request.header(PROJECT_KEY_HEADER, payload.getProjectKey());
+ RequestBody body = RequestBody.create(JSON, payload.toJson());
+ request.post(body);
+
+ WebhookDelivery.Builder builder = new WebhookDelivery.Builder();
+ long startedAt = system.now();
+ builder
+ .setAt(startedAt)
+ .setPayload(payload)
+ .setWebhook(webhook);
+ try {
+ Response response = okHttpClient.newCall(request.build()).execute();
+ builder.setHttpStatus(response.code());
+ builder.setDurationInMs(system.now() - startedAt);
+ } catch (IOException e) {
+ builder.setThrowable(e);
+ }
+ return builder.build();
+ }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java
new file mode 100644
index 00000000000..a2623450e82
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java
@@ -0,0 +1,131 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import java.util.Optional;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A {@link WebhookDelivery} represents the result of a webhook call.
+ */
+@Immutable
+public class WebhookDelivery {
+
+ private final Webhook webhook;
+ private final WebhookPayload payload;
+ private final Integer httpStatus;
+ private final Long durationInMs;
+ private final long at;
+ private final Throwable throwable;
+
+ private WebhookDelivery(Builder builder) {
+ this.webhook = requireNonNull(builder.webhook);
+ this.payload = requireNonNull(builder.payload);
+ this.httpStatus = builder.httpStatus;
+ this.durationInMs = builder.durationInMs;
+ this.at = builder.at;
+ this.throwable = builder.throwable;
+ }
+
+ public Webhook getWebhook() {
+ return webhook;
+ }
+
+ public WebhookPayload getPayload() {
+ return payload;
+ }
+
+ /**
+ * @return the HTTP status if {@link #getThrowable()} is empty, else returns
+ * {@link Optional#empty()}
+ */
+ public Optional<Integer> getHttpStatus() {
+ return Optional.ofNullable(httpStatus);
+ }
+
+ /**
+ * @return the duration in milliseconds if {@link #getThrowable()} is empty,
+ * else returns {@link Optional#empty()}
+ */
+ public Optional<Long> getDurationInMs() {
+ return Optional.ofNullable(durationInMs);
+ }
+
+ /**
+ * @return the date of sending
+ */
+ public long getAt() {
+ return at;
+ }
+
+ /**
+ * @return the error raised if the request could not be executed due to a connectivity
+ * problem or timeout
+ */
+ public Optional<Throwable> getThrowable() {
+ return Optional.ofNullable(throwable);
+ }
+
+ public static class Builder {
+ private Webhook webhook;
+ private WebhookPayload payload;
+ private Integer httpStatus;
+ private Long durationInMs;
+ private long at;
+ private Throwable throwable;
+
+ public Builder setWebhook(Webhook w) {
+ this.webhook = w;
+ return this;
+ }
+
+ public Builder setPayload(WebhookPayload payload) {
+ this.payload = payload;
+ return this;
+ }
+
+ public Builder setHttpStatus(@Nullable Integer httpStatus) {
+ this.httpStatus = httpStatus;
+ return this;
+ }
+
+ public Builder setDurationInMs(@Nullable Long durationInMs) {
+ this.durationInMs = durationInMs;
+ return this;
+ }
+
+ public Builder setAt(long at) {
+ this.at = at;
+ return this;
+ }
+
+ public Builder setThrowable(@Nullable Throwable t) {
+ this.throwable = t;
+ return this;
+ }
+
+ public WebhookDelivery build() {
+ return new WebhookDelivery(this);
+ }
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java
new file mode 100644
index 00000000000..1bc200acdc6
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import org.sonar.core.platform.Module;
+
+public class WebhookModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(
+ WebhookCallerImpl.class,
+ WebhookPostTask.class);
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java
new file mode 100644
index 00000000000..2429b1236d8
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Date;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.ce.posttask.CeTask;
+import org.sonar.api.ce.posttask.PostProjectAnalysisTask;
+import org.sonar.api.ce.posttask.Project;
+import org.sonar.api.ce.posttask.QualityGate;
+import org.sonar.api.utils.text.JsonWriter;
+
+import static java.util.Objects.requireNonNull;
+
+@Immutable
+public class WebhookPayload {
+
+ private final String projectKey;
+ private final String json;
+
+ public WebhookPayload(String projectKey, String json) {
+ this.projectKey = requireNonNull(projectKey);
+ this.json = requireNonNull(json);
+ }
+
+ public String getProjectKey() {
+ return projectKey;
+ }
+
+ public String toJson() {
+ return json;
+ }
+
+ public static WebhookPayload from(PostProjectAnalysisTask.ProjectAnalysis analysis) {
+ Writer string = new StringWriter();
+ JsonWriter writer = JsonWriter.of(string);
+ writer.beginObject();
+ writeTask(writer, analysis.getCeTask());
+ Optional<Date> analysisDate = analysis.getAnalysisDate();
+ if (analysisDate.isPresent()) {
+ writer.propDateTime("analysedAt", analysisDate.get());
+ }
+ writeProject(analysis, writer, analysis.getProject());
+ writeQualityGate(writer, analysis.getQualityGate());
+ writer.endObject().close();
+ return new WebhookPayload(analysis.getProject().getKey(), string.toString());
+ }
+
+ private static void writeTask(JsonWriter writer, CeTask ceTask) {
+ writer.prop("taskId", ceTask.getId());
+ writer.prop("status", ceTask.getStatus().toString());
+ }
+
+ private static void writeProject(PostProjectAnalysisTask.ProjectAnalysis analysis, JsonWriter writer, Project project) {
+ writer.name("project");
+ writer.beginObject();
+ writer.prop("key", project.getKey());
+ writer.prop("name", analysis.getProject().getName());
+ writer.endObject();
+ }
+
+ private static void writeQualityGate(JsonWriter writer, @Nullable QualityGate gate) {
+ if (gate != null) {
+ writer.name("qualityGate");
+ writer.beginObject();
+ writer.prop("name", gate.getName());
+ writer.prop("status", gate.getStatus().toString());
+ writer.name("conditions").beginArray();
+ for (QualityGate.Condition condition : gate.getConditions()) {
+ writer.beginObject();
+ writer.prop("metric", condition.getMetricKey());
+ writer.prop("operator", condition.getOperator().name());
+ if (condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE) {
+ writer.prop("value", condition.getValue());
+ }
+ writer.prop("status", condition.getStatus().name());
+ writer.prop("onLeakPeriod", condition.isOnLeakPeriod());
+ writer.prop("errorThreshold", condition.getErrorThreshold());
+ writer.prop("warningThreshold", condition.getWarningThreshold());
+ writer.endObject();
+ }
+ writer.endArray();
+ writer.endObject();
+ }
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java
new file mode 100644
index 00000000000..8ec33dd1eac
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import com.google.common.collect.Iterables;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import org.sonar.api.ce.posttask.PostProjectAnalysisTask;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.config.WebhookProperties;
+import org.sonar.core.util.stream.Collectors;
+import org.sonar.server.computation.task.projectanalysis.component.SettingsRepository;
+import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolder;
+
+import static com.google.common.base.Throwables.getRootCause;
+import static java.lang.String.format;
+
+public class WebhookPostTask implements PostProjectAnalysisTask {
+
+ private static final Logger LOGGER = Loggers.get(WebhookPostTask.class);
+
+ private final TreeRootHolder rootHolder;
+ private final SettingsRepository settingsRepository;
+ private final WebhookCaller caller;
+
+ public WebhookPostTask(TreeRootHolder rootHolder, SettingsRepository settingsRepository, WebhookCaller caller) {
+ this.rootHolder = rootHolder;
+ this.settingsRepository = settingsRepository;
+ this.caller = caller;
+ }
+
+ @Override
+ public void finished(ProjectAnalysis analysis) {
+ Settings settings = settingsRepository.getSettings(rootHolder.getRoot());
+
+ Iterable<String> webhookProps = Iterables.concat(
+ getWebhookProperties(settings, WebhookProperties.GLOBAL_KEY),
+ getWebhookProperties(settings, WebhookProperties.PROJECT_KEY)
+ );
+ if (!Iterables.isEmpty(webhookProps)) {
+ process(settings, analysis, webhookProps);
+ }
+ }
+
+ private static List<String> getWebhookProperties(Settings settings, String propertyKey) {
+ String[] webhookIds = settings.getStringArray(propertyKey);
+ return Arrays.stream(webhookIds)
+ .map(webhookId -> format("%s.%s", propertyKey, webhookId))
+ .collect(Collectors.toList(webhookIds.length));
+ }
+
+ private void process(Settings settings, ProjectAnalysis analysis, Iterable<String> webhookProperties) {
+ WebhookPayload payload = WebhookPayload.from(analysis);
+ for (String webhookProp : webhookProperties) {
+ String name = settings.getString(format("%s.%s", webhookProp, WebhookProperties.NAME_FIELD));
+ String url = settings.getString(format("%s.%s", webhookProp, WebhookProperties.URL_FIELD));
+ // as webhooks are defined as property sets, we can't ensure validity of fields on creation.
+ if (name != null && url != null) {
+ Webhook webhook = new Webhook(name, url);
+ WebhookDelivery delivery = caller.call(webhook, payload);
+ log(delivery);
+ }
+ }
+ }
+
+ private static void log(WebhookDelivery delivery) {
+ Optional<Throwable> throwable = delivery.getThrowable();
+ if (throwable.isPresent()) {
+ LOGGER.debug("Failed to send webhook '{}' | url={} | message={}",
+ delivery.getWebhook().getName(), delivery.getWebhook().getUrl(), getRootCause(throwable.get()).getMessage());
+ } else {
+ LOGGER.debug("Sent webhook '{}' | url={} | time={}ms | status={}",
+ delivery.getWebhook().getName(), delivery.getWebhook().getUrl(), delivery.getDurationInMs().orElse(-1L), delivery.getHttpStatus().orElse(-1));
+ }
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java
new file mode 100644
index 00000000000..7ef8477a244
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java
new file mode 100644
index 00000000000..1973ebb7576
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.component;
+
+import org.sonar.api.config.Settings;
+
+/**
+ * Implementation of {@link SettingsRepository} that always return the
+ * same mutable {@link Settings}, whatever the component.
+ */
+public class TestSettingsRepository implements SettingsRepository {
+
+ private final Settings settings;
+
+ public TestSettingsRepository(Settings settings) {
+ this.settings = settings;
+ }
+
+ @Override
+ public Settings getSettings(Component component) {
+ return settings;
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java
new file mode 100644
index 00000000000..1e82369282d
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.Nullable;
+
+import static java.util.Objects.requireNonNull;
+
+public class TestWebhookCaller implements WebhookCaller {
+
+ private final Queue<Item> deliveries = new LinkedList<>();
+ private final AtomicInteger countSent = new AtomicInteger(0);
+
+ public TestWebhookCaller enqueueSuccess(long at, int httpCode, long durationMs) {
+ deliveries.add(new Item(at, httpCode, durationMs, null));
+ return this;
+ }
+
+ public TestWebhookCaller enqueueFailure(long at, Throwable t) {
+ deliveries.add(new Item(at, null, null, t));
+ return this;
+ }
+
+ @Override
+ public WebhookDelivery call(Webhook webhook, WebhookPayload payload) {
+ Item item = requireNonNull(deliveries.poll(), "Queue is empty");
+ countSent.incrementAndGet();
+ return new WebhookDelivery.Builder()
+ .setAt(item.at)
+ .setHttpStatus(item.httpCode)
+ .setDurationInMs(item.durationMs)
+ .setThrowable(item.throwable)
+ .setPayload(payload)
+ .setWebhook(webhook)
+ .build();
+ }
+
+ public int countSent() {
+ return countSent.get();
+ }
+
+ private static class Item {
+ final long at;
+ final Integer httpCode;
+ final Long durationMs;
+ final Throwable throwable;
+
+ Item(long at, @Nullable Integer httpCode, @Nullable Long durationMs, @Nullable Throwable throwable) {
+ this.at = at;
+ this.httpCode = httpCode;
+ this.durationMs = durationMs;
+ this.throwable = throwable;
+ }
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java
new file mode 100644
index 00000000000..474974fa5d1
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.config.MapSettings;
+import org.sonar.api.internal.SonarRuntimeImpl;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.Version;
+import org.sonar.api.utils.internal.TestSystem2;
+import org.sonar.server.util.OkHttpClientProvider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+public class WebhookCallerImplTest {
+
+ private static final long NOW = 1_500_000_000_000L;
+
+ @Rule
+ public MockWebServer server = new MockWebServer();
+
+ private System2 system = new TestSystem2().setNow(NOW);
+
+ @Test
+ public void send_posts_payload_to_http_server() throws Exception {
+ Webhook webhook = new Webhook("my-webhook", server.url("/ping").toString());
+ WebhookPayload payload = new WebhookPayload("P1", "{the payload}");
+
+ server.enqueue(new MockResponse().setBody("pong").setResponseCode(201));
+ WebhookDelivery delivery = newSender().call(webhook, payload);
+
+ assertThat(delivery.getHttpStatus().get()).isEqualTo(201);
+ assertThat(delivery.getDurationInMs().get()).isGreaterThanOrEqualTo(0L);
+ assertThat(delivery.getThrowable()).isEmpty();
+ assertThat(delivery.getAt()).isEqualTo(NOW);
+ assertThat(delivery.getWebhook()).isSameAs(webhook);
+ assertThat(delivery.getPayload()).isSameAs(payload);
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertThat(recordedRequest.getMethod()).isEqualTo("POST");
+ assertThat(recordedRequest.getPath()).isEqualTo("/ping");
+ assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(payload.toJson());
+ assertThat(recordedRequest.getHeader("User-Agent")).isEqualTo("SonarQube/6.2");
+ assertThat(recordedRequest.getHeader("Content-Type")).isEqualTo("application/json; charset=utf-8");
+ assertThat(recordedRequest.getHeader("X-SonarQube-Project")).isEqualTo(payload.getProjectKey());
+ }
+
+ @Test
+ public void send_does_not_throw_exception_on_errors() throws Exception {
+ Webhook webhook = new Webhook("my-webhook", server.url("/ping").toString());
+ WebhookPayload payload = new WebhookPayload("P1", "{the payload}");
+
+ server.shutdown();
+ WebhookDelivery delivery = newSender().call(webhook, payload);
+
+ assertThat(delivery.getHttpStatus()).isEmpty();
+ assertThat(delivery.getDurationInMs()).isEmpty();
+ assertThat(delivery.getThrowable().get().getMessage()).startsWith("Failed to connect to");
+ assertThat(delivery.getAt()).isEqualTo(NOW);
+ assertThat(delivery.getWebhook()).isSameAs(webhook);
+ assertThat(delivery.getPayload()).isSameAs(payload);
+ }
+
+ private WebhookCaller newSender() {
+ SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("6.2"), SonarQubeSide.SERVER);
+ return new WebhookCallerImpl(system, new OkHttpClientProvider().provide(new MapSettings(), runtime));
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java
new file mode 100644
index 00000000000..f01d81d401d
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java
@@ -0,0 +1,142 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import java.util.Date;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.sonar.api.ce.posttask.CeTask;
+import org.sonar.api.ce.posttask.PostProjectAnalysisTask;
+import org.sonar.api.ce.posttask.Project;
+import org.sonar.api.ce.posttask.QualityGate;
+import org.sonar.api.ce.posttask.ScannerContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newCeTaskBuilder;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newConditionBuilder;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newProjectBuilder;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newQualityGateBuilder;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newScannerContextBuilder;
+import static org.sonar.test.JsonAssert.assertJson;
+
+public class WebhookPayloadTest {
+
+ private static final String PROJECT_KEY = "P1";
+
+ @Test
+ public void create_payload_for_successful_analysis() {
+ CeTask task = newCeTaskBuilder()
+ .setStatus(CeTask.Status.SUCCESS)
+ .setId("#1")
+ .build();
+ QualityGate gate = newQualityGateBuilder()
+ .setId("G1")
+ .setName("Gate One")
+ .setStatus(QualityGate.Status.WARN)
+ .add(newConditionBuilder()
+ .setMetricKey("coverage")
+ .setOperator(QualityGate.Operator.GREATER_THAN)
+ .setOnLeakPeriod(true)
+ .setWarningThreshold("75.0")
+ .setErrorThreshold("70.0")
+ .build(QualityGate.EvaluationStatus.WARN, "74.0"))
+ .build();
+ PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(task, gate);
+
+ WebhookPayload payload = WebhookPayload.from(analysis);
+ assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY);
+ assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/success.json"));
+ }
+
+ @Test
+ public void create_payload_for_failed_analysis() {
+ CeTask ceTask = newCeTaskBuilder().setStatus(CeTask.Status.FAILED).setId("#1").build();
+ PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(ceTask, null);
+
+ WebhookPayload payload = WebhookPayload.from(analysis);
+
+ assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY);
+ assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/failed.json"));
+ }
+
+ @Test
+ public void create_payload_with_gate_conditions_without_value() {
+ CeTask task = newCeTaskBuilder()
+ .setStatus(CeTask.Status.SUCCESS)
+ .setId("#1")
+ .build();
+ QualityGate gate = newQualityGateBuilder()
+ .setId("G1")
+ .setName("Gate One")
+ .setStatus(QualityGate.Status.WARN)
+ .add(newConditionBuilder()
+ .setMetricKey("coverage")
+ .setOperator(QualityGate.Operator.GREATER_THAN)
+ .setWarningThreshold("75.0")
+ .setErrorThreshold("70.0")
+ .buildNoValue())
+ .build();
+ PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(task, gate);
+
+ WebhookPayload payload = WebhookPayload.from(analysis);
+ assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY);
+ assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/gate_condition_without_value.json"));
+ }
+
+ private static PostProjectAnalysisTask.ProjectAnalysis newAnalysis(CeTask task, @Nullable QualityGate gate) {
+ return new PostProjectAnalysisTask.ProjectAnalysis() {
+ @Override
+ public CeTask getCeTask() {
+ return task;
+ }
+
+ @Override
+ public Project getProject() {
+ return newProjectBuilder()
+ .setUuid("P1_UUID")
+ .setKey(PROJECT_KEY)
+ .setName("Project One")
+ .build();
+ }
+
+ @Override
+ public QualityGate getQualityGate() {
+ return gate;
+ }
+
+ @Override
+ public Date getDate() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Optional<Date> getAnalysisDate() {
+ return Optional.of(new Date(1_500_000_000_000L));
+ }
+
+ @Override
+ public ScannerContext getScannerContext() {
+ return newScannerContextBuilder().build();
+ }
+ };
+ }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java
new file mode 100644
index 00000000000..ac8224ce0f6
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import java.io.IOException;
+import java.util.Date;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.ce.posttask.CeTask;
+import org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester;
+import org.sonar.api.config.MapSettings;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.server.computation.task.projectanalysis.component.SettingsRepository;
+import org.sonar.server.computation.task.projectanalysis.component.TestSettingsRepository;
+import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolderRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newCeTaskBuilder;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newProjectBuilder;
+import static org.sonar.server.computation.task.projectanalysis.component.ReportComponent.DUMB_PROJECT;
+
+public class WebhookPostTaskTest {
+
+ private static final long NOW = 1_500_000_000_000L;
+
+ @Rule
+ public LogTester logTester = new LogTester().setLevel(LoggerLevel.DEBUG);
+
+ @Rule
+ public TreeRootHolderRule rootHolder = new TreeRootHolderRule().setRoot(DUMB_PROJECT);
+
+ private final MapSettings settings = new MapSettings();
+ private final TestWebhookCaller caller = new TestWebhookCaller();
+
+ @Test
+ public void do_nothing_if_no_webhooks() {
+ execute();
+
+ assertThat(caller.countSent()).isEqualTo(0);
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).isEmpty();
+ }
+
+ @Test
+ public void send_global_webhooks() {
+ settings.setProperty("sonar.webhooks.global", "1,2");
+ settings.setProperty("sonar.webhooks.global.1.name", "First");
+ settings.setProperty("sonar.webhooks.global.1.url", "http://url1");
+ settings.setProperty("sonar.webhooks.global.2.name", "Second");
+ settings.setProperty("sonar.webhooks.global.2.url", "http://url2");
+ caller.enqueueSuccess(NOW, 200, 1_234);
+ caller.enqueueFailure(NOW, new IOException("Fail to connect"));
+
+ execute();
+
+ assertThat(caller.countSent()).isEqualTo(2);
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200");
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Failed to send webhook 'Second' | url=http://url2 | message=Fail to connect");
+ }
+ @Test
+ public void send_project_webhooks() {
+ settings.setProperty("sonar.webhooks.project", "1");
+ settings.setProperty("sonar.webhooks.project.1.name", "First");
+ settings.setProperty("sonar.webhooks.project.1.url", "http://url1");
+ caller.enqueueSuccess(NOW, 200, 1_234);
+
+ execute();
+
+ assertThat(caller.countSent()).isEqualTo(1);
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200");
+ }
+
+ private void execute() {
+ SettingsRepository settingsRepository = new TestSettingsRepository(settings);
+ WebhookPostTask task = new WebhookPostTask(rootHolder, settingsRepository, caller);
+
+ PostProjectAnalysisTaskTester.of(task)
+ .at(new Date())
+ .withCeTask(newCeTaskBuilder()
+ .setStatus(CeTask.Status.SUCCESS)
+ .setId("#1")
+ .build())
+ .withProject(newProjectBuilder()
+ .setUuid("P1_UUID")
+ .setKey("P1")
+ .setName("Project One")
+ .build())
+ .execute();
+ }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json
new file mode 100644
index 00000000000..358c1f055ae
--- /dev/null
+++ b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json
@@ -0,0 +1,8 @@
+{
+ "taskId": "#1",
+ "status": "FAILED",
+ "project": {
+ "key": "P1",
+ "name": "Project One"
+ }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json
new file mode 100644
index 00000000000..7cdcfc965df
--- /dev/null
+++ b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json
@@ -0,0 +1,23 @@
+{
+ "taskId": "#1",
+ "status": "SUCCESS",
+ "analysedAt": "2017-07-14T04:40:00+0200",
+ "project": {
+ "key": "P1",
+ "name": "Project One"
+ },
+ "qualityGate": {
+ "name": "Gate One",
+ "status": "WARN",
+ "conditions": [
+ {
+ "metric": "coverage",
+ "operator": "GREATER_THAN",
+ "status": "NO_VALUE",
+ "onLeakPeriod": false,
+ "errorThreshold": "70.0",
+ "warningThreshold": "75.0"
+ }
+ ]
+ }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json
new file mode 100644
index 00000000000..fdb7f8eb112
--- /dev/null
+++ b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json
@@ -0,0 +1,24 @@
+{
+ "taskId": "#1",
+ "status": "SUCCESS",
+ "analysedAt": "2017-07-14T04:40:00+0200",
+ "project": {
+ "key": "P1",
+ "name": "Project One"
+ },
+ "qualityGate": {
+ "name": "Gate One",
+ "status": "WARN",
+ "conditions": [
+ {
+ "metric": "coverage",
+ "operator": "GREATER_THAN",
+ "value": "74.0",
+ "status": "WARN",
+ "onLeakPeriod": true,
+ "errorThreshold": "70.0",
+ "warningThreshold": "75.0"
+ }
+ ]
+ }
+}