ApacheHttpTransportTest.java
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.api.client.http.apache.v2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.LowLevelHttpResponse;
import com.google.api.client.testing.http.apache.MockHttpClient;
import com.google.api.client.util.ByteArrayStreamingContent;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.http.Header;
import org.apache.http.HttpClientConnection;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestExecutor;
import org.junit.Assert;
import org.junit.Test;
/**
* Tests {@link ApacheHttpTransport}.
*
* @author Yaniv Inbar
*/
public class ApacheHttpTransportTest {
private static class MockHttpResponse extends BasicHttpResponse implements CloseableHttpResponse {
public MockHttpResponse() {
super(HttpVersion.HTTP_1_1, 200, "OK");
}
@Override
public void close() throws IOException {}
}
@Test
public void testApacheHttpTransport() {
ApacheHttpTransport transport = new ApacheHttpTransport();
checkHttpTransport(transport);
assertFalse(transport.isMtls());
}
@Test
public void testApacheHttpTransportWithParam() {
ApacheHttpTransport transport = new ApacheHttpTransport(HttpClients.custom().build(), true);
checkHttpTransport(transport);
assertTrue(transport.isMtls());
}
@Test
public void testNewDefaultHttpClient() {
HttpClient client = ApacheHttpTransport.newDefaultHttpClient();
checkHttpClient(client);
}
private void checkHttpTransport(ApacheHttpTransport transport) {
assertNotNull(transport);
HttpClient client = transport.getHttpClient();
checkHttpClient(client);
}
private void checkHttpClient(HttpClient client) {
assertNotNull(client);
// TODO(chingor): Is it possible to test this effectively? The newer HttpClient implementations
// are read-only and we're testing that we built the client with the right configuration
}
@Test
public void testRequestsWithContent() throws IOException {
HttpClient mockClient =
new MockHttpClient() {
@Override
public CloseableHttpResponse execute(HttpUriRequest request)
throws IOException, ClientProtocolException {
return new MockHttpResponse();
}
};
ApacheHttpTransport transport = new ApacheHttpTransport(mockClient);
// Test GET.
subtestUnsupportedRequestsWithContent(
transport.buildRequest("GET", "http://www.test.url"), "GET");
// Test DELETE.
subtestUnsupportedRequestsWithContent(
transport.buildRequest("DELETE", "http://www.test.url"), "DELETE");
// Test HEAD.
subtestUnsupportedRequestsWithContent(
transport.buildRequest("HEAD", "http://www.test.url"), "HEAD");
// Test PATCH.
execute(transport.buildRequest("PATCH", "http://www.test.url"));
// Test PUT.
execute(transport.buildRequest("PUT", "http://www.test.url"));
// Test POST.
execute(transport.buildRequest("POST", "http://www.test.url"));
// Test PATCH.
execute(transport.buildRequest("PATCH", "http://www.test.url"));
}
private void subtestUnsupportedRequestsWithContent(ApacheHttpRequest request, String method)
throws IOException {
try {
execute(request);
fail("expected " + IllegalStateException.class);
} catch (IllegalStateException e) {
// expected
assertEquals(
e.getMessage(),
"Apache HTTP client does not support " + method + " requests with content.");
}
}
private void execute(ApacheHttpRequest request) throws IOException {
byte[] bytes = "abc".getBytes(StandardCharsets.UTF_8);
request.setStreamingContent(new ByteArrayStreamingContent(bytes));
request.setContentType("text/html");
request.setContentLength(bytes.length);
request.execute();
}
@Test
public void testRequestShouldNotFollowRedirects() throws IOException {
final AtomicInteger requestsAttempted = new AtomicInteger(0);
HttpRequestExecutor requestExecutor =
new HttpRequestExecutor() {
@Override
public HttpResponse execute(
HttpRequest request, HttpClientConnection connection, HttpContext context)
throws IOException, HttpException {
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, 302, null);
response.addHeader("location", "https://google.com/path");
requestsAttempted.incrementAndGet();
return response;
}
};
HttpClient client = HttpClients.custom().setRequestExecutor(requestExecutor).build();
ApacheHttpTransport transport = new ApacheHttpTransport(client);
ApacheHttpRequest request = transport.buildRequest("GET", "https://google.com");
LowLevelHttpResponse response = request.execute();
assertEquals(1, requestsAttempted.get());
assertEquals(302, response.getStatusCode());
}
@Test
public void testRequestCanSetHeaders() {
final AtomicBoolean interceptorCalled = new AtomicBoolean(false);
HttpClient client =
HttpClients.custom()
.addInterceptorFirst(
new HttpRequestInterceptor() {
@Override
public void process(HttpRequest request, HttpContext context)
throws HttpException, IOException {
Header header = request.getFirstHeader("foo");
assertNotNull("Should have found header", header);
assertEquals("bar", header.getValue());
interceptorCalled.set(true);
throw new IOException("cancelling request");
}
})
.build();
ApacheHttpTransport transport = new ApacheHttpTransport(client);
ApacheHttpRequest request = transport.buildRequest("GET", "https://google.com");
request.addHeader("foo", "bar");
try {
LowLevelHttpResponse response = request.execute();
fail("should not actually make the request");
} catch (IOException exception) {
assertEquals("cancelling request", exception.getMessage());
}
assertTrue("Expected to have called our test interceptor", interceptorCalled.get());
}
@Test(timeout = 10_000L)
public void testConnectTimeout() {
// Apache HttpClient doesn't appear to behave correctly on windows
assumeFalse(isWindows());
// TODO(chanseok): Java 17 returns an IOException (SocketException: Network is unreachable).
// Figure out a way to verify connection timeout works on Java 17+.
assumeTrue(System.getProperty("java.version").compareTo("17") < 0);
HttpTransport httpTransport = new ApacheHttpTransport();
GenericUrl url = new GenericUrl("http://google.com:81");
try {
httpTransport.createRequestFactory().buildGetRequest(url).setConnectTimeout(100).execute();
fail("should have thrown an exception");
} catch (HttpHostConnectException | ConnectTimeoutException expected) {
// expected
} catch (IOException e) {
fail("unexpected IOException: " + e.getClass().getName() + ": " + e.getMessage());
}
}
private static class FakeServer implements AutoCloseable {
private final HttpServer server;
private final ExecutorService executorService;
FakeServer(HttpHandler httpHandler) throws IOException {
server = HttpServer.create(new InetSocketAddress(0), 0);
executorService = Executors.newFixedThreadPool(1);
server.setExecutor(executorService);
server.createContext("/", httpHandler);
server.start();
}
public int getPort() {
return server.getAddress().getPort();
}
@Override
public void close() {
server.stop(0);
executorService.shutdownNow();
}
}
@Test
public void testNormalizedUrl() throws IOException {
final HttpHandler handler =
new HttpHandler() {
@Override
public void handle(HttpExchange httpExchange) throws IOException {
byte[] response = httpExchange.getRequestURI().toString().getBytes();
httpExchange.sendResponseHeaders(200, response.length);
try (OutputStream out = httpExchange.getResponseBody()) {
out.write(response);
}
}
};
try (FakeServer server = new FakeServer(handler)) {
HttpTransport transport = new ApacheHttpTransport();
GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar");
testUrl.setPort(server.getPort());
com.google.api.client.http.HttpResponse response =
transport.createRequestFactory().buildGetRequest(testUrl).execute();
assertEquals(200, response.getStatusCode());
assertEquals("/foo//bar", response.parseAsString());
}
}
@Test
public void testReadErrorStream() throws IOException {
final HttpHandler handler =
new HttpHandler() {
@Override
public void handle(HttpExchange httpExchange) throws IOException {
byte[] response = "Forbidden".getBytes(StandardCharsets.UTF_8);
httpExchange.sendResponseHeaders(403, response.length);
try (OutputStream out = httpExchange.getResponseBody()) {
out.write(response);
}
}
};
try (FakeServer server = new FakeServer(handler)) {
HttpTransport transport = new ApacheHttpTransport();
GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar");
testUrl.setPort(server.getPort());
com.google.api.client.http.HttpRequest getRequest =
transport.createRequestFactory().buildGetRequest(testUrl);
getRequest.setThrowExceptionOnExecuteError(false);
com.google.api.client.http.HttpResponse response = getRequest.execute();
assertEquals(403, response.getStatusCode());
assertEquals("Forbidden", response.parseAsString());
}
}
@Test
public void testReadErrorStream_withException() throws IOException {
final HttpHandler handler =
new HttpHandler() {
@Override
public void handle(HttpExchange httpExchange) throws IOException {
byte[] response = "Forbidden".getBytes(StandardCharsets.UTF_8);
httpExchange.sendResponseHeaders(403, response.length);
try (OutputStream out = httpExchange.getResponseBody()) {
out.write(response);
}
}
};
try (FakeServer server = new FakeServer(handler)) {
HttpTransport transport = new ApacheHttpTransport();
GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar");
testUrl.setPort(server.getPort());
com.google.api.client.http.HttpRequest getRequest =
transport.createRequestFactory().buildGetRequest(testUrl);
try {
getRequest.execute();
Assert.fail();
} catch (HttpResponseException ex) {
assertEquals("Forbidden", ex.getContent());
}
}
}
private boolean isWindows() {
return System.getProperty("os.name").startsWith("Windows");
}
}