Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mock http servers update #1437

Closed
3 changes: 3 additions & 0 deletions cve-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
<cve>CVE-2021-4235</cve>
<cve>CVE-2022-45688</cve>
<cve>CVE-2020-8908</cve>
<!--TODO: temp, delete-->
<cve>CVE-2023-20861</cve>
<cve>CVE-2023-1370</cve>
</suppress>

<!-- https://github.com/jeremylong/DependencyCheck/issues/1921 -->
Expand Down
6 changes: 6 additions & 0 deletions riptide-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@
<version>2.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
92 changes: 92 additions & 0 deletions riptide-failsafe/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
<dependency>
<groupId>com.github.rest-driver</groupId>
<artifactId>rest-client-driver</artifactId>
<exclusions>
<exclusion>
<groupId>xmlunit</groupId>
<artifactId>xmlunit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
Expand All @@ -58,6 +64,92 @@
<artifactId>riptide-faults</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.10.0</version>
<scope>test</scope>
</dependency>
<!--TODO: Provides transitive vulnerable dependency maven:commons-fileupload:commons-fileupload:1.4
CVE-2023-24998 7.5 Allocation of Resources Without Limits or Throttling vulnerability with medium severity found-->
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>3.0.0-beta-2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>11.0.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>11.0.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>11.0.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>11.0.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>11.0.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<version>11.0.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-xml</artifactId>
<version>11.0.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>11.0.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-hpack</artifactId>
<version>11.0.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
<version>11.0.14</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-common</artifactId>
<version>11.0.14</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package org.zalando.riptide.failsafe;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.restdriver.clientdriver.ClientDriver;
import com.github.restdriver.clientdriver.ClientDriverFactory;
import dev.failsafe.CircuitBreaker;
import dev.failsafe.RetryPolicy;
import lombok.SneakyThrows;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.web.client.MockRestServiceServer;
import org.zalando.riptide.Http;
import org.zalando.riptide.Plugin;
import org.zalando.riptide.RequestExecution;
import org.zalando.riptide.httpclient.ApacheClientHttpRequestFactory;
import org.zalando.riptide.idempotency.IdempotencyPredicate;

import javax.annotation.Nullable;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.time.Duration;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicInteger;

import static com.github.restdriver.clientdriver.RestClientDriver.giveEmptyResponse;
import static com.github.restdriver.clientdriver.RestClientDriver.giveResponseAsBytes;
import static com.github.restdriver.clientdriver.RestClientDriver.onRequestTo;
import static com.google.common.io.Resources.getResource;
import static java.util.concurrent.Executors.newFixedThreadPool;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.stream.IntStream.range;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE;
import static org.springframework.http.HttpStatus.Series.SUCCESSFUL;
import static org.zalando.riptide.Attributes.RETRIES;
import static org.zalando.riptide.Bindings.anySeries;
import static org.zalando.riptide.Bindings.on;
import static org.zalando.riptide.Navigators.series;
import static org.zalando.riptide.Navigators.status;
import static org.zalando.riptide.PassRoute.pass;
import static org.zalando.riptide.Route.call;
import static org.zalando.riptide.failsafe.CheckedPredicateConverter.toCheckedPredicate;
import static org.zalando.riptide.failsafe.RetryRoute.retry;
import static org.zalando.riptide.faults.Predicates.alwaysTrue;
import static org.zalando.riptide.faults.TransientFaults.transientConnectionFaults;
import static org.zalando.riptide.faults.TransientFaults.transientSocketFaults;

final class HttpMockTestClientDriver {

private final ClientDriver driver = new ClientDriverFactory().createClientDriver();

private final CloseableHttpClient client = HttpClientBuilder.create()
.setDefaultRequestConfig(RequestConfig.custom()
.setSocketTimeout(500)
.build())
.build();

private final AtomicInteger attempt = new AtomicInteger();

private final Http unit = Http.builder()
.executor(newFixedThreadPool(2)) // to allow for nested calls
.requestFactory(new ApacheClientHttpRequestFactory(client))
.baseUrl(driver.getBaseUrl())
.converter(createJsonConverter())
.plugin(new Plugin() {
@Override
public RequestExecution aroundNetwork(final RequestExecution execution) {
return arguments -> {
arguments.getAttribute(RETRIES).ifPresent(attempt::set);
return execution.execute(arguments);
};
}
})
.plugin(new FailsafePlugin()
.withPolicy(new RetryRequestPolicy(
RetryPolicy.<ClientHttpResponse>builder()
.handleIf(toCheckedPredicate(transientSocketFaults()))
.handle(RetryException.class)
.handleResultIf(this::isBadGateway)
.withDelay(Duration.ofMillis(500))
.withMaxRetries(4)
.build())
.withPredicate(new IdempotencyPredicate()))
.withPolicy(new RetryRequestPolicy(
RetryPolicy.<ClientHttpResponse>builder()
.handleIf(toCheckedPredicate(transientConnectionFaults()))
.withDelay(Duration.ofMillis(500))
.withMaxRetries(4)
.build())
.withPredicate(alwaysTrue()))
.withPolicy(CircuitBreaker.<ClientHttpResponse>builder()
.withFailureThreshold(5, 10)
.withSuccessThreshold(5)
.withDelay(Duration.ofMinutes(1))
.build()))
.build();

@SneakyThrows
private boolean isBadGateway(@Nullable final ClientHttpResponse response) {
return response != null && response.getStatusCode() == HttpStatus.BAD_GATEWAY;
}

@AfterEach
void tearDown() throws IOException {
driver.verify();
client.close();
}

@SneakyThrows
@Test
void shouldRetrySuccessfully() {
driver.addExpectation(onRequestTo("/foo"), giveEmptyResponse().after(800, MILLISECONDS));
driver.addExpectation(onRequestTo("/foo"), giveResponseAsBytes(getResource("contributors.json").openStream(), "application/json"));

unit.get("/foo")
.call(pass())
.join();
}


@Test
void shouldRetryUnsuccessfully() {
range(0, 5).forEach(i ->
driver.addExpectation(onRequestTo("/bar"), giveEmptyResponse().after(800, MILLISECONDS)));

final CompletionException exception = assertThrows(CompletionException.class,
unit.get("/bar").call(pass())::join);

assertThat(exception.getCause(), is(instanceOf(SocketTimeoutException.class)));
}

@Test
void shouldRetryExplicitly() {
driver.addExpectation(onRequestTo("/baz"), giveEmptyResponse().withStatus(503).withHeader("X-RateLimit-Reset", "1523486068"));
driver.addExpectation(onRequestTo("/baz"), giveEmptyResponse());

unit.get("/baz")
.dispatch(series(),
on(SUCCESSFUL).call(pass()),
anySeries().dispatch(status(),
on(SERVICE_UNAVAILABLE).call(retry())))
.join();
}

@Test
void shouldAllowNestedCalls() {
driver.addExpectation(onRequestTo("/foo"), giveEmptyResponse());
driver.addExpectation(onRequestTo("/bar"), giveEmptyResponse());

assertTimeout(Duration.ofSeconds(1),
unit.get("/foo")
.call(call(() -> unit.get("/bar").call(pass()).join()))::join);
}


private static MappingJackson2HttpMessageConverter createJsonConverter() {
final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(createObjectMapper());
return converter;
}

private static ObjectMapper createObjectMapper() {
return new ObjectMapper().findAndRegisterModules()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
}
Loading