Deliveries older than 1 month are purged.tags/6.2-RC1
@@ -26,14 +26,26 @@ import static java.util.Objects.requireNonNull; | |||
@Immutable | |||
public class Webhook { | |||
private final String componentUuid; | |||
private final String ceTaskUuid; | |||
private final String name; | |||
private final String url; | |||
public Webhook(String name, String url) { | |||
public Webhook(String componentUuid, String ceTaskUuid, String name, String url) { | |||
this.componentUuid = requireNonNull(componentUuid); | |||
this.ceTaskUuid = requireNonNull(ceTaskUuid); | |||
this.name = requireNonNull(name); | |||
this.url = requireNonNull(url); | |||
} | |||
public String getComponentUuid() { | |||
return componentUuid; | |||
} | |||
public String getCeTaskUuid() { | |||
return ceTaskUuid; | |||
} | |||
public String getName() { | |||
return name; | |||
} |
@@ -59,9 +59,9 @@ public class WebhookCallerImpl implements WebhookCaller { | |||
try { | |||
Response response = okHttpClient.newCall(request.build()).execute(); | |||
builder.setHttpStatus(response.code()); | |||
builder.setDurationInMs(system.now() - startedAt); | |||
builder.setDurationInMs((int)(system.now() - startedAt)); | |||
} catch (IOException e) { | |||
builder.setThrowable(e); | |||
builder.setError(e); | |||
} | |||
return builder.build(); | |||
} |
@@ -23,6 +23,7 @@ import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import javax.annotation.concurrent.Immutable; | |||
import static com.google.common.base.Throwables.getRootCause; | |||
import static java.util.Objects.requireNonNull; | |||
/** | |||
@@ -34,9 +35,9 @@ public class WebhookDelivery { | |||
private final Webhook webhook; | |||
private final WebhookPayload payload; | |||
private final Integer httpStatus; | |||
private final Long durationInMs; | |||
private final Integer durationInMs; | |||
private final long at; | |||
private final Throwable throwable; | |||
private final Throwable error; | |||
private WebhookDelivery(Builder builder) { | |||
this.webhook = requireNonNull(builder.webhook); | |||
@@ -44,7 +45,7 @@ public class WebhookDelivery { | |||
this.httpStatus = builder.httpStatus; | |||
this.durationInMs = builder.durationInMs; | |||
this.at = builder.at; | |||
this.throwable = builder.throwable; | |||
this.error = builder.error; | |||
} | |||
public Webhook getWebhook() { | |||
@@ -56,7 +57,7 @@ public class WebhookDelivery { | |||
} | |||
/** | |||
* @return the HTTP status if {@link #getThrowable()} is empty, else returns | |||
* @return the HTTP status if {@link #getError()} is empty, else returns | |||
* {@link Optional#empty()} | |||
*/ | |||
public Optional<Integer> getHttpStatus() { | |||
@@ -64,10 +65,10 @@ public class WebhookDelivery { | |||
} | |||
/** | |||
* @return the duration in milliseconds if {@link #getThrowable()} is empty, | |||
* @return the duration in milliseconds if {@link #getError()} is empty, | |||
* else returns {@link Optional#empty()} | |||
*/ | |||
public Optional<Long> getDurationInMs() { | |||
public Optional<Integer> getDurationInMs() { | |||
return Optional.ofNullable(durationInMs); | |||
} | |||
@@ -82,17 +83,28 @@ public class WebhookDelivery { | |||
* @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 Optional<Throwable> getError() { | |||
return Optional.ofNullable(error); | |||
} | |||
/** | |||
* @return the cause message of {@link #getError()}, Optional.empty() is error is not set. | |||
*/ | |||
public Optional<String> getErrorMessage() { | |||
return error != null ? Optional.ofNullable(getRootCause(error).getMessage()) : Optional.empty(); | |||
} | |||
public boolean isSuccess() { | |||
return httpStatus != null && httpStatus >= 200 && httpStatus < 300; | |||
} | |||
public static class Builder { | |||
private Webhook webhook; | |||
private WebhookPayload payload; | |||
private Integer httpStatus; | |||
private Long durationInMs; | |||
private Integer durationInMs; | |||
private long at; | |||
private Throwable throwable; | |||
private Throwable error; | |||
public Builder setWebhook(Webhook w) { | |||
this.webhook = w; | |||
@@ -109,7 +121,7 @@ public class WebhookDelivery { | |||
return this; | |||
} | |||
public Builder setDurationInMs(@Nullable Long durationInMs) { | |||
public Builder setDurationInMs(@Nullable Integer durationInMs) { | |||
this.durationInMs = durationInMs; | |||
return this; | |||
} | |||
@@ -119,8 +131,8 @@ public class WebhookDelivery { | |||
return this; | |||
} | |||
public Builder setThrowable(@Nullable Throwable t) { | |||
this.throwable = t; | |||
public Builder setError(@Nullable Throwable t) { | |||
this.error = t; | |||
return this; | |||
} | |||
@@ -0,0 +1,80 @@ | |||
/* | |||
* 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.base.Throwables; | |||
import org.sonar.api.ce.ComputeEngineSide; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.webhook.WebhookDeliveryDao; | |||
import org.sonar.db.webhook.WebhookDeliveryDto; | |||
/** | |||
* Persist and purge {@link WebhookDelivery} into database | |||
*/ | |||
@ComputeEngineSide | |||
public class WebhookDeliveryStorage { | |||
private static final long ALIVE_DELAY_MS = 30L * 24 * 60 * 60 * 1000; | |||
private final DbClient dbClient; | |||
private final System2 system; | |||
private final UuidFactory uuidFactory; | |||
public WebhookDeliveryStorage(DbClient dbClient, System2 system, UuidFactory uuidFactory) { | |||
this.dbClient = dbClient; | |||
this.system = system; | |||
this.uuidFactory = uuidFactory; | |||
} | |||
public void persist(WebhookDelivery delivery) { | |||
WebhookDeliveryDao dao = dbClient.webhookDeliveryDao(); | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
dao.insert(dbSession, toDto(delivery)); | |||
dbSession.commit(); | |||
} | |||
} | |||
public void purge(String componentUuid) { | |||
long beforeDate = system.now() - ALIVE_DELAY_MS; | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
dbClient.webhookDeliveryDao().deleteComponentBeforeDate(dbSession, componentUuid, beforeDate); | |||
dbSession.commit(); | |||
} | |||
} | |||
private WebhookDeliveryDto toDto(WebhookDelivery delivery) { | |||
WebhookDeliveryDto dto = new WebhookDeliveryDto(); | |||
dto.setUuid(uuidFactory.create()); | |||
dto.setComponentUuid(delivery.getWebhook().getComponentUuid()); | |||
dto.setCeTaskUuid(delivery.getWebhook().getCeTaskUuid()); | |||
dto.setName(delivery.getWebhook().getName()); | |||
dto.setUrl(delivery.getWebhook().getUrl()); | |||
dto.setSuccess(delivery.isSuccess()); | |||
dto.setHttpStatus(delivery.getHttpStatus().orElse(null)); | |||
dto.setDurationMs(delivery.getDurationInMs().orElse(null)); | |||
dto.setErrorStacktrace(delivery.getError().map(Throwables::getStackTraceAsString).orElse(null)); | |||
dto.setPayload(delivery.getPayload().toJson()); | |||
dto.setCreatedAt(delivery.getAt()); | |||
return dto; | |||
} | |||
} |
@@ -26,6 +26,7 @@ public class WebhookModule extends Module { | |||
protected void configureModule() { | |||
add( | |||
WebhookCallerImpl.class, | |||
WebhookDeliveryStorage.class, | |||
WebhookPostTask.class); | |||
} | |||
} |
@@ -32,7 +32,6 @@ 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 { | |||
@@ -42,11 +41,14 @@ public class WebhookPostTask implements PostProjectAnalysisTask { | |||
private final TreeRootHolder rootHolder; | |||
private final SettingsRepository settingsRepository; | |||
private final WebhookCaller caller; | |||
private final WebhookDeliveryStorage deliveryStorage; | |||
public WebhookPostTask(TreeRootHolder rootHolder, SettingsRepository settingsRepository, WebhookCaller caller) { | |||
public WebhookPostTask(TreeRootHolder rootHolder, SettingsRepository settingsRepository, WebhookCaller caller, | |||
WebhookDeliveryStorage deliveryStorage) { | |||
this.rootHolder = rootHolder; | |||
this.settingsRepository = settingsRepository; | |||
this.caller = caller; | |||
this.deliveryStorage = deliveryStorage; | |||
} | |||
@Override | |||
@@ -55,10 +57,10 @@ public class WebhookPostTask implements PostProjectAnalysisTask { | |||
Iterable<String> webhookProps = Iterables.concat( | |||
getWebhookProperties(settings, WebhookProperties.GLOBAL_KEY), | |||
getWebhookProperties(settings, WebhookProperties.PROJECT_KEY) | |||
); | |||
getWebhookProperties(settings, WebhookProperties.PROJECT_KEY)); | |||
if (!Iterables.isEmpty(webhookProps)) { | |||
process(settings, analysis, webhookProps); | |||
deliveryStorage.purge(analysis.getProject().getUuid()); | |||
} | |||
} | |||
@@ -76,21 +78,22 @@ public class WebhookPostTask implements PostProjectAnalysisTask { | |||
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); | |||
Webhook webhook = new Webhook(analysis.getProject().getUuid(), analysis.getCeTask().getId(), name, url); | |||
WebhookDelivery delivery = caller.call(webhook, payload); | |||
log(delivery); | |||
deliveryStorage.persist(delivery); | |||
} | |||
} | |||
} | |||
private static void log(WebhookDelivery delivery) { | |||
Optional<Throwable> throwable = delivery.getThrowable(); | |||
if (throwable.isPresent()) { | |||
Optional<String> error = delivery.getErrorMessage(); | |||
if (error.isPresent()) { | |||
LOGGER.debug("Failed to send webhook '{}' | url={} | message={}", | |||
delivery.getWebhook().getName(), delivery.getWebhook().getUrl(), getRootCause(throwable.get()).getMessage()); | |||
delivery.getWebhook().getName(), delivery.getWebhook().getUrl(), error.get()); | |||
} else { | |||
LOGGER.debug("Sent webhook '{}' | url={} | time={}ms | status={}", | |||
delivery.getWebhook().getName(), delivery.getWebhook().getUrl(), delivery.getDurationInMs().orElse(-1L), delivery.getHttpStatus().orElse(-1)); | |||
delivery.getWebhook().getName(), delivery.getWebhook().getUrl(), delivery.getDurationInMs().orElse(-1), delivery.getHttpStatus().orElse(-1)); | |||
} | |||
} | |||
} |
@@ -31,7 +31,7 @@ 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) { | |||
public TestWebhookCaller enqueueSuccess(long at, int httpCode, int durationMs) { | |||
deliveries.add(new Item(at, httpCode, durationMs, null)); | |||
return this; | |||
} | |||
@@ -49,7 +49,7 @@ public class TestWebhookCaller implements WebhookCaller { | |||
.setAt(item.at) | |||
.setHttpStatus(item.httpCode) | |||
.setDurationInMs(item.durationMs) | |||
.setThrowable(item.throwable) | |||
.setError(item.throwable) | |||
.setPayload(payload) | |||
.setWebhook(webhook) | |||
.build(); | |||
@@ -62,10 +62,10 @@ public class TestWebhookCaller implements WebhookCaller { | |||
private static class Item { | |||
final long at; | |||
final Integer httpCode; | |||
final Long durationMs; | |||
final Integer durationMs; | |||
final Throwable throwable; | |||
Item(long at, @Nullable Integer httpCode, @Nullable Long durationMs, @Nullable Throwable throwable) { | |||
Item(long at, @Nullable Integer httpCode, @Nullable Integer durationMs, @Nullable Throwable throwable) { | |||
this.at = at; | |||
this.httpCode = httpCode; | |||
this.durationMs = durationMs; |
@@ -39,6 +39,8 @@ import static org.assertj.core.api.Assertions.assertThat; | |||
public class WebhookCallerImplTest { | |||
private static final long NOW = 1_500_000_000_000L; | |||
private static final String PROJECT_UUID = "P_UUID1"; | |||
private static final String CE_TASK_UUID = "CE_UUID1"; | |||
@Rule | |||
public MockWebServer server = new MockWebServer(); | |||
@@ -47,15 +49,15 @@ public class WebhookCallerImplTest { | |||
@Test | |||
public void send_posts_payload_to_http_server() throws Exception { | |||
Webhook webhook = new Webhook("my-webhook", server.url("/ping").toString()); | |||
Webhook webhook = new Webhook(PROJECT_UUID, CE_TASK_UUID, "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.getDurationInMs().get()).isGreaterThanOrEqualTo(0); | |||
assertThat(delivery.getError()).isEmpty(); | |||
assertThat(delivery.getAt()).isEqualTo(NOW); | |||
assertThat(delivery.getWebhook()).isSameAs(webhook); | |||
assertThat(delivery.getPayload()).isSameAs(payload); | |||
@@ -71,7 +73,7 @@ public class WebhookCallerImplTest { | |||
@Test | |||
public void send_does_not_throw_exception_on_errors() throws Exception { | |||
Webhook webhook = new Webhook("my-webhook", server.url("/ping").toString()); | |||
Webhook webhook = new Webhook(PROJECT_UUID, CE_TASK_UUID, "my-webhook", server.url("/ping").toString()); | |||
WebhookPayload payload = new WebhookPayload("P1", "{the payload}"); | |||
server.shutdown(); | |||
@@ -79,7 +81,7 @@ public class WebhookCallerImplTest { | |||
assertThat(delivery.getHttpStatus()).isEmpty(); | |||
assertThat(delivery.getDurationInMs()).isEmpty(); | |||
assertThat(delivery.getThrowable().get().getMessage()).startsWith("Failed to connect to"); | |||
assertThat(delivery.getErrorMessage().get()).startsWith("Failed to connect"); | |||
assertThat(delivery.getAt()).isEqualTo(NOW); | |||
assertThat(delivery.getWebhook()).isSameAs(webhook); | |||
assertThat(delivery.getPayload()).isSameAs(payload); |
@@ -0,0 +1,124 @@ | |||
/* | |||
* 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.List; | |||
import java.util.Map; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.webhook.WebhookDeliveryDto; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
public class WebhookDeliveryStorageTest { | |||
private static final String DELIVERY_UUID = "abcde1234"; | |||
private static final long NOW = 1_500_000_000_000L; | |||
private static final long TWO_MONTHS_AGO = NOW - 60L * 24 * 60 * 60 * 1000; | |||
private static final long TWO_WEEKS_AGO = NOW - 14L * 24 * 60 * 60 * 1000; | |||
private final System2 system = mock(System2.class); | |||
@Rule | |||
public final DbTester dbTester = DbTester.create(system).setDisableDefaultOrganization(true); | |||
private DbClient dbClient = dbTester.getDbClient(); | |||
private DbSession dbSession = dbTester.getSession(); | |||
private UuidFactory uuidFactory = mock(UuidFactory.class); | |||
private WebhookDeliveryStorage underTest = new WebhookDeliveryStorage(dbClient, system, uuidFactory); | |||
@Test | |||
public void persist_generates_uuid_then_inserts_record() { | |||
when(uuidFactory.create()).thenReturn(DELIVERY_UUID); | |||
WebhookDelivery delivery = newBuilderTemplate().build(); | |||
underTest.persist(delivery); | |||
WebhookDeliveryDto dto = dbClient.webhookDeliveryDao().selectByUuid(dbSession, DELIVERY_UUID).get(); | |||
assertThat(dto.getUuid()).isEqualTo(DELIVERY_UUID); | |||
assertThat(dto.getComponentUuid()).isEqualTo(delivery.getWebhook().getComponentUuid()); | |||
assertThat(dto.getCeTaskUuid()).isEqualTo(delivery.getWebhook().getCeTaskUuid()); | |||
assertThat(dto.getName()).isEqualTo(delivery.getWebhook().getName()); | |||
assertThat(dto.getUrl()).isEqualTo(delivery.getWebhook().getUrl()); | |||
assertThat(dto.getCreatedAt()).isEqualTo(delivery.getAt()); | |||
assertThat(dto.getHttpStatus()).isEqualTo(delivery.getHttpStatus().get()); | |||
assertThat(dto.getDurationMs()).isEqualTo(delivery.getDurationInMs().get()); | |||
assertThat(dto.getPayload()).isEqualTo(delivery.getPayload().toJson()); | |||
assertThat(dto.getErrorStacktrace()).isNull(); | |||
} | |||
@Test | |||
public void persist_error_stacktrace() { | |||
when(uuidFactory.create()).thenReturn(DELIVERY_UUID); | |||
WebhookDelivery delivery = newBuilderTemplate() | |||
.setError(new IOException("fail to connect")) | |||
.build(); | |||
underTest.persist(delivery); | |||
WebhookDeliveryDto dto = dbClient.webhookDeliveryDao().selectByUuid(dbSession, DELIVERY_UUID).get(); | |||
assertThat(dto.getErrorStacktrace()).contains("java.io.IOException", "fail to connect"); | |||
} | |||
@Test | |||
public void purge_deletes_records_older_than_one_month_on_the_project() { | |||
when(system.now()).thenReturn(NOW); | |||
dbClient.webhookDeliveryDao().insert(dbSession, newDto("D1", "PROJECT_1", TWO_MONTHS_AGO)); | |||
dbClient.webhookDeliveryDao().insert(dbSession, newDto("D2", "PROJECT_1", TWO_WEEKS_AGO)); | |||
dbClient.webhookDeliveryDao().insert(dbSession, newDto("D3", "PROJECT_2", TWO_MONTHS_AGO)); | |||
dbSession.commit(); | |||
underTest.purge("PROJECT_1"); | |||
List<Map<String, Object>> uuids = dbTester.select(dbSession, "select uuid as \"uuid\" from webhook_deliveries"); | |||
// do not purge another project PROJECT_2 | |||
assertThat(uuids).extracting(column -> column.get("uuid")).containsOnly("D2", "D3"); | |||
} | |||
private static WebhookDelivery.Builder newBuilderTemplate() { | |||
return new WebhookDelivery.Builder() | |||
.setWebhook(new Webhook("COMPONENT1", "TASK1", "Jenkins", "http://jenkins")) | |||
.setPayload(new WebhookPayload("my-project", "{json}")) | |||
.setAt(1_000_000L) | |||
.setHttpStatus(200) | |||
.setDurationInMs(1_000); | |||
} | |||
private static WebhookDeliveryDto newDto(String uuid, String componentUuid, long at) { | |||
return new WebhookDeliveryDto() | |||
.setUuid(uuid) | |||
.setComponentUuid(componentUuid) | |||
.setCeTaskUuid(randomAlphanumeric(40)) | |||
.setName("Jenkins") | |||
.setUrl("http://jenkins") | |||
.setSuccess(true) | |||
.setPayload("{json}") | |||
.setCreatedAt(at); | |||
} | |||
} |
@@ -0,0 +1,82 @@ | |||
/* | |||
* 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 org.junit.Test; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
public class WebhookDeliveryTest { | |||
@Test | |||
public void isSuccess_returns_false_if_failed_to_send_http_request() { | |||
WebhookDelivery delivery = newBuilderTemplate() | |||
.setError(new IOException("Fail to connect")) | |||
.build(); | |||
assertThat(delivery.isSuccess()).isFalse(); | |||
} | |||
@Test | |||
public void isSuccess_returns_false_if_http_response_returns_error_status() { | |||
WebhookDelivery delivery = newBuilderTemplate() | |||
.setHttpStatus(404) | |||
.build(); | |||
assertThat(delivery.isSuccess()).isFalse(); | |||
} | |||
@Test | |||
public void isSuccess_returns_true_if_http_response_returns_2xx_code() { | |||
WebhookDelivery delivery = newBuilderTemplate() | |||
.setHttpStatus(204) | |||
.build(); | |||
assertThat(delivery.isSuccess()).isTrue(); | |||
} | |||
@Test | |||
public void getErrorMessage_returns_empty_if_no_error() { | |||
WebhookDelivery delivery = newBuilderTemplate().build(); | |||
assertThat(delivery.getErrorMessage()).isEmpty(); | |||
} | |||
@Test | |||
public void getErrorMessage_returns_root_cause_message_if_error() { | |||
Exception rootCause = new IOException("fail to connect"); | |||
Exception cause = new IOException("nested", rootCause); | |||
WebhookDelivery delivery = newBuilderTemplate() | |||
.setError(cause) | |||
.build(); | |||
assertThat(delivery.getErrorMessage().get()).isEqualTo("fail to connect"); | |||
} | |||
private static WebhookDelivery.Builder newBuilderTemplate() { | |||
return new WebhookDelivery.Builder() | |||
.setWebhook(mock(Webhook.class)) | |||
.setPayload(mock(WebhookPayload.class)) | |||
.setAt(1_000L); | |||
} | |||
} |
@@ -33,6 +33,11 @@ import org.sonar.server.computation.task.projectanalysis.component.TestSettingsR | |||
import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolderRule; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Matchers.any; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.times; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.verifyZeroInteractions; | |||
import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newCeTaskBuilder; | |||
import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newProjectBuilder; | |||
import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newScannerContextBuilder; | |||
@@ -41,6 +46,7 @@ import static org.sonar.server.computation.task.projectanalysis.component.Report | |||
public class WebhookPostTaskTest { | |||
private static final long NOW = 1_500_000_000_000L; | |||
private static final String PROJECT_UUID = "P1_UUID"; | |||
@Rule | |||
public LogTester logTester = new LogTester().setLevel(LoggerLevel.DEBUG); | |||
@@ -50,6 +56,7 @@ public class WebhookPostTaskTest { | |||
private final MapSettings settings = new MapSettings(); | |||
private final TestWebhookCaller caller = new TestWebhookCaller(); | |||
private final WebhookDeliveryStorage deliveryStorage = mock(WebhookDeliveryStorage.class); | |||
@Test | |||
public void do_nothing_if_no_webhooks() { | |||
@@ -57,6 +64,7 @@ public class WebhookPostTaskTest { | |||
assertThat(caller.countSent()).isEqualTo(0); | |||
assertThat(logTester.logs(LoggerLevel.DEBUG)).isEmpty(); | |||
verifyZeroInteractions(deliveryStorage); | |||
} | |||
@Test | |||
@@ -74,6 +82,8 @@ public class WebhookPostTaskTest { | |||
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"); | |||
verify(deliveryStorage, times(2)).persist(any(WebhookDelivery.class)); | |||
verify(deliveryStorage).purge(PROJECT_UUID); | |||
} | |||
@Test | |||
public void send_project_webhooks() { | |||
@@ -86,11 +96,13 @@ public class WebhookPostTaskTest { | |||
assertThat(caller.countSent()).isEqualTo(1); | |||
assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200"); | |||
verify(deliveryStorage).persist(any(WebhookDelivery.class)); | |||
verify(deliveryStorage).purge(PROJECT_UUID); | |||
} | |||
private void execute() { | |||
SettingsRepository settingsRepository = new TestSettingsRepository(settings); | |||
WebhookPostTask task = new WebhookPostTask(rootHolder, settingsRepository, caller); | |||
WebhookPostTask task = new WebhookPostTask(rootHolder, settingsRepository, caller, deliveryStorage); | |||
PostProjectAnalysisTaskTester.of(task) | |||
.at(new Date()) | |||
@@ -99,7 +111,7 @@ public class WebhookPostTaskTest { | |||
.setId("#1") | |||
.build()) | |||
.withProject(newProjectBuilder() | |||
.setUuid("P1_UUID") | |||
.setUuid(PROJECT_UUID) | |||
.setKey("P1") | |||
.setName("Project One") | |||
.build()) |
@@ -59,8 +59,8 @@ public class WebhookDeliveryDto extends WebhookDeliveryLiteDto<WebhookDeliveryDt | |||
.append("httpStatus", httpStatus) | |||
.append("durationMs", durationMs) | |||
.append("url", url) | |||
.append("createdAt", createdAt) | |||
.append("errorStacktrace", errorStacktrace) | |||
.append("createdAt", createdAt) | |||
.toString(); | |||
} | |||
} |
@@ -20,8 +20,8 @@ | |||
package org.sonar.db.webhook; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.stream.Collectors; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
@@ -32,11 +32,10 @@ import org.sonar.db.DbTester; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class WebhookDeliveryDaoTest { | |||
private static final long DATE_1 = 1_999_000L; | |||
@Rule | |||
public final DbTester dbTester = DbTester.create(System2.INSTANCE).setDisableDefaultOrganization(true); | |||
@@ -47,6 +46,11 @@ public class WebhookDeliveryDaoTest { | |||
private final DbSession dbSession = dbTester.getSession(); | |||
private final WebhookDeliveryDao underTest = dbClient.webhookDeliveryDao(); | |||
@Test | |||
public void selectByUuid_returns_empty_if_uuid_does_not_exist() { | |||
assertThat(underTest.selectByUuid(dbSession, "missing")).isEmpty(); | |||
} | |||
@Test | |||
public void insert_row_with_only_mandatory_columns() { | |||
WebhookDeliveryDto dto = newDto("DELIVERY_1", "COMPONENT_1", "TASK_1"); | |||
@@ -80,7 +84,7 @@ public class WebhookDeliveryDaoTest { | |||
} | |||
@Test | |||
public void delete_rows_before_date() { | |||
public void deleteComponentBeforeDate_deletes_rows_before_date() { | |||
underTest.insert(dbSession, newDto("DELIVERY_1", "COMPONENT_1", "TASK_1").setCreatedAt(1_000_000L)); | |||
underTest.insert(dbSession, newDto("DELIVERY_2", "COMPONENT_1", "TASK_2").setCreatedAt(2_000_000L)); | |||
underTest.insert(dbSession, newDto("DELIVERY_3", "COMPONENT_2", "TASK_3").setCreatedAt(1_000_000L)); | |||
@@ -88,12 +92,24 @@ public class WebhookDeliveryDaoTest { | |||
// should delete the old delivery on COMPONENT_1 and keep the one of COMPONENT_2 | |||
underTest.deleteComponentBeforeDate(dbSession, "COMPONENT_1", 1_500_000L); | |||
List<Object> uuids = dbTester.select(dbSession, "select uuid as \"uuid\" from webhook_deliveries") | |||
.stream() | |||
.map(columns -> columns.get("uuid")) | |||
.collect(Collectors.toList()); | |||
assertThat(uuids).containsOnly("DELIVERY_2", "DELIVERY_3"); | |||
List<Map<String, Object>> uuids = dbTester.select(dbSession, "select uuid as \"uuid\" from webhook_deliveries"); | |||
assertThat(uuids).extracting(column -> column.get("uuid")).containsOnly("DELIVERY_2", "DELIVERY_3"); | |||
} | |||
@Test | |||
public void deleteComponentBeforeDate_does_nothing_on_empty_table() { | |||
underTest.deleteComponentBeforeDate(dbSession, "COMPONENT_1", 1_500_000L); | |||
assertThat(dbTester.countRowsOfTable(dbSession, "webhook_deliveries")).isEqualTo(0); | |||
} | |||
@Test | |||
public void deleteComponentBeforeDate_does_nothing_on_invalid_uuid() { | |||
underTest.insert(dbSession, newDto("DELIVERY_1", "COMPONENT_1", "TASK_1").setCreatedAt(1_000_000L)); | |||
underTest.deleteComponentBeforeDate(dbSession, "COMPONENT_2", 1_500_000L); | |||
assertThat(dbTester.countRowsOfTable(dbSession, "webhook_deliveries")).isEqualTo(1); | |||
} | |||
private void verifyMandatoryFields(WebhookDeliveryDto expected, WebhookDeliveryDto actual) { |