Browse Source

SONAR-8353 persist deliveries in Compute Engine

Deliveries older than 1 month are purged.
tags/6.2-RC1
Simon Brandhof 7 years ago
parent
commit
34cdbe891e
13 changed files with 390 additions and 46 deletions
  1. 13
    1
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java
  2. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java
  3. 25
    13
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java
  4. 80
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDeliveryStorage.java
  5. 1
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java
  6. 12
    9
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java
  7. 4
    4
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java
  8. 7
    5
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java
  9. 124
    0
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDeliveryStorageTest.java
  10. 82
    0
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDeliveryTest.java
  11. 14
    2
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java
  12. 1
    1
      sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryDto.java
  13. 25
    9
      sonar-db/src/test/java/org/sonar/db/webhook/WebhookDeliveryDaoTest.java

+ 13
- 1
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java View File

@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;
} }

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java View File

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();
} }

+ 25
- 13
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java View File

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;
} }



+ 80
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDeliveryStorage.java View File

/*
* 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;
}
}

+ 1
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java View File

protected void configureModule() { protected void configureModule() {
add( add(
WebhookCallerImpl.class, WebhookCallerImpl.class,
WebhookDeliveryStorage.class,
WebhookPostTask.class); WebhookPostTask.class);
} }
} }

+ 12
- 9
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java View File

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));
} }
} }
} }

+ 4
- 4
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java View File

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;

+ 7
- 5
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java View File

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);

+ 124
- 0
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDeliveryStorageTest.java View File

/*
* 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);
}
}

+ 82
- 0
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDeliveryTest.java View File

/*
* 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);
}
}

+ 14
- 2
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java View File

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())

+ 1
- 1
sonar-db/src/main/java/org/sonar/db/webhook/WebhookDeliveryDto.java View File

.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();
} }
} }

+ 25
- 9
sonar-db/src/test/java/org/sonar/db/webhook/WebhookDeliveryDaoTest.java View File

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) {

Loading…
Cancel
Save