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

com.intuit.karate.core.MockHandler Maven / Gradle / Ivy

The newest version!
/*
 * The MIT License
 *
 * Copyright 2022 Karate Labs Inc.
 *
 * 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 com.intuit.karate.core;

import com.intuit.karate.ScenarioActions;
import com.intuit.karate.Suite;
import com.intuit.karate.StringUtils;
import com.intuit.karate.Json;
import com.intuit.karate.KarateException;
import com.intuit.karate.graal.JsValue;
import com.intuit.karate.http.HttpClientFactory;
import com.intuit.karate.http.HttpUtils;
import com.intuit.karate.http.Request;
import com.intuit.karate.http.ResourceType;
import com.intuit.karate.http.Response;
import com.intuit.karate.http.ServerHandler;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import java.util.function.BiFunction;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author pthomas3
 */
public class MockHandler implements ServerHandler {

    private static final Logger logger = LoggerFactory.getLogger(MockHandler.class);

    private static final String REQUEST_BYTES = "requestBytes";
    private static final String REQUEST_PARAMS = "requestParams";
    private static final String REQUEST_PARTS = "requestParts";

    private static final String RESPONSE_DELAY = "responseDelay";

    private static final String PATH_MATCHES = "pathMatches";
    private static final String METHOD_IS = "methodIs";
    private static final String TYPE_CONTAINS = "typeContains";
    private static final String ACCEPT_CONTAINS = "acceptContains";
    private static final String HEADER_CONTAINS = "headerContains";
    private static final String PARAM_VALUE = "paramValue";
    private static final String PARAM_EXISTS = "paramExists";
    private static final String PATH_PARAMS = "pathParams";
    private static final String BODY_PATH = "bodyPath";

    private final LinkedHashMap scenarioRuntimes = new LinkedHashMap<>(); // feature + holds global config and vars
    private final Map globals = new HashMap<>();
    private boolean corsEnabled;

    protected static final ThreadLocal LOCAL_REQUEST = new ThreadLocal<>();
    private final String prefix;

    public MockHandler(Feature feature) {
        this(feature, null);
    }

    public MockHandler(Feature feature, Map args) {
        this(null, Collections.singletonList(feature), args);
    }

    public MockHandler(List features) {
        this(null, features, null);
    }

    public MockHandler(String prefix, List features, Map args) {
        this.prefix = "/".equals(prefix) ? null : prefix;
        features.forEach(feature -> {
            ScenarioRuntime runtime = initRuntime(feature, args);
            corsEnabled = corsEnabled || runtime.engine.getConfig().isCorsEnabled();
            globals.putAll(runtime.engine.shallowCloneVariables());
            runtime.logger.info("mock server initialized: {}", feature);
            scenarioRuntimes.put(feature, runtime);            
        });
    }
    
    public Object getVariable(String name) {
        if (globals.containsKey(name)) {
            Variable v = globals.get(name);
            if (v != null) {
                return JsValue.fromJava(v.getValue());
            }
        }
        return null;
    }

    private ScenarioRuntime initRuntime(Feature feature, Map args) {
        FeatureRuntime featureRuntime = FeatureRuntime.of(Suite.forTempUse(HttpClientFactory.DEFAULT), new FeatureCall(feature), args);
        FeatureSection section = new FeatureSection();
        section.setIndex(-1); // TODO util for creating dummy scenario
        Scenario dummy = new Scenario(feature, section, -1);
        section.setScenario(dummy);
        ScenarioRuntime runtime = new ScenarioRuntime(featureRuntime, dummy);        
        runtime.engine.setVariable(PATH_MATCHES, (Function) this::pathMatches);
        runtime.engine.setVariable(PARAM_EXISTS, (Function) this::paramExists);
        runtime.engine.setVariable(PARAM_VALUE, (Function) this::paramValue);
        runtime.engine.setVariable(METHOD_IS, (Function) this::methodIs);
        runtime.engine.setVariable(TYPE_CONTAINS, (Function) this::typeContains);
        runtime.engine.setVariable(ACCEPT_CONTAINS, (Function) this::acceptContains);
        runtime.engine.setVariable(HEADER_CONTAINS, (BiFunction) this::headerContains);
        runtime.engine.setVariable(BODY_PATH, (Function) this::bodyPath);
        runtime.engine.init();
        if (feature.isBackgroundPresent()) {
            // if we are within a scenario already e.g. karate.start(), preserve context
            ScenarioEngine prevEngine = ScenarioEngine.get();
            try {
                ScenarioEngine.set(runtime.engine);
                for (Step step : feature.getBackground().getSteps()) {
                    Result result = StepRuntime.execute(step, runtime.actions);
                    if (result.isFailed()) {
                        String message = "mock-server background failed - " + feature + ":" + step.getLine();
                        runtime.logger.error(message);
                        throw new KarateException(message, result.getError());
                    }
                }
            } finally {
                ScenarioEngine.set(prevEngine);
            }
        }        
        return runtime;
    }

    private static final Result PASSED = Result.passed(0, 0);
    private static final String ALLOWED_METHODS = "GET, HEAD, POST, PUT, DELETE, PATCH";

    @Override
    public synchronized Response handle(Request req) { // note the [synchronized]
        if (corsEnabled && "OPTIONS".equals(req.getMethod())) {
            Response response = new Response(200);
            response.setHeader("Allow", ALLOWED_METHODS);
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", ALLOWED_METHODS);
            List requestHeaders = req.getHeaderValues("Access-Control-Request-Headers");
            if (requestHeaders != null) {
                response.setHeader("Access-Control-Allow-Headers", requestHeaders);
            }
            return response;
        }
        if (prefix != null && req.getPath().startsWith(prefix)) {
            req.setPath(req.getPath().substring(prefix.length()));
        }
        // rare case when http-client is active within same jvm
        // snapshot existing thread-local to restore
        ScenarioEngine prevEngine = ScenarioEngine.get();
        for (Map.Entry entry : scenarioRuntimes.entrySet()) {
            Feature feature = entry.getKey();
            ScenarioRuntime runtime = entry.getValue();
            // important for graal to work properly
            Thread.currentThread().setContextClassLoader(runtime.featureRuntime.suite.classLoader);            
            LOCAL_REQUEST.set(req);
            req.processBody();
            ScenarioEngine engine = initEngine(runtime, globals, req);
            for (FeatureSection fs : feature.getSections()) {
                if (fs.isOutline()) {
                    runtime.logger.warn("skipping scenario outline - {}:{}", feature, fs.getScenarioOutline().getLine());
                    break;
                }
                Scenario scenario = fs.getScenario();
                if (isMatchingScenario(scenario, engine)) {
                    Map configureHeaders;
                    Variable response, responseStatus, responseHeaders, responseDelay;
                    ScenarioActions actions = new ScenarioActions(engine);
                    Result result = executeScenarioSteps(feature, runtime, scenario, actions);
                    engine.mockAfterScenario();
                    configureHeaders = engine.mockConfigureHeaders();
                    response = engine.vars.remove(ScenarioEngine.RESPONSE);
                    responseStatus = engine.vars.remove(ScenarioEngine.RESPONSE_STATUS);
                    responseHeaders = engine.vars.remove(ScenarioEngine.RESPONSE_HEADERS);
                    responseDelay = engine.vars.remove(RESPONSE_DELAY);
                    globals.putAll(engine.shallowCloneVariables());
                    Response res = new Response(200);
                    if (result.isFailed()) {
                        response = new Variable(result.getError().getMessage());
                        responseStatus = new Variable(500);
                    } else {
                        if (corsEnabled) {
                            res.setHeader("Access-Control-Allow-Origin", "*");
                        }
                        res.setHeaders(configureHeaders);
                        if (responseHeaders != null && responseHeaders.isMap()) {
                            res.setHeaders(responseHeaders.getValue());
                        }
                        if (responseDelay != null) {
                            res.setDelay(responseDelay.getAsInt());
                        }
                    }
                    if (response != null && !response.isNull()) {
                        res.setBody(response.getAsByteArray());
                        if (res.getContentType() == null) {
                            ResourceType rt = ResourceType.fromObject(response.getValue());
                            if (rt != null) {
                                res.setContentType(rt.contentType);
                            }
                        }
                    }
                    if (responseStatus != null) {
                        res.setStatus(responseStatus.getAsInt());
                    }
                    if (prevEngine != null) {
                        ScenarioEngine.set(prevEngine);
                    }
                    return res;
                }
            }
        }
        logger.warn("no scenarios matched, returning 404: {}", req); // NOTE: not logging with engine.logger
        if (prevEngine != null) {
            ScenarioEngine.set(prevEngine);
        }
        return new Response(404);
    }
    
    private static ScenarioEngine initEngine(ScenarioRuntime runtime, Map globals, Request req) {
        ScenarioEngine engine = new ScenarioEngine(runtime.engine.getConfig(), runtime, new HashMap(globals), runtime.logger);        
        engine.init();
        engine.setVariable(ScenarioEngine.REQUEST_URL_BASE, req.getUrlBase());
        engine.setVariable(ScenarioEngine.REQUEST_PATH, req.getPath());
        engine.setVariable(ScenarioEngine.REQUEST_URI, req.getPathRaw());
        engine.setVariable(ScenarioEngine.REQUEST_METHOD, req.getMethod());
        engine.setVariable(ScenarioEngine.REQUEST_HEADERS, req.getHeaders());
        engine.setVariable(ScenarioEngine.REQUEST, req.getBodyConverted());
        engine.setVariable(REQUEST_PARAMS, req.getParams());
        engine.setVariable(REQUEST_BYTES, req.getBody());
        engine.setRequest(req);
        runtime.featureRuntime.setMockEngine(engine);
        ScenarioEngine.set(engine);
        Map>> parts = req.getMultiParts();
        if (parts != null) {
            engine.setHiddenVariable(REQUEST_PARTS, parts);
        }
        return engine;
    }

    private Result executeScenarioSteps(Feature feature, ScenarioRuntime runtime, Scenario scenario, ScenarioActions actions) {
        Result result = PASSED;
        for (Step step : scenario.getSteps()) {
            result = StepRuntime.execute(step, actions);
            if (result.isAborted()) {
                runtime.logger.debug("abort at {}:{}", feature, step.getLine());
                break;
            }
            if (result.isFailed()) {
                String message = "server-side scenario failed, " + feature + ":" + step.getLine()
                        + "\n" + step.toString() + "\n" + result.getError().getMessage();
                runtime.logger.error(message);
                break;
            }
        }
        return result;
    }

    private boolean isMatchingScenario(Scenario scenario, ScenarioEngine engine) {
        String expression = StringUtils.trimToNull(scenario.getName() + scenario.getDescription());
        if (expression == null) {
            engine.logger.debug("default scenario matched at line: {} - {}", scenario.getLine(), engine.getVariable(ScenarioEngine.REQUEST_URI));
            return true;
        }
        try {
            Variable v = engine.evalJs(expression);
            if (v.isTrue()) {
                engine.logger.debug("scenario matched at line {}: {}", scenario.getLine(), expression);
                return true;
            } else {
                engine.logger.trace("scenario skipped at line {}: {}", scenario.getLine(), expression);
                return false;
            }
        } catch (Exception e) {
            engine.logger.warn("scenario match evaluation failed at line {}: {} - {}", scenario.getLine(), expression, e + "");
            return false;
        }
    }

    public boolean pathMatches(String pattern) {
        String uri = LOCAL_REQUEST.get().getPath();
        if (uri.equals(pattern)) {
            return true;
        }
        Map pathParams = HttpUtils.parseUriPattern(pattern, uri);
        if (pathParams == null) {
            return false;
        } else {
            ScenarioEngine.get().setVariable(PATH_PARAMS, pathParams);
            return true;
        }
    }

    public boolean paramExists(String name) {
        Map> params = LOCAL_REQUEST.get().getParams();
        return params != null && params.containsKey(name);

    }

    public String paramValue(String name) {
        return LOCAL_REQUEST.get().getParam(name);
    }

    public boolean methodIs(String name) { // TODO no more supporting array arg
        return LOCAL_REQUEST.get().getMethod().equalsIgnoreCase(name);
    }

    public boolean typeContains(String text) {
        String contentType = LOCAL_REQUEST.get().getContentType();
        return contentType != null && contentType.contains(text);
    }

    public boolean acceptContains(String text) {
        String acceptHeader = LOCAL_REQUEST.get().getHeader("Accept");
        return acceptHeader != null && acceptHeader.contains(text);
    }

    public boolean headerContains(String name, String value) {
        List values = LOCAL_REQUEST.get().getHeaderValues(name);
        if (values != null) {
            for (String v : values) {
                if (v.contains(value)) {
                    return true;
                }
            }
        }
        return false;
    }

    public Object bodyPath(String path) {
        Object body = LOCAL_REQUEST.get().getBodyConverted();
        if (body == null) {
            return null;
        }
        if (path.startsWith("/")) {
            Variable v = ScenarioEngine.evalXmlPath(new Variable(body), path);
            if (v.isNotPresent()) {
                return null;
            } else {
                return JsValue.fromJava(v.getValue());
            }
        } else {
            Json json = Json.of(body);
            Object result;
            try {
                result = json.get(path);
            } catch (Exception e) {
                return null;
            }
            return JsValue.fromJava(result);
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy