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

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

There is a newer version: 1.4.1
Show newest version
/*
 * The MIT License
 *
 * Copyright 2018 Intuit 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.CallContext;
import com.intuit.karate.Script;
import com.intuit.karate.ScriptBindings;
import com.intuit.karate.ScenarioContext;
import com.intuit.karate.FeatureContext;
import com.intuit.karate.FileUtils;
import com.intuit.karate.JsonUtils;
import com.intuit.karate.Match;
import com.intuit.karate.ScriptValue;
import com.intuit.karate.ScriptValueMap;
import com.intuit.karate.StepActions;
import com.intuit.karate.StringUtils;
import com.intuit.karate.XmlUtils;
import com.intuit.karate.exception.KarateException;
import com.intuit.karate.http.HttpRequest;
import com.intuit.karate.http.HttpResponse;
import com.intuit.karate.http.HttpUtils;
import java.util.List;
import java.util.Map;

/**
 *
 * @author pthomas3
 */
public class FeatureBackend {

    private final Feature feature;
    private final StepActions actions;

    private boolean corsEnabled;

    private final ScenarioContext context;
    private final String featureName;

    private static void putBinding(String name, ScenarioContext context) {
        String function = "function(s){ return " + ScriptBindings.KARATE + "." + name + "(s) }";
        context.getVars().put(name, Script.evalJsExpression(function, context));
    }

    public boolean isCorsEnabled() {
        return corsEnabled;
    }

    public ScenarioContext getContext() {
        return context;
    }

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

    public FeatureBackend(Feature feature, Map vars) {
        this.feature = feature;
        featureName = feature.getPath().toFile().getName();
        CallContext callContext = new CallContext(null, false);
        FeatureContext featureContext = new FeatureContext(feature, null);
        actions = new StepActions(featureContext, callContext);
        context = actions.context;
        putBinding(ScriptBindings.PATH_MATCHES, context);
        putBinding(ScriptBindings.METHOD_IS, context);
        putBinding(ScriptBindings.PARAM_VALUE, context);
        putBinding(ScriptBindings.TYPE_CONTAINS, context);
        putBinding(ScriptBindings.ACCEPT_CONTAINS, context);
        putBinding(ScriptBindings.BODY_PATH, context);
        if (vars != null) {
            ScriptValueMap backendVars = context.getVars();
            vars.forEach((k, v) -> backendVars.put(k, v));
        }
        // the background is evaluated one-time
        if (feature.isBackgroundPresent()) {
            for (Step step : feature.getBackground().getSteps()) {
                Result result = Engine.executeStep(step, actions);
                if (result.isFailed()) {
                    String message = "server-side background init failed - " + featureName + ":" + step.getLine();
                    actions.context.logger.error(message);
                    throw new KarateException(message, result.getError());
                }
            }
        }
        // this is a special case, we support the auto-handling of cors
        // only if '* configure cors = true' has been done in the Background
        corsEnabled = context.getConfig().isCorsEnabled(); 
        actions.context.logger.info("backend initialized");
    }

    public ScriptValueMap handle(ScriptValueMap args) {
        boolean matched = false;
        ScriptValueMap vars = actions.context.getVars();
        vars.putAll(args);
        for (FeatureSection fs : feature.getSections()) {
            if (fs.isOutline()) {
                actions.context.logger.warn("skipping scenario outline - {}:{}", featureName, fs.getScenarioOutline().getLine());
                break;
            }
            Scenario scenario = fs.getScenario();
            if (isMatchingScenario(scenario)) {
                matched = true;
                for (Step step : scenario.getSteps()) {
                    Result result = Engine.executeStep(step, actions);
                    if (result.isAborted()) {
                        actions.context.logger.debug("abort at {}:{}", featureName, step.getLine());
                        break;
                    }
                    if (result.isFailed()) {
                        String message = "server-side scenario failed - " + featureName + ":" + step.getLine();
                        actions.context.logger.error(message);
                        throw new KarateException(message, result.getError());
                    }
                }
                break; // process only first matching scenario
            }
        }
        if (!matched) {
            context.logger.warn("no scenarios matched");
        }
        return vars;
    }

    private boolean isMatchingScenario(Scenario scenario) {
        String expression = StringUtils.trimToNull(scenario.getName() + scenario.getDescription());
        if (expression == null) {
            context.logger.debug("scenario matched: (empty)");
            return true;
        }
        try {
            ScriptValue sv = Script.evalJsExpression(expression, context);
            if (sv.isBooleanTrue()) {
                context.logger.debug("scenario matched: {}", expression);
                return true;
            } else {
                context.logger.debug("scenario skipped: {}", expression);
                return false;
            }
        } catch (Exception e) {
            context.logger.warn("scenario match evaluation failed: {}", e.getMessage());
            return false;
        }
    }
    
    private static final String VAR_AFTER_SCENARIO = "afterScenario";
    private static final String ALLOWED_METHODS = "GET, HEAD, POST, PUT, DELETE, PATCH";
    
    public HttpResponse buildResponse(HttpRequest request, long startTime) {
        if (corsEnabled && "OPTIONS".equals(request.getMethod())) {
            HttpResponse response = new HttpResponse(startTime, System.currentTimeMillis());
            response.setStatus(200);
            response.addHeader(HttpUtils.HEADER_ALLOW, ALLOWED_METHODS);
            response.addHeader(HttpUtils.HEADER_AC_ALLOW_ORIGIN, "*");
            response.addHeader(HttpUtils.HEADER_AC_ALLOW_METHODS, ALLOWED_METHODS);
            List requestHeaders = request.getHeaders().get(HttpUtils.HEADER_AC_REQUEST_HEADERS);
            if (requestHeaders != null) {
                response.putHeader(HttpUtils.HEADER_AC_ALLOW_HEADERS, requestHeaders);
            }            
            return response;
        }
        Match match = Match.init()
                .defText(ScriptValueMap.VAR_REQUEST_URL_BASE, request.getUrlBase())
                .defText(ScriptValueMap.VAR_REQUEST_URI, request.getUri())
                .defText(ScriptValueMap.VAR_REQUEST_METHOD, request.getMethod())
                .def(ScriptValueMap.VAR_REQUEST_HEADERS, request.getHeaders())
                .def(ScriptValueMap.VAR_RESPONSE_STATUS, 200)
                .def(ScriptValueMap.VAR_REQUEST_PARAMS, request.getParams());
        byte[] requestBytes = request.getBody();
        if (requestBytes != null) {
            match.def(ScriptValueMap.VAR_REQUEST_BYTES, requestBytes);
            String requestString = FileUtils.toString(requestBytes);
            Object requestBody = requestString;
            if (Script.isJson(requestString)) {
                try {
                    requestBody = JsonUtils.toJsonDoc(requestString);
                } catch (Exception e) {
                    context.logger.warn("json parsing failed, request data type set to string: {}", e.getMessage());
                }
            } else if (Script.isXml(requestString)) {
                try {
                    requestBody = XmlUtils.toXmlDoc(requestString);
                } catch (Exception e) {
                    context.logger.warn("xml parsing failed, request data type set to string: {}", e.getMessage());
                }
            }
            match.def(ScriptValueMap.VAR_REQUEST, requestBody);
        }
        ScriptValue responseValue, responseStatusValue, responseHeaders, afterScenario;
        Map responseHeadersMap, configResponseHeadersMap;
        // this is a sledgehammer approach to concurrency !
        // which is why for simulating 'delay', users should use the VAR_AFTER_SCENARIO (see end)
        synchronized (this) { // BEGIN TRANSACTION !
            ScriptValueMap result = handle(match.vars());
            ScriptValue configResponseHeaders = context.getConfig().getResponseHeaders();
            responseValue = result.remove(ScriptValueMap.VAR_RESPONSE);
            responseStatusValue = result.remove(ScriptValueMap.VAR_RESPONSE_STATUS);
            responseHeaders = result.remove(ScriptValueMap.VAR_RESPONSE_HEADERS);
            afterScenario = result.remove(VAR_AFTER_SCENARIO);
            if (afterScenario == null) {
                afterScenario = context.getConfig().getAfterScenario();
            }
            configResponseHeadersMap = configResponseHeaders == null ? null : configResponseHeaders.evalAsMap(context);
            responseHeadersMap = responseHeaders == null ? null : responseHeaders.evalAsMap(context);
        } // END TRANSACTION !!
        int responseStatus = responseStatusValue == null ? 200 : Integer.valueOf(responseStatusValue.getAsString());
        HttpResponse response = new HttpResponse(startTime, System.currentTimeMillis());
        response.setStatus(responseStatus);
        if (responseValue != null && !responseValue.isNull()) {
            if (responseValue.isByteArray()) {
                response.setBody(responseValue.getValue(byte[].class));
            } else {
                response.setBody(FileUtils.toBytes(responseValue.getAsString()));
            }
        }
        // trying to avoid creating a map unless absolutely necessary
        if (responseHeadersMap != null) {
            if (configResponseHeadersMap != null) {
                responseHeadersMap.putAll(configResponseHeadersMap);
            }
        } else if (configResponseHeadersMap != null) {
            responseHeadersMap = configResponseHeadersMap;
        }
        if (responseHeadersMap != null) {
            responseHeadersMap.forEach((k, v) -> {
                if (v instanceof List) { // MultiValueMap returned by proceed / response.headers
                    response.putHeader(k, (List) v);
                } else if (v != null) {                    
                    response.addHeader(k, v.toString());
                }
            }); 
        }
        if (responseValue != null && (responseHeadersMap == null || !responseHeadersMap.containsKey(HttpUtils.HEADER_CONTENT_TYPE))) {
            response.addHeader(HttpUtils.HEADER_CONTENT_TYPE, HttpUtils.getContentType(responseValue));
        }
        if (corsEnabled) {
            response.addHeader(HttpUtils.HEADER_AC_ALLOW_ORIGIN, "*");
        }
        // functions here are outside of the 'transaction' and should not mutate global state !
        // typically this is where users can set up an artificial delay or sleep
        if (afterScenario != null && afterScenario.isFunction()) {
            afterScenario.invokeFunction(context);
        }
        return response;
    }    

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy