Invocation.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 kong.unirest.core;

import kong.unirest.core.json.JSONElement;

import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.lang.System.lineSeparator;

class Invocation implements Expectation {
    private Routes routes;
    private List<HttpRequest> requests = new ArrayList<>();
    private Headers expectedHeaders = new Headers();
    private Headers expectedQueryParams = new Headers();
    private Boolean expected = false;
    private BodyMatcher expectedBody;
    private MatchStatus expectedBodyStatus;
    private ExpectedResponseRecord expectedResponse = new ExpectedResponseRecord(this);
    private Function<HttpRequest<?>, ExpectedResponse> functionalResponse = r -> expectedResponse;
    private Times expectedTimes = Times.atLeastOnce();

    public Invocation(Routes routes){
        this.routes = routes;
        this.expected = true;
    }

    public Invocation(Routes routes, HttpRequest request) {
        this.routes = routes;
        this.expectedHeaders = request.getHeaders();
    }

    Invocation(Routes routes, Invocation other) {
        this.routes = routes;
        this.expectedResponse = other.expectedResponse;
    }

    Invocation() {

    }

    @Override
    public ExpectedResponse thenReturn(String body) {
        return expectedResponse.thenReturn(body);
    }

    @Override
    public ExpectedResponse thenReturn(JSONElement jsonObject) {
        return expectedResponse.thenReturn(jsonObject);
    }

    @Override
    public ExpectedResponse thenReturn(Object pojo) {
        return expectedResponse.thenReturn(pojo);
    }

    @Override
    public ExpectedResponse thenReturn(Supplier<String> supplier) {
        return expectedResponse.thenReturn(supplier);
    }

    @Override
    public void thenReturn(Function<HttpRequest<?>, ExpectedResponse> fun) {
        this.functionalResponse = fun;
    }

    RawResponse getResponse(Config config, HttpRequest request) {
        return tryCast(functionalResponse.apply(request), ExpectedResponseRecord.class)
                .map(e -> {
                    e.setExpectation(this);
                    return e.toRawResponse(config, request);
                })
                .orElseThrow(() -> new UnirestException("No Result Configured For Response"));
    }

    private Headers allHeaders() {
        return requests.stream()
                .map(i -> i.getHeaders())
                .reduce((h1, h2) -> {
                    h1.putAll(h2);
                    return h1;
                })
                .orElseGet(Headers::new);
    }

    public void log(HttpRequest request) {
        this.requests.add(request);
    }

    @Override
    public Expectation header(String key, String value) {
        expectedHeaders.add(key, value);
        return this;
    }

    @Override
    public Expectation queryString(String key, String value) {
        expectedQueryParams.add(key, value);
        return this;
    }

    @Override
    public Expectation body(String body) {
        expectedBody = new EqualsBodyMatcher(body);
        return this;
    }

    @Override
    public Expectation body(BodyMatcher matcher) {
        expectedBody = matcher;
        return this;
    }

    @Override
    public ExpectedResponse thenReturn() {
        return expectedResponse;
    }

    @Override
    public void verify() {
        verify(null);
    }

    @Override
    public void verify(Times times) {
        var thisTime = times == null ? expectedTimes : times;
        var match = thisTime.matches(requests.size());
        if(!match.isSuccess()){
            throw new UnirestAssertion("%s\n%s", match.getMessage(), details());
        }
    }

    @Override
    public Expectation times(Times times) {
        this.expectedTimes = times;
        return this;
    }

    private String details() {
        var sb  = new StringBuilder(routes.getMethod().name())
                .append(" ")
                .append(routes.getPath()).append(lineSeparator());
        if(expectedHeaders.size() > 0){
            sb.append("Headers:\n");
            sb.append(expectedHeaders);
        }
        if(expectedQueryParams.size() > 0){
            sb.append("Params:\n");
            sb.append(expectedQueryParams);
        }
        if(expectedBody != null){
            sb.append("Body:\n");
            if(expectedBodyStatus != null) {
                sb.append("\t" + expectedBodyStatus.getDescription());
            } else {
                sb.append("\t null");
            }
        }
        return sb.toString();
    }


    public boolean hasExpectedHeader(String key, String value) {
        Headers allH = allHeaders();
        return allH.containsKey(key) && allH.get(key).contains(value);
    }

    private Stream<Body> getBodyStream() {
        return requests.stream()
                .filter(r -> r.getBody().isPresent())
                .map(r -> (Body) r.getBody().get());
    }

    public boolean hasBody(String body){
        return getBodyStream()
                .anyMatch(b -> uniBodyMatches(body, b));
    }

    private boolean uniBodyMatches(String body, Body o) {
        return tryCast(o, HttpRequestUniBody.class)
                .map(h -> h.uniPart())
                .map(u -> u.getValue().equals(body))
                .orElse(false);
    }

    public boolean hasField(String name, String value) {
        return getBodyStream()
                .anyMatch(b -> hasField(name, value, b));
    }

    private boolean hasField(String name, String value, Body b) {
        return tryCast(b, HttpRequestMultiPart.class)
                .map(h -> h.multiParts())
                .orElse(Collections.emptyList())
                .stream()
                .anyMatch(part -> part.getName().equals(name) && part.getValue().equals(value));
    }

    public Integer requestSize() {
        return requests.size();
    }

    public List<HttpRequest> getRequests() {
        return requests;
    }

    public Boolean isExpected() {
        return expected;
    }

    public Integer scoreMatch(HttpRequest request) {
        int score = 0;
        score += scoreHeaders(request);
        score += scoreQuery(request);
        score += scoreBody(request);
        return score;
    }

    private int scoreBody(HttpRequest request) {
        if(expectedBody != null){
            return tryCast(request, Body.class)
                    .map(this::matchBody)
                    .orElse(-1000);
        }
        return 0;
    }

    private Integer matchBody(Body b) {
        if(b.isEntityBody()){
            BodyPart bodyPart = b.uniPart();
            if(bodyPart == null && expectedBody == null){
                return 1;
            } else if(String.class.isAssignableFrom(bodyPart.getPartType())){
                expectedBodyStatus = expectedBody.matches(Arrays.asList((String) bodyPart.getValue()));
                if(expectedBodyStatus.isSuccess()){
                    return 1;
                } else {
                    return -1000;
                }
            } else if (expectedBody != null) {
                return -1000;
            }
        } else {
            List<String> parts = b.multiParts().stream().map(p -> p.toString()).collect(Collectors.toList());
            if(parts.isEmpty() && expectedBody == null){
                return 1;
            } else {
                expectedBodyStatus = expectedBody.matches(parts);
                if(expectedBodyStatus.isSuccess()){
                    return 1;
                } else {
                    return -1000;
                }
            }
        }
        return 0;
    }

    private int scoreHeaders(HttpRequest request) {
        if(expectedHeaders.size() > 0){
            long b = expectedHeaders.all().stream().filter(h ->
                    request.getHeaders().get(h.getName()).contains(h.getValue()))
                    .count();

            if(b != expectedHeaders.size()){
                return -1000;
            }
            return Long.valueOf(b).intValue();
        }
        return 0;
    }

    private int scoreQuery(HttpRequest request) {
        if(expectedQueryParams.size() > 0){
            QueryParams p = QueryParams.fromURI(request.getUrl());
            long b = expectedQueryParams.all().stream().filter(h ->
                    p.getQueryParams().stream().anyMatch(q -> q.getName().equalsIgnoreCase(h.getName())
                            && q.getValue().equalsIgnoreCase(h.getValue())))
                    .count();
            if(b != expectedQueryParams.size()){
                return -1000;
            }
            return Long.valueOf(b).intValue();
        }
        return 0;
    }

    private static <T, M extends T> Optional<M> tryCast(T original, Class<M> too) {
        if (original != null && too.isAssignableFrom(original.getClass())) {
            return Optional.of((M) original);
        }
        return Optional.empty();
    }

}