diff --git a/pom.xml b/pom.xml
index e925ca42..c8095c6a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -253,6 +253,21 @@ OF THE POSSIBILITY OF SUCH DAMAGE.
provided
true
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+ org.glassfish
+ javax.json
+ provided
+
@@ -263,6 +278,20 @@ OF THE POSSIBILITY OF SUCH DAMAGE.
import
pom
+
+ org.testcontainers
+ testcontainers-bom
+ 1.15.3
+ pom
+ import
+
+
+ org.junit
+ junit-bom
+ 5.7.2
+ pom
+ import
+
diff --git a/src/test/java/com/jcabi/http/RequestITCaseTemplate.java b/src/test/java/com/jcabi/http/RequestITCaseTemplate.java
new file mode 100644
index 00000000..8f43acea
--- /dev/null
+++ b/src/test/java/com/jcabi/http/RequestITCaseTemplate.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (c) 2011-2017, jcabi.com
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met: 1) Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer. 2) Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution. 3) Neither the name of the jcabi.com nor
+ * the names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
+ * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jcabi.http;
+
+import com.jcabi.aspects.Tv;
+import com.jcabi.http.request.JdkRequest;
+import com.jcabi.http.response.JsonResponse;
+import com.jcabi.http.response.RestResponse;
+import com.jcabi.http.response.XmlResponse;
+import com.jcabi.http.wire.AutoRedirectingWire;
+import com.jcabi.http.wire.BasicAuthWire;
+import com.jcabi.http.wire.CookieOptimizingWire;
+import com.jcabi.xml.XML;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.Locale;
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonValue;
+import javax.ws.rs.core.HttpHeaders;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIf;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests for any implementation of {@link Request}.
+ *
+ * @checkstyle JavadocMethodCheck (500 lines)
+ * @since 1.17.8
+ */
+@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
+@SuppressWarnings({"PMD.AbstractClassWithoutAbstractMethod", "PMD.TooManyMethods"})
+public abstract class RequestITCaseTemplate {
+
+ /**
+ * Type of Request.
+ */
+ private final Class extends Request> type;
+
+ /**
+ * Base URI.
+ */
+ private final URI uri;
+
+ /**
+ * Make request for a specific path.
+ * @param path Path.
+ * @return Request.
+ */
+ protected final Request request(final String path) {
+ return RequestTestTemplate.request(
+ this.uri.resolve(path),
+ this.type
+ );
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {
+ HttpURLConnection.HTTP_NOT_FOUND,
+ HttpURLConnection.HTTP_OK,
+ HttpURLConnection.HTTP_UNAVAILABLE
+ })
+ final void readsReturnStatusCode(final int code) throws IOException {
+ this.request(String.format("/status/%d", code))
+ .fetch()
+ .as(RestResponse.class)
+ .assertStatus(code);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ Request.DELETE,
+ Request.GET,
+ Request.POST,
+ Request.PUT
+ }
+ )
+ final void readsReturnStatusCode(final String method) throws IOException {
+ this.request(String.format("/%s", method.toLowerCase(Locale.ENGLISH)))
+ .method(method)
+ .fetch()
+ .as(RestResponse.class)
+ .assertStatus(HttpURLConnection.HTTP_OK);
+ }
+
+ @Test
+ final void sendsCookies() throws IOException {
+ MatcherAssert.assertThat(
+ "Must send cookies",
+ this.request("/cookies")
+ .header(HttpHeaders.COOKIE, "foo=bar")
+ .header(HttpHeaders.COOKIE, "baz=foz")
+ .through(CookieOptimizingWire.class)
+ .fetch()
+ .as(JsonResponse.class)
+ .json()
+ .readObject()
+ .getJsonObject("cookies"),
+ Matchers.allOf(
+ Matchers.hasEntry(
+ Matchers.is("foo"),
+ Matchers.is(Json.createValue("bar"))
+ ),
+ Matchers.hasEntry(
+ Matchers.is("baz"),
+ Matchers.is(Json.createValue("foz"))
+ )
+ )
+ );
+ }
+
+ @Test
+ final void followsLocationHeader() throws IOException {
+ this.request("/absolute-redirect/5")
+ .through(AutoRedirectingWire.class, Tv.SIX)
+ .fetch()
+ .as(RestResponse.class)
+ .assertStatus(HttpURLConnection.HTTP_OK);
+ }
+
+ @Test
+ final void failsOnTimeout() {
+ Assertions.assertThrows(
+ IOException.class,
+ () ->
+ this.request("/delay/3")
+ .timeout(Tv.THOUSAND, Tv.THOUSAND)
+ .fetch()
+ );
+ }
+
+ @Test
+ final void readsJsonResponse() throws Exception {
+ MatcherAssert.assertThat(
+ "Must parse Json response",
+ this.request("/json")
+ .fetch()
+ .as(JsonResponse.class)
+ .json()
+ .readObject(),
+ Matchers.notNullValue(JsonObject.class)
+ );
+ }
+
+ @Test
+ @DisabledIf("isJdkRequest")
+ final void readsDeflatedJsonResponse() throws Exception {
+ MatcherAssert.assertThat(
+ "Must undeflate & parse Json response",
+ this.request("/deflate")
+ .fetch()
+ .as(JsonResponse.class)
+ .json()
+ .readObject(),
+ Matchers.hasEntry(
+ Matchers.is("deflated"),
+ Matchers.is(JsonValue.TRUE)
+ )
+ );
+ }
+
+ @Test
+ @DisabledIf("isJdkRequest")
+ final void readsGzippedJsonResponse() throws Exception {
+ MatcherAssert.assertThat(
+ "Must unzip & parse Json response",
+ this.request("/gzip")
+ .fetch()
+ .as(JsonResponse.class)
+ .json()
+ .readObject(),
+ Matchers.hasEntry(
+ Matchers.is("gzipped"),
+ Matchers.is(JsonValue.TRUE)
+ )
+ );
+ }
+
+ @Test
+ final void handlesBasicAuth() throws Exception {
+ MatcherAssert.assertThat(
+ "Must authenticate with userInfo",
+ this.request("/basic-auth/jeff/secret")
+ .uri()
+ .userInfo("jeff:secret")
+ .back()
+ .through(BasicAuthWire.class)
+ .fetch()
+ .as(JsonResponse.class)
+ .status(),
+ Matchers.is(
+ HttpURLConnection.HTTP_OK
+ )
+ );
+ }
+
+ @Test
+ final void readsXmlResponse() throws Exception {
+ MatcherAssert.assertThat(
+ "Must parse XML response",
+ this.request("/xml")
+ .fetch()
+ .as(XmlResponse.class)
+ .xml(),
+ Matchers.notNullValue(XML.class)
+ );
+ }
+
+ /**
+ * Is JdkRequest being tested?
+ * @return True if so.
+ */
+ @SuppressWarnings("PMD.UnusedPrivateMethod")
+ private boolean isJdkRequest() {
+ return JdkRequest.class.equals(this.type);
+ }
+}
diff --git a/src/test/java/com/jcabi/http/RequestSecondITCase.java b/src/test/java/com/jcabi/http/RequestSecondITCase.java
new file mode 100644
index 00000000..d8e65ca7
--- /dev/null
+++ b/src/test/java/com/jcabi/http/RequestSecondITCase.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2011-2017, jcabi.com
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met: 1) Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer. 2) Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution. 3) Neither the name of the jcabi.com nor
+ * the names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
+ * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jcabi.http;
+
+import com.jcabi.http.request.ApacheRequest;
+import com.jcabi.http.request.JdkRequest;
+import java.net.URI;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.TestInstance;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Integration test for {@link Request}.
+ *
+ * @since 1.17.8
+ */
+@SuppressWarnings("PMD.AbstractClassWithoutAbstractMethod")
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@Testcontainers(disabledWithoutDocker = true)
+public final class RequestSecondITCase {
+
+ /**
+ * Container with HttpBin.
+ */
+ private final GenericContainer> container = new GenericContainer<>(
+ DockerImageName.parse("kennethreitz/httpbin")
+ ).withExposedPorts(80);
+
+ @BeforeAll
+ void beforeAll() {
+ this.container.start();
+ }
+
+ @AfterAll
+ void tearDown() {
+ this.container.stop();
+ }
+
+ /**
+ * URI of the container.
+ * @return URI.
+ */
+ private URI uri() {
+ return URI.create(
+ String.format(
+ "http://%s:%d",
+ this.container.getHost(),
+ this.container.getFirstMappedPort()
+ )
+ );
+ }
+
+ /**
+ * Test for {@link JdkRequest}.
+ * @since 1.17.8
+ */
+ @Nested
+ final class JdkRequestITCase extends RequestITCaseTemplate {
+ JdkRequestITCase() {
+ super(JdkRequest.class, RequestSecondITCase.this.uri());
+ }
+ }
+
+ /**
+ * Test for {@link ApacheRequest}.
+ * @since 1.17.8
+ */
+ @Nested
+ final class ApacheRequestITCase extends RequestITCaseTemplate {
+ ApacheRequestITCase() {
+ super(ApacheRequest.class, RequestSecondITCase.this.uri());
+ }
+ }
+}
diff --git a/src/test/java/com/jcabi/http/RequestTestTemplate.java b/src/test/java/com/jcabi/http/RequestTestTemplate.java
index 68986fef..ba552896 100644
--- a/src/test/java/com/jcabi/http/RequestTestTemplate.java
+++ b/src/test/java/com/jcabi/http/RequestTestTemplate.java
@@ -34,6 +34,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URI;
+import lombok.SneakyThrows;
import org.junit.jupiter.params.provider.ValueSource;
/**
@@ -58,11 +59,9 @@ abstract class RequestTestTemplate {
* @param uri URI to start with
* @param type Type of the request
* @return Request
- * @throws Exception If fails
*/
- static Request request(final URI uri, final Class extends Request> type)
- throws Exception {
+ @SneakyThrows
+ static Request request(final URI uri, final Class extends Request> type) {
return type.getDeclaredConstructor(URI.class).newInstance(uri);
}
-
}