From 713268621882b20ac7f522934fd661e28ca2efa4 Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Tue, 10 Nov 2020 11:52:31 +0530 Subject: [PATCH] [rewrite] #1281 mocks done, but with new api, see prev commit for diff --- .../intuit/karate/runtime/MockHandler.java | 47 ++++--- .../com/intuit/karate/runtime/MockServer.java | 125 ++++++++++++++++++ .../intuit/karate/runtime/ScenarioBridge.java | 69 ++++++++-- .../intuit/karate/runtime/ScenarioEngine.java | 24 ++-- .../karate/runtime/ScenarioListener.java | 14 +- .../karate/runtime/ScenarioRuntime.java | 65 ++++----- .../karate/server/HttpRequestBuilder.java | 12 +- .../com/intuit/karate/server/HttpServer.java | 6 +- .../karate/server/HttpServerHandler.java | 1 + .../karate/server/MultiPartBuilder.java | 3 +- .../intuit/karate/server/ResourceType.java | 8 +- .../karate/server/SslContextFactory.java | 89 +++++++++++++ .../karate/runtime/MockHandlerTest.java | 25 +++- .../karate/runtime/ScenarioRuntimeTest.java | 20 ++- .../com/intuit/karate/runtime/increment.js | 9 ++ 15 files changed, 430 insertions(+), 87 deletions(-) create mode 100644 karate-core/src/main/java/com/intuit/karate/runtime/MockServer.java create mode 100644 karate-core/src/main/java/com/intuit/karate/server/SslContextFactory.java create mode 100644 karate-core/src/test/java/com/intuit/karate/runtime/increment.js diff --git a/karate-core/src/main/java/com/intuit/karate/runtime/MockHandler.java b/karate-core/src/main/java/com/intuit/karate/runtime/MockHandler.java index 8e39b9927..c230c4ed8 100644 --- a/karate-core/src/main/java/com/intuit/karate/runtime/MockHandler.java +++ b/karate-core/src/main/java/com/intuit/karate/runtime/MockHandler.java @@ -50,15 +50,15 @@ * @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_FILES = "requestFiles"; - + 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"; @@ -68,17 +68,22 @@ public class MockHandler implements ServerHandler { private static final String PARAM_EXISTS = "paramExists"; private static final String PATH_PARAMS = "pathParams"; private static final String BODY_PATH = "bodyPath"; - + private final Feature feature; private final String featureName; private final ScenarioRuntime runtime; // holds global config and vars protected static final ThreadLocal LOCAL_REQUEST = new ThreadLocal(); - + public MockHandler(Feature feature) { + this(feature, null); + } + + public MockHandler(Feature feature, Map args) { this.feature = feature; featureName = feature.getPath().toFile().getName(); FeatureRuntime featureRuntime = new FeatureRuntime(SuiteRuntime.forMock(), feature); + featureRuntime.setCallArg(args); FeatureSection section = new FeatureSection(); section.setIndex(-1); // TODO util for creating dummy scenario Scenario dummy = new Scenario(feature, section, -1); @@ -93,6 +98,7 @@ public MockHandler(Feature feature) { runtime.engine.setVariable(HEADER_CONTAINS, (BiFunction) this::headerContains); runtime.engine.setVariable(BODY_PATH, (Function) this::bodyPath); if (feature.isBackgroundPresent()) { + ScenarioEngine.set(runtime.engine); runtime.engine.init(); for (Step step : feature.getBackground().getSteps()) { Result result = StepRuntime.execute(step, runtime.actions); @@ -105,9 +111,9 @@ public MockHandler(Feature feature) { } runtime.logger.info("mock server initialized: {}", featureName); } - + private static final Result PASSED = Result.passed(0); - + @Override public Response handle(Request req) { Thread.currentThread().setContextClassLoader(runtime.featureRuntime.suite.classLoader); @@ -183,7 +189,9 @@ public Response handle(Request req) { res.setBody(response.getAsByteArray()); if (res.getContentType() == null) { ResourceType rt = ResourceType.fromObject(response.getValue()); - res.setContentType(rt.contentType); + if (rt != null) { + res.setContentType(rt.contentType); + } } } if (responseStatus != null) { @@ -196,11 +204,12 @@ public Response handle(Request req) { runtime.logger.warn("no scenarios matched, returning 404: {}", req); return new Response(404); } - + private boolean isMatchingScenario(Scenario scenario, ScenarioEngine engine) { String expression = StringUtils.trimToNull(scenario.getName() + scenario.getDescription()); if (expression == null) { runtime.logger.debug("default scenario matched at line: {}", scenario.getLine()); + return true; } try { Variable v = engine.evalJs(expression); @@ -216,7 +225,7 @@ private boolean isMatchingScenario(Scenario scenario, ScenarioEngine engine) { return false; } } - + public boolean pathMatches(String pattern) { String uri = LOCAL_REQUEST.get().getPath(); Map pathParams = HttpUtils.parseUriPattern(pattern, uri); @@ -227,30 +236,30 @@ public boolean pathMatches(String pattern) { return true; } } - + public boolean paramExists(String name) { Map> params = LOCAL_REQUEST.get().getParams(); return params == null ? false : 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 ? false : contentType.contains(text); } - + public boolean acceptContains(String text) { String acceptHeader = LOCAL_REQUEST.get().getHeader("Accept"); return acceptHeader == null ? false : acceptHeader.contains(text); } - + public boolean headerContains(String name, String value) { List values = LOCAL_REQUEST.get().getHeaderValues(name); if (values != null) { @@ -262,7 +271,7 @@ public boolean headerContains(String name, String value) { } return false; } - + public Object bodyPath(String path) { Object body = LOCAL_REQUEST.get().getBodyConverted(); if (body == null) { @@ -276,5 +285,5 @@ public Object bodyPath(String path) { return JsValue.fromJava(json.get(path)); } } - + } diff --git a/karate-core/src/main/java/com/intuit/karate/runtime/MockServer.java b/karate-core/src/main/java/com/intuit/karate/runtime/MockServer.java new file mode 100644 index 000000000..37c53996d --- /dev/null +++ b/karate-core/src/main/java/com/intuit/karate/runtime/MockServer.java @@ -0,0 +1,125 @@ +/* + * The MIT License + * + * Copyright 2020 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.runtime; + +import com.intuit.karate.core.Feature; +import com.intuit.karate.core.FeatureParser; +import com.intuit.karate.server.HttpServer; +import com.intuit.karate.server.ServerHandler; +import com.intuit.karate.server.SslContextFactory; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.server.Server; +import com.linecorp.armeria.server.ServerBuilder; +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author pthomas3 + */ +public class MockServer extends HttpServer { + + private MockServer(ServerBuilder sb, ServerHandler handler) { + super(sb, handler); + } + + public static class Builder { + + Builder(Feature feature) { + this.feature = feature; + } + + final Feature feature; + int port; + boolean ssl; + File certFile; + File keyFile; + Map args; + + public Builder http(int value) { + port = value; + return this; + } + + public Builder https(int value) { + ssl = true; + port = value; + return this; + } + + public Builder certFile(File value) { + certFile = value; + return this; + } + + public Builder keyFile(File value) { + keyFile = value; + return this; + } + + public Builder args(Map value) { + args = value; + return this; + } + + public Builder arg(String name, Object value) { + if (args == null) { + args = new HashMap(); + } + args.put(name, value); + return this; + } + + public MockServer build() { + ServerBuilder sb = Server.builder(); + if (ssl) { + sb.https(port); + SslContextFactory factory = new SslContextFactory(); + factory.setCertFile(certFile); + factory.setKeyFile(keyFile); + factory.build(); + sb.tls(factory.getCertFile(), factory.getKeyFile()); + } else { + sb.http(port); + } + MockHandler mockHandler = new MockHandler(feature, args); + return new MockServer(sb, mockHandler); + } + + } + + public static Builder feature(String path) { + return new Builder(FeatureParser.parse(path)); + } + + public static Builder feature(File file) { + return new Builder(FeatureParser.parse(file)); + } + + public static Builder feature(Feature feature) { + return new Builder(feature); + } + +} diff --git a/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioBridge.java b/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioBridge.java index 61160059d..6122c5cc4 100644 --- a/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioBridge.java +++ b/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioBridge.java @@ -28,12 +28,12 @@ import com.intuit.karate.Resource; import com.intuit.karate.StringUtils; import com.intuit.karate.XmlUtils; +import com.intuit.karate.core.Feature; import com.intuit.karate.core.PerfEvent; import com.intuit.karate.core.Scenario; import com.intuit.karate.data.Json; import com.intuit.karate.data.JsonUtils; import com.intuit.karate.exception.KarateException; -import com.intuit.karate.graal.JsEngine; import com.intuit.karate.graal.JsList; import com.intuit.karate.graal.JsMap; import com.intuit.karate.graal.JsValue; @@ -49,12 +49,12 @@ import com.intuit.karate.shell.Command; import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -228,7 +228,7 @@ public void configure(String key, Value value) { public void embed(Object o, String contentType) { if (contentType == null) { - contentType = ResourceType.fromObject(o).contentType; + contentType = ResourceType.fromObject(o, ResourceType.BINARY).contentType; } getEngine().runtime.embed(JsValue.toBytes(o), contentType); } @@ -247,7 +247,7 @@ public String exec(String line) { } public String exec(Value value) { - return execInternal(new JsValue(value).getValue()); + return execInternal(new JsValue(value).getAsMap()); } private String execInternal(Map options) { @@ -397,12 +397,12 @@ private Command toCommand(boolean useLineFeed, Map options) { Value funOut = (Value) options.get("listener"); if (funOut != null && funOut.canExecute()) { ScenarioListener sl = new ScenarioListener(engine, funOut); - command.setListener(s -> sl.consume(s)); + command.setListener(sl); } Value funErr = (Value) options.get("errorListener"); if (funErr != null && funErr.canExecute()) { ScenarioListener sl = new ScenarioListener(engine, funErr); - command.setErrorListener(s -> sl.consume(s)); + command.setErrorListener(sl); } Boolean start = (Boolean) options.get("start"); if (start == null) { @@ -677,6 +677,47 @@ public void signal(Object result) { getEngine().signal(result); } + public MockServer start(String mock) { + return startInternal(Collections.singletonMap("mock", mock)); + } + + public MockServer start(Value value) { + return startInternal(new JsValue(value).getAsMap()); + } + + private MockServer startInternal(Map config) { + String mock = (String) config.get("mock"); + if (mock == null) { + throw new RuntimeException("'mock' is missing: " + config); + } + File feature = toJavaFile(mock); + MockServer.Builder builder = MockServer.feature(feature); + String certFile = (String) config.get("cert"); + if (certFile != null) { + builder.certFile(toJavaFile(certFile)); + } + String keyFile = (String) config.get("key"); + if (keyFile != null) { + builder.keyFile(toJavaFile(keyFile)); + } + Boolean ssl = (Boolean) config.get("ssl"); + if (ssl == null) { + ssl = false; + } + Integer port = (Integer) config.get("port"); + if (port == null) { + port = 0; + } + Map arg = (Map) config.get("arg"); + builder.args(arg); + if (ssl) { + builder.https(port); + } else { + builder.http(port); + } + return builder.build(); + } + public void stop(int port) { Command.waitForSocket(port); } @@ -702,7 +743,15 @@ public String toCsv(Object o) { } public Object toJava(Value value) { - return new JsValue(value).getValue(); + if (value.canExecute()) { + return new ScenarioListener(getEngine(), value); + } else { + return new JsValue(value).getValue(); + } + } + + private File toJavaFile(String path) { + return getEngine().fileReader.relativePathToFile(path); } public Object toJson(Value value) { @@ -775,8 +824,7 @@ public WebSocketClient webSocket(String url, Value listener, Value value) { if (listener == null || !listener.canExecute()) { handler = m -> true; } else { - ScenarioListener sl = new ScenarioListener(getEngine(), listener); - handler = m -> sl.apply(m); + handler = new ScenarioListener(getEngine(), listener); } WebSocketOptions options = new WebSocketOptions(url, value == null ? null : new JsValue(value).getValue()); options.setTextHandler(handler); @@ -796,8 +844,7 @@ public WebSocketClient webSocketBinary(String url, Value listener, Value value) if (listener == null || !listener.canExecute()) { handler = m -> true; } else { - ScenarioListener sl = new ScenarioListener(getEngine(), listener); - handler = m -> sl.apply(m); + handler = new ScenarioListener(getEngine(), listener); } WebSocketOptions options = new WebSocketOptions(url, value == null ? null : new JsValue(value).getValue()); options.setBinaryHandler(handler); diff --git a/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java b/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java index 7d6dd61e5..1b2a7d572 100644 --- a/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java +++ b/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java @@ -102,6 +102,7 @@ public class ScenarioEngine { public static final String REQUEST = "request"; public static final String REQUEST_URL_BASE = "requestUrlBase"; public static final String REQUEST_URI = "requestUri"; + private static final String REQUEST_PARAMS = "requestParams"; public static final String REQUEST_METHOD = "requestMethod"; public static final String REQUEST_HEADERS = "requestHeaders"; private static final String REQUEST_TIME_STAMP = "requestTimeStamp"; @@ -114,7 +115,6 @@ public class ScenarioEngine { private final Function readFunction; private final ScenarioBridge bridge; - protected Map magicVariables; private boolean aborted; private Throwable failedReason; @@ -587,7 +587,7 @@ public void method(String method) { http.method(method); httpInvoke(); } - + // extracted for mock proceed() private void httpInvoke() { if (http.isRetry()) { @@ -738,6 +738,7 @@ public void proceed(String requestUrlBase) { String uri = vars.get(REQUEST_URI).getValue(); String url = uri == null ? urlBase : urlBase + "/" + uri; http.url(url); + http.params(vars.get(REQUEST_PARAMS).getValue()); http.method(vars.get(REQUEST_METHOD).getValue()); http.headers(vars.get(REQUEST_HEADERS).getValue()); http.removeHeader(HttpConstants.HDR_CONTENT_LENGTH); @@ -832,7 +833,7 @@ protected void init() { // not in constructor because it has to be on Runnable.r attachVariables(); setHiddenVariable(KARATE, bridge); setHiddenVariable(READ, readFunction); - setVariables(magicVariables); + setVariables(runtime.magicVariables); HttpClient client = runtime.featureRuntime.suite.clientFactory.create(this); http = new HttpRequestBuilder(client); } @@ -958,10 +959,6 @@ public Value attach(Value before) { return JS.attach(before); } - public JsValue executeJsValue(Value function, Object... args) { - return JS.execute(function, args); - } - protected Map getOrEvalAsMap(Variable var) { if (var.isJsOrJavaFunction()) { Variable res = executeFunction(var); @@ -975,7 +972,7 @@ public Variable executeFunction(Variable var, Object... args) { switch (var.type) { case JS_FUNCTION: Value jsFunction = var.getValue(); - JsValue jsResult = JS.execute(jsFunction, args); + JsValue jsResult = executeJsValue(jsFunction, args); return new Variable(jsResult); case JAVA_FUNCTION: // definitely a "call" with a single argument Function javaFunction = var.getValue(); @@ -987,6 +984,17 @@ public Variable executeFunction(Variable var, Object... args) { } } + private JsValue executeJsValue(Value function, Object... args) { + try { + return JS.execute(function, args); + } catch (Exception e) { + String jsSource = function.getSourceLocation().getCharacters().toString(); + KarateException ke = fromJsEvalException(jsSource, e); + setFailedReason(ke); + throw ke; + } + } + public Variable evalJs(String js) { try { return new Variable(JS.eval(js)); diff --git a/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioListener.java b/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioListener.java index 811a0c4e1..f6c005596 100644 --- a/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioListener.java +++ b/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioListener.java @@ -24,13 +24,15 @@ package com.intuit.karate.runtime; import com.intuit.karate.graal.JsValue; +import java.util.function.Consumer; +import java.util.function.Function; import org.graalvm.polyglot.Value; /** * * @author pthomas3 */ -public class ScenarioListener { +public class ScenarioListener implements Consumer, Function { private final ScenarioEngine parent; private final ScenarioEngine child; @@ -54,12 +56,14 @@ private Value get() { return function; } - public void consume(Object... args) { - get().executeVoid(args); + @Override + public void accept(Object arg) { + get().executeVoid(arg); } - public T apply(Object... args) { - Value result = get().execute(args); + @Override + public Object apply(Object arg) { + Value result = get().execute(arg); return new JsValue(result).getValue(); } diff --git a/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioRuntime.java b/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioRuntime.java index 884239b41..86dc66d1a 100644 --- a/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioRuntime.java +++ b/karate-core/src/main/java/com/intuit/karate/runtime/ScenarioRuntime.java @@ -51,9 +51,9 @@ * @author pthomas3 */ public class ScenarioRuntime implements Runnable { - + public final Logger logger = new Logger(); - + public final FeatureRuntime featureRuntime; public final ScenarioRuntime background; public final ScenarioCall caller; @@ -63,11 +63,12 @@ public class ScenarioRuntime implements Runnable { public final ScenarioResult result; public final ScenarioEngine engine; public final boolean reportDisabled; - + public final Map magicVariables; + public ScenarioRuntime(FeatureRuntime featureRuntime, Scenario scenario) { this(featureRuntime, scenario, null); } - + public ScenarioRuntime(FeatureRuntime featureRuntime, Scenario scenario, ScenarioRuntime background) { this.featureRuntime = featureRuntime; this.caller = featureRuntime.caller; @@ -84,6 +85,7 @@ public ScenarioRuntime(FeatureRuntime featureRuntime, Scenario scenario, Scenari } actions = new ScenarioActions(engine); this.scenario = scenario; + magicVariables = initMagicVariables(); // depends on scenario if (background == null) { this.background = null; result = new ScenarioResult(scenario, null); @@ -97,44 +99,44 @@ public ScenarioRuntime(FeatureRuntime featureRuntime, Scenario scenario, Scenari tags = Tags.merge(featureRuntime.feature.getTags(), scenario.getTags()); reportDisabled = tags.valuesFor("report").isAnyOf("false"); } - + public boolean isFailed() { return error != null || result.isFailed(); } - + public Step getCurrentStep() { return currentStep; } - + public boolean isStopped() { return stopped; } - + public LogAppender getAppender() { return appender; } - + public void embed(byte[] bytes, String contentType) { Embed embed = new Embed(); embed.setBytes(bytes); embed.setMimeType(contentType); embed(embed); } - + public void embed(Embed embed) { if (embeds == null) { embeds = new ArrayList(); } embeds.add(embed); } - + public void addCallResult(FeatureResult fr) { if (callResults == null) { callResults = new ArrayList(); } callResults.add(fr); } - + private List steps; private LogAppender appender; private List embeds; @@ -145,7 +147,7 @@ public void addCallResult(FeatureResult fr) { private boolean stopped; private boolean aborted; private int stepIndex; - + public void stepBack() { stopped = false; stepIndex -= 2; @@ -153,7 +155,7 @@ public void stepBack() { stepIndex = 0; } } - + public void stepReset() { stopped = false; stepIndex--; @@ -161,15 +163,15 @@ public void stepReset() { stepIndex = 0; } } - + public void stepProceed() { stopped = false; } - + private int nextStepIndex() { return stepIndex++; } - + public Result evalAsStep(String expression) { Step evalStep = new Step(scenario.getFeature(), scenario, scenario.getIndex() + 1); try { @@ -179,7 +181,7 @@ public Result evalAsStep(String expression) { } return StepRuntime.execute(evalStep, actions); } - + public boolean hotReload() { boolean success = false; Feature feature = scenario.getFeature(); @@ -203,7 +205,7 @@ public boolean hotReload() { } return success; } - + public Map getScenarioInfo() { Map info = new HashMap(6); Path featurePath = featureRuntime.feature.getPath(); @@ -218,7 +220,7 @@ public Map getScenarioInfo() { info.put("errorMessage", errorMessage); return info; } - + protected void logError(String message) { if (currentStep != null) { message = currentStep.getDebugInfo() @@ -227,7 +229,7 @@ protected void logError(String message) { } logger.error("{}", message); } - + @Override public void run() { try { // make sure we call afterRun() even on crashes @@ -251,7 +253,7 @@ public void run() { afterRun(); } } - + private static final ThreadLocal APPENDER = new ThreadLocal() { @Override protected LogAppender initialValue() { @@ -259,8 +261,8 @@ protected LogAppender initialValue() { return new FileLogAppender(new File(fileName)); } }; - - protected Map getMagicVariables() { + + private Map initMagicVariables() { Map map = new HashMap(); Variable arg = caller.getArg(); if (caller.isNone()) { // if feature called via java api @@ -274,7 +276,7 @@ protected Map getMagicVariables() { map.putAll(arg.getValue()); } } - if (scenario.isOutline()) { // init examples row magic variables + if (scenario.isOutline() && !scenario.isDynamic()) { // init examples row magic variables Map exampleData = scenario.getExampleData(); exampleData.forEach((k, v) -> map.put(k, v)); map.put("__row", exampleData); @@ -283,7 +285,7 @@ protected Map getMagicVariables() { } return map; } - + public void beforeRun() { String env = featureRuntime.suite.getEnv(); // this lazy-inits (one time) the suite env if (appender == null) { // not perf, not debug @@ -294,7 +296,6 @@ public void beforeRun() { steps = scenario.getBackgroundSteps(); } else { steps = background == null ? scenario.getStepsIncludingBackground() : scenario.getSteps(); - engine.magicVariables = getMagicVariables(); } ScenarioEngine.set(engine); engine.init(); @@ -308,7 +309,7 @@ public void beforeRun() { } featureRuntime.suite.resolveHooks().forEach(h -> h.beforeScenario(this)); } - + private void evalConfigJs(String js, String name) { if (js == null) { return; @@ -373,7 +374,7 @@ public StepResult execute(Step step) { return currentStepResult; } } - + public void afterRun() { try { result.setEndTime(System.currentTimeMillis() - featureRuntime.suite.results.getStartTime()); @@ -403,7 +404,7 @@ public void afterRun() { // ScenarioEngine.remove(); } } - + public boolean isSelectedForExecution() { Feature feature = scenario.getFeature(); int callLine = feature.getCallLine(); @@ -446,10 +447,10 @@ public boolean isSelectedForExecution() { return true; // when called, all scenarios match by default } } - + @Override public String toString() { return scenario.toString(); } - + } diff --git a/karate-core/src/main/java/com/intuit/karate/server/HttpRequestBuilder.java b/karate-core/src/main/java/com/intuit/karate/server/HttpRequestBuilder.java index f32660708..c29ab46f0 100644 --- a/karate-core/src/main/java/com/intuit/karate/server/HttpRequestBuilder.java +++ b/karate-core/src/main/java/com/intuit/karate/server/HttpRequestBuilder.java @@ -172,8 +172,11 @@ public HttpRequest build() { request.setBody(JsValue.toBytes(body)); if (multiPart == null) { String contentType = getContentType(); - if (contentType == null) { - contentType = ResourceType.fromObject(body).contentType; + if (contentType == null) { + ResourceType rt = ResourceType.fromObject(body); + if (rt != null) { + contentType = rt.contentType; + } } // TODO clean up http utils Charset charset = HttpUtils.parseContentTypeCharset(contentType); @@ -359,6 +362,11 @@ public HttpRequestBuilder param(String name, List values) { params.put(name, values); return this; } + + public HttpRequestBuilder params(Map> params) { + this.params = params; + return this; + } public HttpRequestBuilder cookies(Collection cookies) { for (Map map : cookies) { diff --git a/karate-core/src/main/java/com/intuit/karate/server/HttpServer.java b/karate-core/src/main/java/com/intuit/karate/server/HttpServer.java index 04078b16f..4cc307a08 100644 --- a/karate-core/src/main/java/com/intuit/karate/server/HttpServer.java +++ b/karate-core/src/main/java/com/intuit/karate/server/HttpServer.java @@ -59,9 +59,11 @@ public CompletableFuture stop() { } public HttpServer(int port, ServerHandler handler) { + this(Server.builder().http(port), handler); + } + + public HttpServer(ServerBuilder sb, ServerHandler handler) { this.handler = handler; - ServerBuilder sb = Server.builder(); - sb.http(port); sb.service("prefix:/", new HttpServerHandler(handler)); server = sb.build(); future = server.start(); diff --git a/karate-core/src/main/java/com/intuit/karate/server/HttpServerHandler.java b/karate-core/src/main/java/com/intuit/karate/server/HttpServerHandler.java index 2a044ad8f..651b53017 100644 --- a/karate-core/src/main/java/com/intuit/karate/server/HttpServerHandler.java +++ b/karate-core/src/main/java/com/intuit/karate/server/HttpServerHandler.java @@ -63,6 +63,7 @@ private Request toRequest(ServiceRequestContext ctx, AggregatedHttpRequest req) Request request = new Request(); request.setRequestContext(ctx); request.setUrl(req.path()); + request.setUrlBase(req.scheme() + "://" + req.authority()); request.setMethod(req.method().name()); RequestHeaders rh = req.headers(); if (rh != null) { diff --git a/karate-core/src/main/java/com/intuit/karate/server/MultiPartBuilder.java b/karate-core/src/main/java/com/intuit/karate/server/MultiPartBuilder.java index ac819b150..642fb4257 100644 --- a/karate-core/src/main/java/com/intuit/karate/server/MultiPartBuilder.java +++ b/karate-core/src/main/java/com/intuit/karate/server/MultiPartBuilder.java @@ -185,8 +185,7 @@ MultiPartBuilder add() { charset = client.getConfig().getCharset(); } if (contentType == null) { - ResourceType rt = ResourceType.fromObject(value); - contentType = rt.contentType; + contentType = ResourceType.fromObject(value, ResourceType.BINARY).contentType; } byte[] encoded = value == null ? HttpConstants.ZERO_BYTES : JsValue.toBytes(value); if (filename == null) { diff --git a/karate-core/src/main/java/com/intuit/karate/server/ResourceType.java b/karate-core/src/main/java/com/intuit/karate/server/ResourceType.java index d4509ddc0..ad1e13cc7 100644 --- a/karate-core/src/main/java/com/intuit/karate/server/ResourceType.java +++ b/karate-core/src/main/java/com/intuit/karate/server/ResourceType.java @@ -127,14 +127,20 @@ public static ResourceType fromContentType(String ct) { } public static ResourceType fromObject(Object o) { + return fromObject(o, null); + } + + public static ResourceType fromObject(Object o, ResourceType defaultType) { if (o instanceof List || o instanceof Map) { return JSON; } else if (o instanceof String) { return TEXT; } else if (o instanceof Node) { return XML; - } else { + } else if (o instanceof byte[]) { return BINARY; + } else { + return defaultType; } } diff --git a/karate-core/src/main/java/com/intuit/karate/server/SslContextFactory.java b/karate-core/src/main/java/com/intuit/karate/server/SslContextFactory.java new file mode 100644 index 000000000..a96097f4c --- /dev/null +++ b/karate-core/src/main/java/com/intuit/karate/server/SslContextFactory.java @@ -0,0 +1,89 @@ +/* + * The MIT License + * + * Copyright 2020 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.server; + +import com.intuit.karate.FileUtils; +import com.intuit.karate.netty.NettyUtils; +import java.io.File; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author pthomas3 + */ +public class SslContextFactory { + + private static final Logger logger = LoggerFactory.getLogger(SslContextFactory.class); + + public static final String DEFAULT_CERT_NAME = "cert.pem"; + public static final String DEFAULT_KEY_NAME = "key.pem"; + + private String buildDir; + private File certFile; + private File keyFile; + + public void setBuildDir(String buildDir) { + this.buildDir = buildDir; + } + + public void setCertFile(File certFile) { + this.certFile = certFile; + } + + public void setKeyFile(File keyFile) { + this.keyFile = keyFile; + } + + public File getCertFile() { + return certFile; + } + + public File getKeyFile() { + return keyFile; + } + + public void build() { + if (buildDir == null) { + buildDir = FileUtils.getBuildDir(); + } + try { + if (certFile == null || keyFile == null) { + // attempt to re-use as far as possible + certFile = new File(buildDir + File.separator + DEFAULT_CERT_NAME); + keyFile = new File(buildDir + File.separator + DEFAULT_KEY_NAME); + } + if (!certFile.exists() || !keyFile.exists()) { + logger.warn("ssl - " + certFile + " and / or " + keyFile + " not found, will create"); + NettyUtils.createSelfSignedCertificate(certFile, keyFile); + } else { + logger.info("ssl - re-using existing files: {} and {}", certFile, keyFile); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + +} diff --git a/karate-core/src/test/java/com/intuit/karate/runtime/MockHandlerTest.java b/karate-core/src/test/java/com/intuit/karate/runtime/MockHandlerTest.java index 6a213e282..7c35b3a7e 100644 --- a/karate-core/src/test/java/com/intuit/karate/runtime/MockHandlerTest.java +++ b/karate-core/src/test/java/com/intuit/karate/runtime/MockHandlerTest.java @@ -95,7 +95,18 @@ void testQueryParams() { request.path("/hello").param("foo", "world"); handle(); match(response.getBodyAsString(), "hello world"); - } + } + + @Test + void testQueryParamExists() { + background().scenario( + "pathMatches('/hello') && paramExists('foo')", + "def response = 'hello ' + paramValue('foo')" + ); + request.path("/hello").param("foo", "world"); + handle(); + match(response.getBodyAsString(), "hello world"); + } @Test void testFormFieldsRequestPost() { @@ -254,4 +265,16 @@ void testGraalJavaClassLoading() { match(response.getBody(), MockUtils.testBytes); } + @Test + void testJsVariableInBackground() { + background( + "def nextId = call read('increment.js')" + ).scenario( + "pathMatches('/hello')", "def response = nextId()" + ); + request.path("/hello"); + handle(); + match(response.getBodyAsString(), "1"); + } + } diff --git a/karate-core/src/test/java/com/intuit/karate/runtime/ScenarioRuntimeTest.java b/karate-core/src/test/java/com/intuit/karate/runtime/ScenarioRuntimeTest.java index 0f0612e0e..14e12c206 100644 --- a/karate-core/src/test/java/com/intuit/karate/runtime/ScenarioRuntimeTest.java +++ b/karate-core/src/test/java/com/intuit/karate/runtime/ScenarioRuntimeTest.java @@ -128,6 +128,18 @@ void testCallJsFunction() { matchVar("foo", 3); } + @Test + void testCallJsFunctionFromFile() { + run( + "def nextId = call read('increment.js')", + "def res1 = nextId()", + "def res2 = nextId()" + ); + matchVar("res1", 1); + matchVar("res2", 2); + matchVar("_curId", 2); + } + @Test void testCallKarateFeature() { run( @@ -625,14 +637,14 @@ void testJavaInteropBase64() { "def res = Base64.encoder.encodeToString('hello'.getBytes())" ); matchVar("res", java.util.Base64.getEncoder().encodeToString("hello".getBytes())); - } - + } + @Test void testTypeConversionCsvEmpty() { run( "csv temp = ''" ); - matchVar("temp", "[]"); - } + matchVar("temp", "[]"); + } } diff --git a/karate-core/src/test/java/com/intuit/karate/runtime/increment.js b/karate-core/src/test/java/com/intuit/karate/runtime/increment.js new file mode 100644 index 000000000..5f41a94a6 --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/runtime/increment.js @@ -0,0 +1,9 @@ +function fn() { + karate.set('_curId', 0); + return function() { + var curId = karate.get('_curId'); + var nextId = curId + 1; + karate.set('_curId', nextId); + return nextId; + } +}