MockServer.java

/**
 * The MIT License
 *
 * Copyright for portions of unirest-java are held by Kong Inc (c) 2013.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */


package BehaviorTests;

import com.google.common.base.Strings;
import io.javalin.Javalin;
import io.javalin.http.Context;
import io.javalin.http.Cookie;
import io.javalin.http.HttpStatus;
import io.javalin.websocket.WsConfig;
import jakarta.servlet.ServletOutputStream;
import org.eclipse.jetty.util.UrlEncoded;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.zip.GZIPOutputStream;

import static org.junit.jupiter.api.Assertions.assertEquals;


public class MockServer {


    public static int timesCalled;
    private static int pages = 1;
    private static int onPage = 1;
    private static int retryTimes = 0;
    private static int retryStatus = 429;
    private static String retrySeconds = "";
    private static final List<Pair<String, String>> responseHeaders = new ArrayList<>();
    private static final List<Cookie> cookies = new ArrayList<>();
    private static final WebSocketHandler ws = new WebSocketHandler();
    private static final JacksonObjectMapper om = new JacksonObjectMapper();
    private static String responseBody;
    public static final int PORT = 4567;
    public static final String HOST = "http://localhost:" + PORT;
    public static final String WEBSOCKET = "ws://localhost:" + PORT + "/websocket";
    public static final String WINDOWS_LATIN_1_FILE = HOST + "/public/data/cp1250.txt";
    public static final String REDIRECT = HOST + "/redirect";
    public static final String JAVALIN = HOST + "/sparkle/{spark}/yippy";
    public static final String BINARYFILE = HOST + "/binary";
    public static final String NOBODY = HOST + "/nobody";
    public static final String PAGED = HOST + "/paged";
    public static final String POST = HOST + "/post";
    public static final String GET = HOST + "/get";
    public static final String ERROR_RESPONSE = HOST + "/error";
    public static final String DELETE = HOST + "/delete";
    public static final String GZIP = HOST + "/gzip";
    public static final String EMPTY_GZIP = HOST + "/empty-gzip";
    public static final String PATCH = HOST + "/patch";
    public static final String INVALID_REQUEST = HOST + "/invalid";
    public static final String PASSED_PATH_PARAM = GET + "/{params}/passed";
    public static final String PASSED_PATH_PARAM_MULTI = PASSED_PATH_PARAM + "/{another}";
    public static final String CHEESE = HOST + "/cheese";
    public static final String ALTGET = "http://127.0.0.1:" + PORT + "/get";
    public static final String ECHO_RAW = HOST + "/raw";
    public static final String TIMEOUT = HOST + "/timeout";
    private static Javalin app;
    private static int errorCode = 400;


    public static void setJsonAsResponse(Object o) {
        responseBody = om.writeValue(o);
    }

    public static void reset() {
        responseBody = null;
        responseHeaders.clear();
        cookies.clear();
        pages = 1;
        onPage = 1;
        timesCalled = 0;
        WebSocketHandler.reset();
        retryTimes = 0;
        errorCode = 400;
    }

    static {
        app = Javalin.create(c -> {
            c.staticFiles.add("public/");

        }).start(PORT);
        app.error(404, MockServer::notFound);
        app.before(c -> timesCalled++);
        app.ws("/websocket", ws);
        app.delete("/delete", MockServer::jsonResponse);
        app.get("/sparkle/{spark}/yippy", MockServer::sparkle);
        app.post("/post", MockServer::jsonResponse);
        app.get("/get", MockServer::jsonResponse);
        app.get("/gzip", MockServer::gzipResponse);
        app.post("/empty-gzip", MockServer::emptyGzipResponse);
        app.get("/redirect", MockServer::redirect);
        app.post("/redirect", MockServer::redirect);
        app.patch("/patch", MockServer::jsonResponse);
        app.get("/invalid", MockServer::inValid);
        app.options("/get", MockServer::jsonResponse);
        app.get("/nobody", MockServer::nobody);
        app.head("/get", MockServer::jsonResponse);
        app.put("/post", MockServer::jsonResponse);
        app.get("/get/{params}/passed", MockServer::jsonResponse);
        app.get("/get/{params}/passed/{another}", MockServer::jsonResponse);
        app.get("/proxy", MockServer::proxiedResponse);
        app.get("/binary", MockServer::file);
        app.get("/paged", MockServer::paged);
        app.post("/paged", MockServer::paged);
        app.post("/raw", MockServer::echo);
        app.get("/error", MockServer::error);
        app.get("/hello", MockServer::helloWOrld);
        app.get("/timeout", MockServer::timeout);
        Runtime.getRuntime().addShutdownHook(new Thread(app::stop));
        try {
            new CountDownLatch(1).await(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void timeout(Context context) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {

        }
        context.result("Hello World");
    }


    private static void helloWOrld(Context c) {
         c.result("Hello World");
    }


    public static void main(String[] args){

    }

    private static void sparkle(Context request) {
        Map<String, String> sparks = new HashMap<>();
        sparks.put("contentType()", request.contentType());
        sparks.put("contextPath()", request.contextPath());
        sparks.put("host()", request.host());
        sparks.put("ip()", request.ip());
        sparks.put("pathInfo()", request.req().getPathInfo());
        sparks.put("port()", String.valueOf(request.port()));
        sparks.put("protocol()", request.protocol());
        sparks.put("scheme()", request.scheme());
        sparks.put("servletPath()", request.req().getServletPath());
        //sparks.put("requestMethod()", request.me());
        sparks.put("uri()", request.req().getRequestURI());
        sparks.put("url()", request.url());
        sparks.put("userAgent()", request.userAgent());
        sparks.put("queryString()", request.queryString());
        request.result(om.writeValue(sparks));
    }

    private static void error(Context request) {
        request.status(errorCode);
        request.result(Strings.nullToEmpty(responseBody));
    }

    private static void echo(Context request) {
        request.result(request.body());
    }

    private static void paged(Context context) {
        if (pages > onPage) {
            onPage++;
            context.header("nextPage", PAGED + "?page=" + onPage);
        }
        jsonResponse(context);
    }

    private static void notFound(Context context) {
        RequestCapture value = getRequestCapture(context);
        value.setStatus(404);
        context.status(404);
        context.result(om.writeValue(value));
    }

    private static Object file(Context context) throws Exception {
        File f = TestUtil.rezFile("/spidey.jpg");
        context.contentType("application/octet-stream");
        context.header("Content-Disposition", "attachment;filename=image.jpg");
        context.header("Content-Length", String.valueOf(f.length()));
        context.status(200);
        final ServletOutputStream out = context.res().getOutputStream();
        final FileInputStream in = new FileInputStream(f);
        in.transferTo(out);
        out.close();
        in.close();
        return null;
    }

    private static void nobody(Context request) {
        request.status(200);
    }

    private static void redirect(Context request) {
        request.redirect(GET, HttpStatus.MOVED_PERMANENTLY);
    }

    private static void inValid(Context request) {
        request.status(400);
        request.result("You did something bad");
    }

    private static Object emptyGzipResponse(Context response) throws Exception {
        response.res().setHeader("Content-Encoding", "gzip");
        response.res().setContentType("application/json");
        response.res().setStatus(200);
        response.res().getOutputStream().close();
        return null;
    }

    private static void gzipResponse(Context context) {
        context.header("Content-Encoding", "gzip");
        jsonResponse(context, true);
    }

    private static void proxiedResponse(Context context) {
         simpleResponse(context)
                .orElseGet(() -> {
                    RequestCapture value = getRequestCapture(context);
                    value.setIsProxied(true);
                    return om.writeValue(value);
                });
    }

    private static Optional<String> simpleResponse(Context context) {
        cookies.forEach(c -> context.cookie(c));
        responseHeaders.forEach(h -> context.res().addHeader(h.key, h.value));

        if (responseBody != null) {
            return Optional.of(responseBody);
        }
        return Optional.empty();
    }

    private static void jsonResponse(Context c){
        jsonResponse(c, false);
    }
    private static void jsonResponse(Context c, Boolean compress) {
        if(retryTimes > 0){
            if(retrySeconds != null) {
                c.header("Retry-After", retrySeconds);
            }
            retryTimes--;
            c.status(retryStatus);
            return;
        }

         String content = simpleResponse(c)
                .orElseGet(() -> {
                    RequestCapture value = getRequestCapture(c);
                    return om.writeValue(value);
                });
         if(compress){
             c.result(zip(content));
         } else {
             c.res().setCharacterEncoding(StandardCharsets.UTF_8.name());
             c.result(content);
         }
    }

    private static byte[] zip(String content) {
        try {
            ByteArrayOutputStream obj = new ByteArrayOutputStream();
            GZIPOutputStream gzip = new GZIPOutputStream(obj);
            gzip.write(content.getBytes("UTF-8"));
            gzip.close();

            return obj.toByteArray();
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    private static RequestCapture getRequestCapture(Context context) {
        RequestCapture value = new RequestCapture(context);
        value.writeBody(context);
        return value;
    }

    public static void shutdown() {
        app.stop();
    }

    public static void setStringResponse(String stringResponse) {
        MockServer.responseBody = stringResponse;
    }

    public static void addResponseHeader(String key, String value) {
        responseHeaders.add(new Pair<>(key, value));
    }

    public static void expectedPages(int expected) {
        pages = expected;
    }

    public static void clearCookies() {
        cookies.clear();
    }

    public static void expectCookie(String name, String value) {
        cookies.add(new Cookie(name, UrlEncoded.encodeString(value)));
    }

    public static void expectCookie(Cookie cookie) {
        cookies.add(cookie);
    }

    public static void clearHeaders() {
        responseHeaders.clear();
    }

    public static class WebSocketHandler implements Consumer<WsConfig> {

        private static String onOpenMessage = "Open";
        private WsConfig handler;
        public static Map<String, String> headers = new HashMap<>();

        public static void reset() {
            headers.clear();
            onOpenMessage = "Open";
        }

        public static void expectOpeningMessage(String message) {
            onOpenMessage = message;
        }

        @Override
        public void accept(WsConfig wsHandler) {
            this.handler = wsHandler;
            this.handler.onMessage(c -> {
                c.send("thank you");
            });
            this.handler.onConnect(c -> {
                headers = c.headerMap();
                c.send(onOpenMessage);
            });
        }
    }

    public static void retryTimes(int numberOfTimeToFail, int status, Double seconds){
        retryTimes(numberOfTimeToFail, status, String.valueOf(seconds));
    }

    public static void retryTimes(int numberOfTimeToFail, int status, String seconds) {
        retryTimes = numberOfTimeToFail;
        retryStatus = status;
        retrySeconds = seconds;
    }

    public static void assertRequestCount(int i) {
        assertEquals(i, timesCalled);
    }

    public static void expectErrorCode(int i) {
        errorCode = i;
    }
}