All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ste.xtest.net.StubHttpClient Maven / Gradle / Ivy

There is a newer version: 3.7.1
Show newest version
/*
 * xTest
 * Copyright (C) 2023 Stefano Fornari
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY Stefano Fornari, Stefano Fornari
 * DISCLAIMS THE WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * 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 General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA.
 */

package ste.xtest.net;

import java.io.File;
import java.io.IOException;
import java.net.Authenticator;
import java.net.CookieHandler;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.BodySubscriber;
import java.net.http.HttpResponse.ResponseInfo;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow.Subscription;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;

/**
 * This is a stubber for JDK11 HttpClient. The basic idea is to setup the stubber
 * with response stubs (as StubHttpClient.StubHttpResponse) associated each to
 * its url. When HttpClient invokes send() or sendAsync() on a stubbed url, the
 * corresponding response is used instead of using the network.
 *
 * If send()/sendAsync() is invoked on a URI not stabbed, the default HttpClient
 * created with HttpClient.newHttpClient() is used, accessing therefore the
 * network. (TODO)
 *
 * example:
 * 
 *   public class MyRealClass {
 *     Builder builder = HttpClient.newBuilder();
 *     ...
 *
 *     public void doSomething() {
 *       HttpClient client = builder.build();
 *
 *       HttpResponse response = client.send(
 *         HttpRequest.newBuilder("http://somehwere.com/resource").GET().build(),
 *         BodyHandlers.ofString()
 *       );
 *
 *       System.out.println(response.getBody());
 *     }

 *   }
 *
 *   public MyRealClassTest() {
 *
 *     @Test
 *     public void say_hello() {
 *       HttpClientStubber builder = HttpClientStubber()
 *         .withStub(
 *           "http://somehwere.com/resource", new StubHttpResponse().text("hello world")
 *         );
 *
 *       MyRealClass myClass = new MyRealClass();
 *       myClass.builder = builder;
 *
 *       myClass,doSomething(); // -> prints "hello world"
 *     }
 *
 * 
* * */ public class StubHttpClient extends HttpClient { private final HttpClientStubber builder; protected StubHttpClient(HttpClientStubber builder) { this.builder = builder; } @Override public Optional cookieHandler() { return Optional.ofNullable(builder.cookieHandler()); } @Override public Optional connectTimeout() { return Optional.ofNullable(builder.connectTimeout()); } @Override public Redirect followRedirects() { return builder.followRedirects(); } @Override public Optional proxy() { return Optional.ofNullable(builder.proxy()); } @Override public SSLContext sslContext() { return builder.sslContext(); } @Override public SSLParameters sslParameters() { return builder.sslParameters(); } @Override public Optional authenticator() { return Optional.ofNullable(builder.authenticator()); } @Override public Version version() { return builder.version(); } @Override public Optional executor() { return Optional.ofNullable(builder.executor()); } @Override public HttpResponse send(HttpRequest request, BodyHandler responseBodyHandler) throws IOException, InterruptedException { ImmutablePair stub = builder.stub(request.uri().toString()); if (stub == null) { // // No stub found for the given request, let's tell it with an IOException // throw new IOException("no stub found for " + request.uri()); } StubHttpResponse response = stub.right; BodySubscriber bodySubscriber = responseBodyHandler.apply(response); bodySubscriber.onSubscribe(new Subscription() { @Override public void request(long n) { bodySubscriber.onNext(List.of(ByteBuffer.wrap(response.content))); bodySubscriber.onComplete(); } @Override public void cancel() {} }); bodySubscriber.getBody().whenComplete((body, error) -> response.body(body)); return response; } /** * Simulate the semantic of HttpClient.sendAsync() although it does not * perform real async computation. It fundamentally returns a completed * CompletableFuture with the result of calling {@code send(request, responseBodyHandler}. * If send() throws an exception the return future will be in failed status. * * @param request the request * @param responseBodyHandler the handler for the response data * * @return a completed CompletableFuture with the result of calling * {@code send(request, responseBodyHandler}. * */ @Override public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) { try { return CompletableFuture.completedFuture(send(request, responseBodyHandler)); } catch (Exception x) { return CompletableFuture.failedFuture(x); } } @Override public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler, HttpResponse.PushPromiseHandler pushPromiseHandler) { throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody } // ------------------------------------------------------------ HttpResponse /** * Note the difference between body (of type T) and content (a byte[]). The * former represents the body created by the body handler when a request is * done (e.g. @{code httpClient.send(uri, bodyHandler);}); the latter is the * stubbed raw content a web server would return. * * @param the type of the body of a response */ static public class StubHttpResponse implements HttpResponse, ResponseInfo { private int statusCode = 200; private Map> headers = new HashMap<>(); private T body; private Class returnType; private HttpClient.Version version = HttpClient.Version.HTTP_2; private byte[] content = new byte[0]; public StubHttpResponse() { this((Class)String.class); } public StubHttpResponse(Class returnType) { this.returnType = returnType; text(""); } // --------------------------------------------- HttèpResponse, HttpInfo @Override public int statusCode() { return statusCode; } public StubHttpResponse statusCode(int code) { this.statusCode = code; return this; } @Override public HttpRequest request() { return null; } @Override public Optional previousResponse() { return Optional.ofNullable(null); } @Override public HttpHeaders headers() { return HttpHeaders.of(headers, (a, b) -> true); } @Override public T body() { return body; } // // TODO: bugfree code // /** * The body of the response * * @param body the object representing the body of the response * * @return this instance */ public StubHttpResponse body(T body) { this.body = body; return this; } /** * The stubbed raw data the web server is supposed to return * * @return the stubbed raw data the web server is supposed to return */ public byte[] content() { return content; } public StubHttpResponse content(final byte[] content) { this.content = (content == null) ? new byte[0] : content; headers.put("Content-type", headerValue("application/octet-stream")); headers.put("Content-length", headerValue(this.content.length)); return this; } public StubHttpResponse text(String body) { stringContent(body, "text/plain"); return this; } public StubHttpResponse html(String body) { stringContent(body, "text/html"); return this; } public StubHttpResponse json(String body) { stringContent((body == null) ? "{}" : body, "application/json"); return this; } public StubHttpResponse file(String content) throws IOException { if (content == null) { return StubHttpResponse.this.content(new byte[0]); } File f = new File(content); this.content = FileUtils.readFileToByteArray(new File(content)); headers.put("Content-type", headerValue(Files.probeContentType(f.toPath()))); headers.put("Content-length", headerValue(f.length())); return this; } public StubHttpResponse header(final String key, final String value) { if (value == null) { headers.remove(key); } else { headers.put(key, headerValue(value)); } return this; } public StubHttpResponse headers(Map> headers) { this.headers.clear(); if (headers != null) { this.headers.putAll(headers); } return this; } public StubHttpResponse contentType(final String type) { headers.put("Content-type", headerValue(type)); return this; } public String contentType() { List value = headers.get("Content-type"); return ((value != null) && (!value.isEmpty())) ? value.get(0) : null; } @Override public Optional sslSession() { return Optional.ofNullable(null); } @Override public URI uri() { return null; } @Override public Version version() { return version; } public StubHttpResponse version(HttpClient.Version version) { this.version = version; return this; } public List headerValue(Object... values) { List ret = new ArrayList<>(); for(Object v: values) { ret.add(String.valueOf(v)); } return ret; } // ----------------------------------------------------- private methods private void stringContent(final String content, final String type) { this.content = (content == null) ? new byte[0] : content.getBytes(); headers.put("Content-type", headerValue(type)); headers.put("Content-length", headerValue(this.content.length)); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy