Deliveries older than 1 month are purged.tags/6.2-RC1
@Immutable | @Immutable | ||||
public class Webhook { | public class Webhook { | ||||
private final String componentUuid; | |||||
private final String ceTaskUuid; | |||||
private final String name; | private final String name; | ||||
private final String url; | 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.name = requireNonNull(name); | ||||
this.url = requireNonNull(url); | this.url = requireNonNull(url); | ||||
} | } | ||||
public String getComponentUuid() { | |||||
return componentUuid; | |||||
} | |||||
public String getCeTaskUuid() { | |||||
return ceTaskUuid; | |||||
} | |||||
public String getName() { | public String getName() { | ||||
return name; | return name; | ||||
} | } |
try { | try { | ||||
Response response = okHttpClient.newCall(request.build()).execute(); | Response response = okHttpClient.newCall(request.build()).execute(); | ||||
builder.setHttpStatus(response.code()); | builder.setHttpStatus(response.code()); | ||||
builder.setDurationInMs(system.now() - startedAt); | |||||
builder.setDurationInMs((int)(system.now() - startedAt)); | |||||
} catch (IOException e) { | } catch (IOException e) { | ||||
builder.setThrowable(e); | |||||
builder.setError(e); | |||||
} | } | ||||
return builder.build(); | return builder.build(); | ||||
} | } |
import javax.annotation.Nullable; | import javax.annotation.Nullable; | ||||
import javax.annotation.concurrent.Immutable; | import javax.annotation.concurrent.Immutable; | ||||
import static com.google.common.base.Throwables.getRootCause; | |||||
import static java.util.Objects.requireNonNull; | import static java.util.Objects.requireNonNull; | ||||
/** | /** | ||||
private final Webhook webhook; | private final Webhook webhook; | ||||
private final WebhookPayload payload; | private final WebhookPayload payload; | ||||
private final Integer httpStatus; | private final Integer httpStatus; | ||||
private final Long durationInMs; | |||||
private final Integer durationInMs; | |||||
private final long at; | private final long at; | ||||
private final Throwable throwable; | |||||
private final Throwable error; | |||||
private WebhookDelivery(Builder builder) { | private WebhookDelivery(Builder builder) { | ||||
this.webhook = requireNonNull(builder.webhook); | this.webhook = requireNonNull(builder.webhook); | ||||
this.httpStatus = builder.httpStatus; | this.httpStatus = builder.httpStatus; | ||||
this.durationInMs = builder.durationInMs; | this.durationInMs = builder.durationInMs; | ||||
this.at = builder.at; | this.at = builder.at; | ||||
this.throwable = builder.throwable; | |||||
this.error = builder.error; | |||||
} | } | ||||
public Webhook getWebhook() { | public Webhook getWebhook() { | ||||
} | } | ||||
/** | /** | ||||
* @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()} | * {@link Optional#empty()} | ||||
*/ | */ | ||||
public Optional<Integer> getHttpStatus() { | public Optional<Integer> getHttpStatus() { | ||||
} | } | ||||
/** | /** | ||||
* @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()} | * else returns {@link Optional#empty()} | ||||
*/ | */ | ||||
public Optional<Long> getDurationInMs() { | |||||
public Optional<Integer> getDurationInMs() { | |||||
return Optional.ofNullable(durationInMs); | return Optional.ofNullable(durationInMs); | ||||
} | } | ||||
* @return the error raised if the request could not be executed due to a connectivity | * @return the error raised if the request could not be executed due to a connectivity | ||||
* problem or timeout | * 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 { | public static class Builder { | ||||
private Webhook webhook; | private Webhook webhook; | ||||
private WebhookPayload payload; | private WebhookPayload payload; | ||||
private Integer httpStatus; | private Integer httpStatus; | ||||
private Long durationInMs; | |||||
private Integer durationInMs; | |||||
private long at; | private long at; | ||||
private Throwable throwable; | |||||
private Throwable error; | |||||
public Builder setWebhook(Webhook w) { | public Builder setWebhook(Webhook w) { | ||||
this.webhook = w; | this.webhook = w; | ||||
return this; | return this; | ||||
} | } | ||||
public Builder setDurationInMs(@Nullable Long durationInMs) { | |||||
public Builder setDurationInMs(@Nullable Integer durationInMs) { | |||||
this.durationInMs = durationInMs; | this.durationInMs = durationInMs; | ||||
return this; | return this; | ||||
} | } | ||||
return this; | return this; | ||||
} | } | ||||
public Builder setThrowable(@Nullable Throwable t) { | |||||
this.throwable = t; | |||||
public Builder setError(@Nullable Throwable t) { | |||||
this.error = t; | |||||
return this; | return this; | ||||
} | } | ||||
/* | |||||
* 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; | |||||
} | |||||
} |
protected void configureModule() { | protected void configureModule() { | ||||
add( | add( | ||||
WebhookCallerImpl.class, | WebhookCallerImpl.class, | ||||
WebhookDeliveryStorage.class, | |||||
WebhookPostTask.class); | WebhookPostTask.class); | ||||
} | } | ||||
} | } |
import org.sonar.server.computation.task.projectanalysis.component.SettingsRepository; | import org.sonar.server.computation.task.projectanalysis.component.SettingsRepository; | ||||
import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolder; | import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolder; | ||||
import static com.google.common.base.Throwables.getRootCause; | |||||
import static java.lang.String.format; | import static java.lang.String.format; | ||||
public class WebhookPostTask implements PostProjectAnalysisTask { | public class WebhookPostTask implements PostProjectAnalysisTask { | ||||
private final TreeRootHolder rootHolder; | private final TreeRootHolder rootHolder; | ||||
private final SettingsRepository settingsRepository; | private final SettingsRepository settingsRepository; | ||||
private final WebhookCaller caller; | 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.rootHolder = rootHolder; | ||||
this.settingsRepository = settingsRepository; | this.settingsRepository = settingsRepository; | ||||
this.caller = caller; | this.caller = caller; | ||||
this.deliveryStorage = deliveryStorage; | |||||
} | } | ||||
@Override | @Override | ||||
Iterable<String> webhookProps = Iterables.concat( | Iterable<String> webhookProps = Iterables.concat( | ||||
getWebhookProperties(settings, WebhookProperties.GLOBAL_KEY), | getWebhookProperties(settings, WebhookProperties.GLOBAL_KEY), | ||||
getWebhookProperties(settings, WebhookProperties.PROJECT_KEY) | |||||
); | |||||
getWebhookProperties(settings, WebhookProperties.PROJECT_KEY)); | |||||
if (!Iterables.isEmpty(webhookProps)) { | if (!Iterables.isEmpty(webhookProps)) { | ||||
process(settings, analysis, webhookProps); | process(settings, analysis, webhookProps); | ||||
deliveryStorage.purge(analysis.getProject().getUuid()); | |||||
} | } | ||||
} | } | ||||
String url = settings.getString(format("%s.%s", webhookProp, WebhookProperties.URL_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. | // as webhooks are defined as property sets, we can't ensure validity of fields on creation. | ||||
if (name != null && url != null) { | 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); | WebhookDelivery delivery = caller.call(webhook, payload); | ||||
log(delivery); | log(delivery); | ||||
deliveryStorage.persist(delivery); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
private static void log(WebhookDelivery 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={}", | 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 { | } else { | ||||
LOGGER.debug("Sent webhook '{}' | url={} | time={}ms | status={}", | 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)); | |||||
} | } | ||||
} | } | ||||
} | } |
private final Queue<Item> deliveries = new LinkedList<>(); | private final Queue<Item> deliveries = new LinkedList<>(); | ||||
private final AtomicInteger countSent = new AtomicInteger(0); | 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)); | deliveries.add(new Item(at, httpCode, durationMs, null)); | ||||
return this; | return this; | ||||
} | } | ||||
.setAt(item.at) | .setAt(item.at) | ||||
.setHttpStatus(item.httpCode) | .setHttpStatus(item.httpCode) | ||||
.setDurationInMs(item.durationMs) | .setDurationInMs(item.durationMs) | ||||
.setThrowable(item.throwable) | |||||
.setError(item.throwable) | |||||
.setPayload(payload) | .setPayload(payload) | ||||
.setWebhook(webhook) | .setWebhook(webhook) | ||||
.build(); | .build(); | ||||
private static class Item { | private static class Item { | ||||
final long at; | final long at; | ||||
final Integer httpCode; | final Integer httpCode; | ||||
final Long durationMs; | |||||
final Integer durationMs; | |||||
final Throwable throwable; | 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.at = at; | ||||
this.httpCode = httpCode; | this.httpCode = httpCode; | ||||
this.durationMs = durationMs; | this.durationMs = durationMs; |
public class WebhookCallerImplTest { | public class WebhookCallerImplTest { | ||||
private static final long NOW = 1_500_000_000_000L; | 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 | @Rule | ||||
public MockWebServer server = new MockWebServer(); | public MockWebServer server = new MockWebServer(); | ||||
@Test | @Test | ||||
public void send_posts_payload_to_http_server() throws Exception { | 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}"); | WebhookPayload payload = new WebhookPayload("P1", "{the payload}"); | ||||
server.enqueue(new MockResponse().setBody("pong").setResponseCode(201)); | server.enqueue(new MockResponse().setBody("pong").setResponseCode(201)); | ||||
WebhookDelivery delivery = newSender().call(webhook, payload); | WebhookDelivery delivery = newSender().call(webhook, payload); | ||||
assertThat(delivery.getHttpStatus().get()).isEqualTo(201); | 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.getAt()).isEqualTo(NOW); | ||||
assertThat(delivery.getWebhook()).isSameAs(webhook); | assertThat(delivery.getWebhook()).isSameAs(webhook); | ||||
assertThat(delivery.getPayload()).isSameAs(payload); | assertThat(delivery.getPayload()).isSameAs(payload); | ||||
@Test | @Test | ||||
public void send_does_not_throw_exception_on_errors() throws Exception { | 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}"); | WebhookPayload payload = new WebhookPayload("P1", "{the payload}"); | ||||
server.shutdown(); | server.shutdown(); | ||||
assertThat(delivery.getHttpStatus()).isEmpty(); | assertThat(delivery.getHttpStatus()).isEmpty(); | ||||
assertThat(delivery.getDurationInMs()).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.getAt()).isEqualTo(NOW); | ||||
assertThat(delivery.getWebhook()).isSameAs(webhook); | assertThat(delivery.getWebhook()).isSameAs(webhook); | ||||
assertThat(delivery.getPayload()).isSameAs(payload); | assertThat(delivery.getPayload()).isSameAs(payload); |
/* | |||||
* 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); | |||||
} | |||||
} |
/* | |||||
* 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); | |||||
} | |||||
} |
import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolderRule; | import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolderRule; | ||||
import static org.assertj.core.api.Assertions.assertThat; | 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.newCeTaskBuilder; | ||||
import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newProjectBuilder; | import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newProjectBuilder; | ||||
import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newScannerContextBuilder; | import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newScannerContextBuilder; | ||||
public class WebhookPostTaskTest { | public class WebhookPostTaskTest { | ||||
private static final long NOW = 1_500_000_000_000L; | private static final long NOW = 1_500_000_000_000L; | ||||
private static final String PROJECT_UUID = "P1_UUID"; | |||||
@Rule | @Rule | ||||
public LogTester logTester = new LogTester().setLevel(LoggerLevel.DEBUG); | public LogTester logTester = new LogTester().setLevel(LoggerLevel.DEBUG); | ||||
private final MapSettings settings = new MapSettings(); | private final MapSettings settings = new MapSettings(); | ||||
private final TestWebhookCaller caller = new TestWebhookCaller(); | private final TestWebhookCaller caller = new TestWebhookCaller(); | ||||
private final WebhookDeliveryStorage deliveryStorage = mock(WebhookDeliveryStorage.class); | |||||
@Test | @Test | ||||
public void do_nothing_if_no_webhooks() { | public void do_nothing_if_no_webhooks() { | ||||
assertThat(caller.countSent()).isEqualTo(0); | assertThat(caller.countSent()).isEqualTo(0); | ||||
assertThat(logTester.logs(LoggerLevel.DEBUG)).isEmpty(); | assertThat(logTester.logs(LoggerLevel.DEBUG)).isEmpty(); | ||||
verifyZeroInteractions(deliveryStorage); | |||||
} | } | ||||
@Test | @Test | ||||
assertThat(caller.countSent()).isEqualTo(2); | 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("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"); | 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 | @Test | ||||
public void send_project_webhooks() { | public void send_project_webhooks() { | ||||
assertThat(caller.countSent()).isEqualTo(1); | assertThat(caller.countSent()).isEqualTo(1); | ||||
assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200"); | 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() { | private void execute() { | ||||
SettingsRepository settingsRepository = new TestSettingsRepository(settings); | SettingsRepository settingsRepository = new TestSettingsRepository(settings); | ||||
WebhookPostTask task = new WebhookPostTask(rootHolder, settingsRepository, caller); | |||||
WebhookPostTask task = new WebhookPostTask(rootHolder, settingsRepository, caller, deliveryStorage); | |||||
PostProjectAnalysisTaskTester.of(task) | PostProjectAnalysisTaskTester.of(task) | ||||
.at(new Date()) | .at(new Date()) | ||||
.setId("#1") | .setId("#1") | ||||
.build()) | .build()) | ||||
.withProject(newProjectBuilder() | .withProject(newProjectBuilder() | ||||
.setUuid("P1_UUID") | |||||
.setUuid(PROJECT_UUID) | |||||
.setKey("P1") | .setKey("P1") | ||||
.setName("Project One") | .setName("Project One") | ||||
.build()) | .build()) |
.append("httpStatus", httpStatus) | .append("httpStatus", httpStatus) | ||||
.append("durationMs", durationMs) | .append("durationMs", durationMs) | ||||
.append("url", url) | .append("url", url) | ||||
.append("createdAt", createdAt) | |||||
.append("errorStacktrace", errorStacktrace) | .append("errorStacktrace", errorStacktrace) | ||||
.append("createdAt", createdAt) | |||||
.toString(); | .toString(); | ||||
} | } | ||||
} | } |
package org.sonar.db.webhook; | package org.sonar.db.webhook; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | |||||
import java.util.Optional; | import java.util.Optional; | ||||
import java.util.stream.Collectors; | |||||
import org.junit.Rule; | import org.junit.Rule; | ||||
import org.junit.Test; | import org.junit.Test; | ||||
import org.junit.rules.ExpectedException; | import org.junit.rules.ExpectedException; | ||||
import static org.assertj.core.api.Assertions.assertThat; | import static org.assertj.core.api.Assertions.assertThat; | ||||
public class WebhookDeliveryDaoTest { | public class WebhookDeliveryDaoTest { | ||||
private static final long DATE_1 = 1_999_000L; | private static final long DATE_1 = 1_999_000L; | ||||
@Rule | @Rule | ||||
public final DbTester dbTester = DbTester.create(System2.INSTANCE).setDisableDefaultOrganization(true); | public final DbTester dbTester = DbTester.create(System2.INSTANCE).setDisableDefaultOrganization(true); | ||||
private final DbSession dbSession = dbTester.getSession(); | private final DbSession dbSession = dbTester.getSession(); | ||||
private final WebhookDeliveryDao underTest = dbClient.webhookDeliveryDao(); | private final WebhookDeliveryDao underTest = dbClient.webhookDeliveryDao(); | ||||
@Test | |||||
public void selectByUuid_returns_empty_if_uuid_does_not_exist() { | |||||
assertThat(underTest.selectByUuid(dbSession, "missing")).isEmpty(); | |||||
} | |||||
@Test | @Test | ||||
public void insert_row_with_only_mandatory_columns() { | public void insert_row_with_only_mandatory_columns() { | ||||
WebhookDeliveryDto dto = newDto("DELIVERY_1", "COMPONENT_1", "TASK_1"); | WebhookDeliveryDto dto = newDto("DELIVERY_1", "COMPONENT_1", "TASK_1"); | ||||
} | } | ||||
@Test | @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_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_2", "COMPONENT_1", "TASK_2").setCreatedAt(2_000_000L)); | ||||
underTest.insert(dbSession, newDto("DELIVERY_3", "COMPONENT_2", "TASK_3").setCreatedAt(1_000_000L)); | underTest.insert(dbSession, newDto("DELIVERY_3", "COMPONENT_2", "TASK_3").setCreatedAt(1_000_000L)); | ||||
// should delete the old delivery on COMPONENT_1 and keep the one of COMPONENT_2 | // should delete the old delivery on COMPONENT_1 and keep the one of COMPONENT_2 | ||||
underTest.deleteComponentBeforeDate(dbSession, "COMPONENT_1", 1_500_000L); | 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) { | private void verifyMandatoryFields(WebhookDeliveryDto expected, WebhookDeliveryDto actual) { |