Skip to content

Commit

Permalink
Merge pull request #1570 from ivangsa/features/mockHandler
Browse files Browse the repository at this point in the history
adds support for multiple mocks and service prefix
  • Loading branch information
ptrthomas authored May 3, 2021
2 parents c5e015f + 9e4913b commit 178a98a
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 140 deletions.
13 changes: 9 additions & 4 deletions karate-core/src/main/java/com/intuit/karate/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -66,8 +67,11 @@ public class Main implements Callable<Void> {
@Parameters(split = "$", description = "one or more tests (features) or search-paths to run")
List<String> paths;

@Option(names = {"-m", "--mock"}, description = "mock server file")
File mock;
@Option(names = {"-m", "--mock", "--mocks"}, split = ",", description = "one or more mock server files")
List<File> mocks;

@Option(names = {"-P", "--prefix"}, description = "mock server path prefix (context-path)")
String prefix = "/";

@Option(names = {"-p", "--port"}, description = "server port (default 8080)")
int port = 8080;
Expand Down Expand Up @@ -365,7 +369,7 @@ public Void call() throws Exception {
server.waitSync();
return null;
}
if (mock == null) {
if (mocks == null || mocks.isEmpty()) {
CommandLine.usage(this, System.err);
return null;
}
Expand All @@ -379,7 +383,8 @@ public Void call() throws Exception {
System.setProperty(Constants.KARATE_ENV, env);
}
MockServer.Builder builder = MockServer
.feature(mock)
.featureFiles(mocks)
.pathPrefix(prefix)
.certFile(cert)
.keyFile(key)
.watch(watch);
Expand Down
285 changes: 172 additions & 113 deletions karate-core/src/main/java/com/intuit/karate/core/MockHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@
import com.intuit.karate.http.ResourceType;
import com.intuit.karate.http.Response;
import com.intuit.karate.http.ServerHandler;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.slf4j.Logger;
Expand Down Expand Up @@ -66,25 +65,70 @@ public class MockHandler implements ServerHandler {
private static final String PATH_PARAMS = "pathParams";
private static final String BODY_PATH = "bodyPath";

private final Feature feature;
private final ScenarioRuntime runtime; // holds global config and vars
private final Map<String, Variable> globals;
private final boolean corsEnabled;

protected static final ThreadLocal<Request> LOCAL_REQUEST = new ThreadLocal<Request>();
private final LinkedHashMap<Feature, ScenarioRuntime> features = new LinkedHashMap<>(); // feature + holds global config and vars
private final Map<String, Variable> globals = new HashMap<>();
private boolean corsEnabled;

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

public MockHandler withPrefix(String prefix) {
this.prefix = prefix;
return this;
}


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

public MockHandler(Feature feature, Map<String, Object> args) {
this.feature = feature;
FeatureRuntime featureRuntime = FeatureRuntime.of(Suite.forTempUse(), 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);
runtime = new ScenarioRuntime(featureRuntime, dummy);
this(Collections.singletonList(feature), args);
}

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

public MockHandler(List<Feature> features, Map<String, Object> args) {
for (Feature feature: features) {
FeatureRuntime featureRuntime = FeatureRuntime.of(Suite.forTempUse(), 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);
initiateScenarioRunTime(runtime);
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);
}
}
corsEnabled = corsEnabled || runtime.engine.getConfig().isCorsEnabled();
globals.putAll(runtime.engine.detachVariables());
runtime.logger.info("mock server initialized: {}", feature);
this.features.put(feature, runtime);
}
}

/**
* Parse Scenario values from feature file ex- pathMatches(), method type - get/post
* @param runtime ScenarioRuntime
*/
private void initiateScenarioRunTime(ScenarioRuntime runtime)
{
runtime.engine.setVariable(PATH_MATCHES, (Function<String, Boolean>) this::pathMatches);
runtime.engine.setVariable(PARAM_EXISTS, (Function<String, Boolean>) this::paramExists);
runtime.engine.setVariable(PARAM_VALUE, (Function<String, String>) this::paramValue);
Expand All @@ -93,27 +137,7 @@ public MockHandler(Feature feature, Map<String, Object> args) {
runtime.engine.setVariable(ACCEPT_CONTAINS, (Function<String, Boolean>) this::acceptContains);
runtime.engine.setVariable(HEADER_CONTAINS, (BiFunction<String, String, Boolean>) this::headerContains);
runtime.engine.setVariable(BODY_PATH, (Function<String, Object>) 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);
}
}
corsEnabled = runtime.engine.getConfig().isCorsEnabled();
globals = runtime.engine.detachVariables();
runtime.logger.info("mock server initialized: {}", feature);
runtime.engine.init();
}

private static final Result PASSED = Result.passed(0);
Expand All @@ -132,90 +156,124 @@ public synchronized Response handle(Request req) { // note the [synchronized]
}
return response;
}
// important for graal to work properly
Thread.currentThread().setContextClassLoader(runtime.featureRuntime.suite.classLoader);
LOCAL_REQUEST.set(req);
req.processBody();
ScenarioEngine engine = new ScenarioEngine(runtime, new HashMap(globals));
ScenarioEngine.set(engine);
engine.init();
engine.setVariable(ScenarioEngine.REQUEST_URL_BASE, req.getUrlBase());
engine.setVariable(ScenarioEngine.REQUEST_URI, req.getPath());
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());
Map<String, List<Map<String, Object>>> parts = req.getMultiParts();
if (parts != null) {
engine.setHiddenVariable(REQUEST_PARTS, parts); // TODO add to docs
}
for (FeatureSection fs : feature.getSections()) {
if (fs.isOutline()) {
runtime.logger.warn("skipping scenario outline - {}:{}", feature, fs.getScenarioOutline().getLine());
break;
req.setPath(req.getPath().substring(prefix.length()));
for (Map.Entry<Feature, ScenarioRuntime> entry: this.features.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();
//Create Scenario engine
ScenarioEngine engine = createScenarioEngine(req, runtime);
Map<String, List<Map<String, Object>>> parts = req.getMultiParts();
if (parts != null) {
engine.setHiddenVariable(REQUEST_PARTS, parts); // TODO add to docs
}
Scenario scenario = fs.getScenario();
if (isMatchingScenario(scenario, engine)) {
Map<String, Object> configureHeaders;
Variable response, responseStatus, responseHeaders, responseDelay;
ScenarioActions actions = new ScenarioActions(engine);
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;
}
for (FeatureSection fs : feature.getSections()) {
if (fs.isOutline()) {
runtime.logger.warn("skipping scenario outline - {}:{}", feature, fs.getScenarioOutline().getLine());
break;
}
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.detachVariables());
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());
Scenario scenario = fs.getScenario();
if (isMatchingScenario(scenario, engine)) {
Map<String, Object> configureHeaders;
Variable response, responseStatus, responseHeaders, responseDelay;
ScenarioActions actions = new ScenarioActions(engine);
Result result = PASSED;
//Execute Steps in Scenario
result = executeScenarioSteps(feature, runtime, scenario, actions, result);
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.detachVariables());
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 (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());
}
return res;
}
if (responseStatus != null) {
res.setStatus(responseStatus.getAsInt());
}
return res;
}
}
runtime.logger.warn("no scenarios matched, returning 404: {}", req);
logger.warn("no scenarios matched, returning 404: {}", req); // NOTE: not logging with engine.logger
return new Response(404);
}

/**
* Execute steps for every scenario identified
* @param feature
* @param runtime
* @param scenario
* @param actions
* @param result
* @return
*/
private Result executeScenarioSteps(Feature feature,
ScenarioRuntime runtime,
Scenario scenario,
ScenarioActions actions,
Result result)
{
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 ScenarioEngine createScenarioEngine(Request req,
ScenarioRuntime runtime)
{
ScenarioEngine engine = new ScenarioEngine(runtime, new HashMap<>(globals));
ScenarioEngine.set(engine);
engine.init();
engine.setVariable(ScenarioEngine.REQUEST_URL_BASE, req.getUrlBase());
engine.setVariable(ScenarioEngine.REQUEST_URI, req.getPath());
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());
return engine;
}

private boolean isMatchingScenario(Scenario scenario, ScenarioEngine engine) {
String expression = StringUtils.trimToNull(scenario.getName() + scenario.getDescription());
if (expression == null) {
Expand Down Expand Up @@ -250,7 +308,8 @@ public boolean pathMatches(String pattern) {

public boolean paramExists(String name) {
Map<String, List<String>> params = LOCAL_REQUEST.get().getParams();
return params == null ? false : params.containsKey(name);
return params != null && params.containsKey(name);

}

public String paramValue(String name) {
Expand All @@ -263,12 +322,12 @@ public boolean methodIs(String name) { // TODO no more supporting array arg

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

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

public boolean headerContains(String name, String value) {
Expand Down
Loading

0 comments on commit 178a98a

Please sign in to comment.