package org.jooby.test; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import java.io.ByteArrayInputStream; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.BiConsumer; import javax.net.ssl.SSLContext; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.ProtocolException; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.RedirectStrategy; import org.apache.http.client.fluent.Executor; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.entity.ContentType; import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.StandardHttpRequestRetryHandler; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HttpContext; import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils; import org.junit.rules.ExternalResource; import com.google.common.base.Throwables; /** * Utility test class integration test. Internal use only. * * @author edgar */ public class Client extends ExternalResource { public interface Callback { void execute(String value) throws Exception; } public interface ArrayCallback { void execute(String[] values) throws Exception; } public interface ServerCallback { void execute(Client request) throws Exception; } public static class Request { private Executor executor; private org.apache.http.client.fluent.Request req; private org.apache.http.HttpResponse rsp; private Client server; public Request(final Client server, final Executor executor, final org.apache.http.client.fluent.Request req) { this.server = server; this.executor = executor; this.req = req; } public Response execute() throws Exception { this.rsp = executor.execute(req).returnResponse(); return new Response(server, rsp); } public Response expect(final String content) throws Exception { return execute().expect(content); } public Response expect(final Callback callback) throws Exception { return execute().expect(callback); } public Response expect(final int status) throws Exception { return execute().expect(status); } public Response expect(final byte[] content) throws Exception { return execute().expect(content); } public Request header(final String name, final Object value) { req.addHeader(name, value.toString()); return this; } public Body multipart() { return new Body(MultipartEntityBuilder.create(), this); } public Body form() { return new Body(this); } public void close() { EntityUtils.consumeQuietly(rsp.getEntity()); } public Request body(final String body, final String type) { if (type == null) { byte[] bytes = body.getBytes(StandardCharsets.UTF_8); HttpEntity entity = new InputStreamEntity(new ByteArrayInputStream(bytes), bytes.length); req.body(entity); } else { req.bodyString(body, ContentType.parse(type)); } return this; } } public static class Body { private Request req; private MultipartEntityBuilder parts; private List<BasicNameValuePair> fields; public Body(final MultipartEntityBuilder parts, final Request req) { this.parts = parts; this.req = req; } public Body(final Request req) { this.fields = new ArrayList<>(); this.req = req; } public Response expect(final String content) throws Exception { if (parts != null) { req.req.body(parts.build()); } else { req.req.bodyForm(fields); } return req.expect(content); } public Response expect(final int status) throws Exception { if (parts != null) { req.req.body(parts.build()); } else { req.req.bodyForm(fields); } return req.expect(status); } public Body add(final String name, final Object value, final String type) { if (parts != null) { parts.addTextBody(name, value.toString(), ContentType.parse(type)); } else { fields.add(new BasicNameValuePair(name, value.toString())); } return this; } public Body add(final String name, final Object value) { return add(name, value, "text/plain"); } public Body file(final String name, final byte[] bytes, final String type, final String filename) { if (parts != null) { parts.addBinaryBody(name, bytes, ContentType.parse(type), filename); } else { throw new IllegalStateException("Not a multipart"); } return this; } } public static class Response { private Client server; private HttpResponse rsp; public Response(final Client server, final org.apache.http.HttpResponse rsp) { this.server = server; this.rsp = rsp; } public Response expect(final String content) throws Exception { assertEquals(content, EntityUtils.toString(this.rsp.getEntity())); return this; } public Response expect(final int status) throws Exception { assertEquals(status, rsp.getStatusLine().getStatusCode()); return this; } public Response expect(final byte[] content) throws Exception { assertArrayEquals(content, EntityUtils.toByteArray(this.rsp.getEntity())); return this; } public Response expect(final Callback callback) throws Exception { callback.execute(EntityUtils.toString(this.rsp.getEntity())); return this; } public Response header(final String headerName, final String headerValue) throws Exception { if (headerValue == null) { assertNull(rsp.getFirstHeader(headerName)); } else { Header header = rsp.getFirstHeader(headerName); if (header == null) { // friendly junit err assertEquals(headerValue, header); } else { assertEquals(headerValue.toLowerCase(), header.getValue().toLowerCase()); } } return this; } public Response headers(final BiConsumer<String, String> headers) throws Exception { for (Header header : rsp.getAllHeaders()) { headers.accept(header.getName(), header.getValue()); } return this; } public Response header(final String headerName, final Object headerValue) throws Exception { if (headerValue == null) { return header(headerName, (String) null); } else { return header(headerName, headerValue.toString()); } } public Response header(final String headerName, final Optional<Object> headerValue) throws Exception { Header header = rsp.getFirstHeader(headerName); if (header != null) { assertEquals(headerValue.get(), header.getValue()); } return this; } public Response header(final String headerName, final Callback callback) throws Exception { callback.execute( Optional.ofNullable(rsp.getFirstHeader(headerName)) .map(Header::getValue) .orElse(null)); return this; } public Response headers(final String headerName, final ArrayCallback callback) throws Exception { Header[] headers = rsp.getHeaders(headerName); String[] values = new String[headers.length]; for (int i = 0; i < values.length; i++) { values[i] = headers[i].getValue(); } callback.execute(values); return this; } public Response empty() throws Exception { HttpEntity entity = this.rsp.getEntity(); if (entity != null) { assertEquals("", EntityUtils.toString(entity)); } return this; } public void request(final ServerCallback request) throws Exception { request.execute(server); } public void startsWith(final String value) throws IOException { String rsp = EntityUtils.toString(this.rsp.getEntity()); if (!rsp.startsWith(value)) { assertEquals(value, rsp); } } } private Executor executor; private CloseableHttpClient client; private BasicCookieStore cookieStore; private String host; private Request req; private HttpClientBuilder builder; private UsernamePasswordCredentials creds; public Client(final String host) { this.host = host; } public Client() { this("http://localhost:8080"); } public void start() { this.cookieStore = new BasicCookieStore(); this.builder = HttpClientBuilder.create() .setMaxConnTotal(1) .setRetryHandler(new StandardHttpRequestRetryHandler(0, false)) .setMaxConnPerRoute(1) .setDefaultCookieStore(cookieStore); } public Client resetCookies() { cookieStore.clear(); return this; } public Client dontFollowRedirect() { builder.setRedirectStrategy(new RedirectStrategy() { @Override public boolean isRedirected(final HttpRequest request, final HttpResponse response, final HttpContext context) throws ProtocolException { return false; } @Override public HttpUriRequest getRedirect(final HttpRequest request, final HttpResponse response, final HttpContext context) throws ProtocolException { return null; } }); return this; } public Request get(final String path) { this.req = new Request(this, executor(), org.apache.http.client.fluent.Request.Get(host + path)); return req; } public Request trace(final String path) { this.req = new Request(this, executor(), org.apache.http.client.fluent.Request.Trace(host + path)); return req; } public Request options(final String path) { this.req = new Request(this, executor(), org.apache.http.client.fluent.Request.Options(host + path)); return req; } public Request head(final String path) { this.req = new Request(this, executor(), org.apache.http.client.fluent.Request.Head(host + path)); return req; } private Executor executor() { if (executor == null) { if (this.host.startsWith("https://")) { try { SSLContext sslContext = SSLContexts.custom() .loadTrustMaterial(null, (chain, authType) -> true) .build(); builder.setSSLContext(sslContext); builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE); } catch (Exception ex) { Throwables.propagate(ex); } } client = builder.build(); executor = Executor.newInstance(client); if (creds != null) { executor.auth(creds); } } return executor; } public Request post(final String path) { this.req = new Request(this, executor(), org.apache.http.client.fluent.Request.Post(host + path)); return req; } public Request put(final String path) { this.req = new Request(this, executor(), org.apache.http.client.fluent.Request.Put(host + path)); return req; } public Request delete(final String path) { this.req = new Request(this, executor(), org.apache.http.client.fluent.Request.Delete(host + path)); return req; } public Request patch(final String path) { this.req = new Request(this, executor(), pathHack(host + path)); return req; } @SuppressWarnings({"unchecked", "rawtypes" }) private org.apache.http.client.fluent.Request pathHack(final String string) { try { // Patch is available since 4.4, but we are in 4.3 because of AWS-SDK Class ireqclass = getClass().getClassLoader().loadClass( "org.apache.http.client.fluent.InternalHttpRequest"); Constructor<org.apache.http.client.fluent.Request> constructor = org.apache.http.client.fluent.Request.class .getDeclaredConstructor(ireqclass); constructor.setAccessible(true); Constructor ireqcons = ireqclass.getDeclaredConstructor(String.class, URI.class); ireqcons.setAccessible(true); Object ireq = ireqcons.newInstance("PATCH", URI.create(string)); return constructor.newInstance(ireq); } catch (NoSuchMethodException | SecurityException | ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { throw new UnsupportedOperationException(ex); } } public void stop() throws IOException { if (this.req != null) { try { this.req.close(); } catch (NullPointerException ex) { } } if (client != null) { client.close(); } this.builder = null; this.executor = null; } public Client basic(final String username, final String password) { creds = new UsernamePasswordCredentials(username, password); return this; } @Override protected void before() throws Throwable { start(); } @Override protected void after() { try { stop(); } catch (IOException ex) { throw new IllegalStateException("Unable to stop client", ex); } } }