diff --git a/README.md b/README.md index 7635c7f53..6d1841207 100755 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ This is all that you need within your ``: com.intuit.karate karate-junit4 - 0.2.5 + 0.2.6 test ``` @@ -193,7 +193,7 @@ You can replace the values of 'com.mycompany' and 'myproject' as per your needs. mvn archetype:generate \ -DarchetypeGroupId=com.intuit.karate \ -DarchetypeArtifactId=karate-archetype \ --DarchetypeVersion=0.2.5 \ +-DarchetypeVersion=0.2.6 \ -DgroupId=com.mycompany \ -DartifactId=myproject ``` @@ -1493,7 +1493,7 @@ function() { ip_header: '123.45.67.89', }; var authString = ''; - var authToken = karate.get('authToken'); // use the 'karate' helper to do a 'safe' get of a variable + var authToken = karate.get('authToken'); // use the 'karate' helper to do a 'safe' get of a 'dynamic' variable if (authToken) { // and if 'authToken' is not null ... authString = ',auth_type=MyAuthScheme' + ',auth_key=' + authToken.key diff --git a/karate-archetype/pom.xml b/karate-archetype/pom.xml index d2591cfc6..0b1c64c56 100755 --- a/karate-archetype/pom.xml +++ b/karate-archetype/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.2.5 + 0.2.6 karate-archetype jar diff --git a/karate-archetype/src/main/resources/archetype-resources/pom.xml b/karate-archetype/src/main/resources/archetype-resources/pom.xml index 8dba08c9b..fa3aa4dd0 100755 --- a/karate-archetype/src/main/resources/archetype-resources/pom.xml +++ b/karate-archetype/src/main/resources/archetype-resources/pom.xml @@ -17,7 +17,7 @@ com.intuit.karate karate-junit4 - 0.2.5 + 0.2.6 test diff --git a/karate-core/pom.xml b/karate-core/pom.xml index b9ac084fb..7d2c2e95f 100755 --- a/karate-core/pom.xml +++ b/karate-core/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.2.5 + 0.2.6 karate-core jar diff --git a/karate-core/src/main/java/com/intuit/karate/RequestFilter.java b/karate-core/src/main/java/com/intuit/karate/RequestFilter.java index a5d9ab13b..812d8f3ff 100755 --- a/karate-core/src/main/java/com/intuit/karate/RequestFilter.java +++ b/karate-core/src/main/java/com/intuit/karate/RequestFilter.java @@ -16,16 +16,11 @@ */ public class RequestFilter implements ClientRequestFilter { - private static final Logger logger = LoggerFactory.getLogger(RequestFilter.class); - - private final ScriptContext context; - - public RequestFilter(ScriptContext context) { - this.context = context; - } + private static final Logger logger = LoggerFactory.getLogger(RequestFilter.class); @Override - public void filter(ClientRequestContext ctx) throws IOException { + public void filter(ClientRequestContext ctx) throws IOException { + ScriptContext context = (ScriptContext) ctx.getProperty(ScriptContext.KARATE_DOT_CONTEXT); ScriptValue headersFunction = context.headers; if (headersFunction.getType() != JS_FUNCTION) { logger.trace("configured 'headers' is not a js function: {}", headersFunction); diff --git a/karate-core/src/main/java/com/intuit/karate/Script.java b/karate-core/src/main/java/com/intuit/karate/Script.java index 2ad8235c8..d2e3e903b 100755 --- a/karate-core/src/main/java/com/intuit/karate/Script.java +++ b/karate-core/src/main/java/com/intuit/karate/Script.java @@ -266,6 +266,7 @@ public static ScriptValue evalInNashorn(String exp, ScriptContext context, Scrip for (Map.Entry entry : map.entrySet()) { bindings.put(entry.getKey(), entry.getValue()); } + bindings.put(ScriptContext.KARATE_NAME, new ScriptBridge(context)); } if (selfValue != null) { bindings.put(VAR_SELF, selfValue.getValue()); @@ -562,10 +563,15 @@ public static AssertionResult matchJsonPath(MatchType matchType, ScriptValue act case JSON: actualDoc = actual.getValue(DocumentContext.class); break; + case JS_ARRAY: // happens for json resulting from nashorn + ScriptObjectMirror som = actual.getValue(ScriptObjectMirror.class); + actualDoc = JsonPath.parse(som.values()); + break; + case JS_OBJECT: // is a map-like object, happens for json resulting from nashorn case MAP: // this happens because some jsonpath operations result in Map Map map = actual.getValue(Map.class); actualDoc = JsonPath.parse(map); - break; + break; case LIST: // this also happens because some jsonpath operations result in List List list = actual.getValue(List.class); actualDoc = JsonPath.parse(list); @@ -583,6 +589,8 @@ public static AssertionResult matchJsonPath(MatchType matchType, ScriptValue act } else { return matchStringOrPattern('.', path, matchType, null, actual, expected.getValue(String.class), context); } + case PRIMITIVE: + return matchPrimitive(path, actual.getValue(), eval(expression, context).getValue()); default: throw new RuntimeException("not json, cannot do json path for value: " + actual + ", path: " + path); } @@ -702,29 +710,33 @@ public static AssertionResult matchNestedObject(char delimiter, String path, Mat return AssertionResult.PASS; // lists (and order) are identical } } else if (ClassUtils.isPrimitiveOrWrapper(expObject.getClass())) { - if (actObject == null) { - return matchFailed(path, actObject, expObject, "actual value is null"); - } - if (!expObject.getClass().equals(actObject.getClass())) { - // types are not the same, use the JS engine for a lenient equality check - String exp = actObject + " == " + expObject; - ScriptValue sv = evalInNashorn(exp, null); - if (sv.isBooleanTrue()) { - return AssertionResult.PASS; - } else { - return matchFailed(path, actObject, expObject, "not equal"); - } - } - if (!expObject.equals(actObject)) { - return matchFailed(path, actObject, expObject, "not equal"); - } else { - return AssertionResult.PASS; // primitives, are equal - } + return matchPrimitive(path, actObject, expObject); } else { // this should never happen throw new RuntimeException("unexpected type: " + expObject.getClass()); } } + private static AssertionResult matchPrimitive(String path, Object actObject, Object expObject) { + if (actObject == null) { + return matchFailed(path, actObject, expObject, "actual value is null"); + } + if (!expObject.getClass().equals(actObject.getClass())) { + // types are not the same, use the JS engine for a lenient equality check + String exp = actObject + " == " + expObject; + ScriptValue sv = evalInNashorn(exp, null); + if (sv.isBooleanTrue()) { + return AssertionResult.PASS; + } else { + return matchFailed(path, actObject, expObject, "not equal"); + } + } + if (!expObject.equals(actObject)) { + return matchFailed(path, actObject, expObject, "not equal"); + } else { + return AssertionResult.PASS; // primitives, are equal + } + } + public static void setValueByPath(String name, String path, String exp, ScriptContext context) { name = StringUtils.trim(name); path = StringUtils.trimToNull(path); @@ -810,9 +822,10 @@ public static ScriptValue call(String name, String argString, ScriptContext cont } public static ScriptValue evalFunctionCall(ScriptObjectMirror som, Object callArg, ScriptContext context) { - for (Map.Entry entry : context.getVariableBindings().entrySet()) { - som.put(entry.getKey(), entry.getValue()); - } + // ensure that things like 'karate.get' operate on the latest variable state + som.setMember(ScriptContext.KARATE_NAME, new ScriptBridge(context)); + // convenience for users, can use 'karate' instead of 'this.karate' + som.eval(String.format("var %s = this.%s", ScriptContext.KARATE_NAME, ScriptContext.KARATE_NAME)); Object result; if (callArg != null) { result = som.call(som, callArg); diff --git a/karate-core/src/main/java/com/intuit/karate/ScriptBridge.java b/karate-core/src/main/java/com/intuit/karate/ScriptBridge.java index 17cc71f5b..25c901e90 100755 --- a/karate-core/src/main/java/com/intuit/karate/ScriptBridge.java +++ b/karate-core/src/main/java/com/intuit/karate/ScriptBridge.java @@ -19,6 +19,10 @@ public class ScriptBridge { public ScriptBridge(ScriptContext context) { this.context = context; } + + public ScriptContext getContext() { + return context; + } public Object read(String fileName) { ScriptValue sv = FileUtils.readFile(fileName, context); @@ -30,7 +34,13 @@ public void set(String name, Object o) { } public Object get(String exp) { - ScriptValue sv = Script.eval(exp, context); // even json path expressions will work + ScriptValue sv; + try { + sv = Script.eval(exp, context); // even json path expressions will work + } catch (Exception e) { + logger.warn("karate.get failed for expression: '{}': {}", exp, e.getMessage()); + return null; + } if (sv != null) { return sv.getAfterConvertingToMapIfNeeded(); } else { diff --git a/karate-core/src/main/java/com/intuit/karate/ScriptContext.java b/karate-core/src/main/java/com/intuit/karate/ScriptContext.java index dcceb3283..8aa276e54 100755 --- a/karate-core/src/main/java/com/intuit/karate/ScriptContext.java +++ b/karate-core/src/main/java/com/intuit/karate/ScriptContext.java @@ -22,8 +22,8 @@ public class ScriptContext { private static final Logger logger = LoggerFactory.getLogger(ScriptContext.class); - private static final String KARATE_NAME = "karate"; - private static final String VAR_CONTEXT = "_context"; + public static final String KARATE_DOT_CONTEXT = "karate.context"; + public static final String KARATE_NAME = "karate"; private static final String VAR_READ = "read"; protected final ScriptValueMap vars; @@ -89,7 +89,7 @@ public ScriptContext(ScriptEnv env, ScriptContext parent, Map ar private static String getFileReaderFunction() { return "function(path) {\n" + " var FileUtils = Java.type('" + FileUtils.class.getCanonicalName() + "');\n" - + " return FileUtils.readFile(path, " + VAR_CONTEXT + ").value;\n" + + " return FileUtils.readFile(path, " + KARATE_DOT_CONTEXT + ").value;\n" + "}"; } @@ -137,7 +137,7 @@ public void buildClient() { Level.SEVERE, LoggingFeature.Verbosity.PAYLOAD_TEXT, null)); } - clientBuilder.register(new RequestFilter(this)); + clientBuilder.register(new RequestFilter()); if (sslEnabled) { logger.info("ssl enabled, initializing generic trusted certificate / key-store with algorithm: {}", sslAlgorithm); SSLContext ssl = SslUtils.getSslContext(sslAlgorithm); @@ -167,10 +167,7 @@ public Map getVariableBindings() { Map map = Script.simplify(vars); if (readFunction != null) { map.put(VAR_READ, readFunction.getValue()); - } - // for future function calls if needed, see getFileReaderFunction() - map.put(VAR_CONTEXT, this); - map.put(KARATE_NAME, new ScriptBridge(this)); + } return map; } diff --git a/karate-core/src/main/java/com/intuit/karate/StepDefs.java b/karate-core/src/main/java/com/intuit/karate/StepDefs.java index 1f5639f43..171710662 100755 --- a/karate-core/src/main/java/com/intuit/karate/StepDefs.java +++ b/karate-core/src/main/java/com/intuit/karate/StepDefs.java @@ -200,6 +200,7 @@ public void asssertBoolean(String expression) { private Invocation.Builder prepare() { hasUrlBeenSet(); Invocation.Builder builder = target.request(); + builder.property(ScriptContext.KARATE_DOT_CONTEXT, context); if (headers != null) { for (Map.Entry entry : headers.entrySet()) { builder = builder.header(entry.getKey(), entry.getValue()); @@ -351,7 +352,9 @@ public void soapAction(String action) { xml = request.getAsString(); } startTimer(); - response = target.request().header("SOAPAction", action).method("POST", Entity.entity(xml, MediaType.TEXT_XML)); + Invocation.Builder builder = target.request(); + builder.property(ScriptContext.KARATE_DOT_CONTEXT, context); + response = builder.header("SOAPAction", action).method("POST", Entity.entity(xml, MediaType.TEXT_XML)); stopTimer(); String rawResponse = response.readEntity(String.class); try { diff --git a/karate-core/src/test/java/com/intuit/karate/ScriptTest.java b/karate-core/src/test/java/com/intuit/karate/ScriptTest.java index c3ae78059..b8b436cc0 100755 --- a/karate-core/src/test/java/com/intuit/karate/ScriptTest.java +++ b/karate-core/src/test/java/com/intuit/karate/ScriptTest.java @@ -699,6 +699,36 @@ public void testFromJsKarateCallFeatureWithJsonArg() { assertTrue(Script.matchNamed(MatchType.EQUALS, "res.a", null, "1", ctx).pass); assertTrue(Script.matchNamed(MatchType.EQUALS, "res.b", null, "2", ctx).pass); assertTrue(Script.matchNamed(MatchType.EQUALS, "res.c", null, "3", ctx).pass); + } + + @Test + public void testFromJsKarateGetForNonExistentVariable() { + ScriptContext ctx = getContext(); + Script.assign("fun", "function(){ var foo = karate.get('foo'); return foo ? true : false }", ctx); + Script.assign("res", "fun()", ctx); + assertTrue(Script.matchNamed(MatchType.EQUALS, "res", null, "false", ctx).pass); + } + + @Test + public void testFromJsKarateGetForJsonArrayVariable() { + ScriptContext ctx = getContext(); + Script.assign("fun", "function(){ return [1, 2, 3] }", ctx); + Script.assign("res", "call fun", ctx); + assertTrue(Script.matchNamed(MatchType.EQUALS, "res", null, "[1, 2, 3]", ctx).pass); } + + @Test + public void testFromJsKarateGetForJsonObjectVariableAndCallFeatureAndJs() { + ScriptContext ctx = getContext(); + Script.assign("fun", "read('headers.js')", ctx); + Script.assign("res", "call fun", ctx); + assertTrue(Script.matchNamed(MatchType.EQUALS, "res", null, "{ foo: 'bar_someValue' }", ctx).pass); + Script.assign("signin", "call read('signin.feature')", ctx); + Script.assign("ticket", "signin.ticket", ctx); + assertTrue(Script.matchNamed(MatchType.EQUALS, "ticket", null, "{ foo: 'bar' }", ctx).pass); + Script.assign("res", "call fun", ctx); + assertTrue(Script.matchNamed(MatchType.EQUALS, "res", null, "{ foo: 'bar_someValue', baz: 'ban' }", ctx).pass); + } + } diff --git a/karate-core/src/test/java/com/intuit/karate/headers.js b/karate-core/src/test/java/com/intuit/karate/headers.js new file mode 100644 index 000000000..7c5842ab8 --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/headers.js @@ -0,0 +1,10 @@ +function() { + var out = { + foo: 'bar_' + someConfig // someConfig is 'start-up' config, will never change + }; + var ticket = karate.get('ticket'); // get can get the 'latest' values of variables + if (ticket) { + out.baz = 'ban' + } + return out; +} diff --git a/karate-core/src/test/java/com/intuit/karate/signin.feature b/karate-core/src/test/java/com/intuit/karate/signin.feature new file mode 100644 index 000000000..3155b4537 --- /dev/null +++ b/karate-core/src/test/java/com/intuit/karate/signin.feature @@ -0,0 +1,6 @@ +Feature: + +Scenario: +* def iamTicket = { ticket: { foo: 'bar' } } +* def ticket = iamTicket.ticket + diff --git a/karate-demo/README.md b/karate-demo/README.md index 663b8db78..d8814020f 100644 --- a/karate-demo/README.md +++ b/karate-demo/README.md @@ -5,6 +5,7 @@ as well as demonstrate various Karate features and best-practices. | Example | Demonstrates ----------| -------- [`greeting.feature`](src/test/java/demo/greeting/greeting.feature) | Simple GET requests and multiple scenarios in a test. +[`headers.feature`](src/test/java/demo/headers/headers.feature) | A simple example of header management using a js file ([`classpath:headers.js`](src/test/java/headers.js)), and also shows how cookies can be accessed and how path and query parameters can be specified. [`cats.feature`](src/test/java/demo/cats/cats.feature) | Great example of [embedded-expressions](https://github.com/intuit/karate#embedded-expressions) (or JSON / XML templating). Also shows how to set the `Accept` [header](https://github.com/intuit/karate#header) for getting XML from the server. [`kittens.feature`](src/test/java/demo/cats/kittens.feature) | Reading a complex payload expected response [from a file](https://github.com/intuit/karate#reading-files). You can do the same for request payloads as well. Observe how [JSON templating](https://github.com/intuit/karate#embedded-expressions) makes creating dynamic JSON super-easy, look at [line #24](src/test/java/demo/cats/kittens.feature#L24) for example. [`upload.feature`](src/test/java/demo/upload/upload.feature) | [Multi-part](https://github.com/intuit/karate#multipart-field) file-upload example, as well as comparing the binary content of a download. Also shows how to assert for expected response [headers](https://github.com/intuit/karate#match-header). diff --git a/karate-demo/pom.xml b/karate-demo/pom.xml index 8b9e2fb24..6aabe1900 100755 --- a/karate-demo/pom.xml +++ b/karate-demo/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.2.5 + 0.2.6 karate-demo diff --git a/karate-demo/src/main/java/com/intuit/karate/demo/controller/HeadersController.java b/karate-demo/src/main/java/com/intuit/karate/demo/controller/HeadersController.java new file mode 100644 index 000000000..fb7cabc6d --- /dev/null +++ b/karate-demo/src/main/java/com/intuit/karate/demo/controller/HeadersController.java @@ -0,0 +1,71 @@ +/* + * The MIT License + * + * Copyright 2017 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.demo.controller; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * + * @author pthomas3 + */ +@RestController +@RequestMapping("/headers") +public class HeadersController { + + private Map tokens = new HashMap<>(); + + @GetMapping + public ResponseEntity getToken(HttpServletResponse response) { + String token = UUID.randomUUID().toString(); + String time = System.currentTimeMillis() + ""; + tokens.put(token, time); + response.addCookie(new Cookie("time", time)); + return ResponseEntity.ok().body(token); + } + + @GetMapping("/{token:.+}") + public ResponseEntity validateToken(@CookieValue("time") String time, + @RequestHeader("Authorization") String authorization, @PathVariable String token, + @RequestParam String url) { + String temp = tokens.get(token); + if (temp.equals(time) && authorization.equals(token + time + url)) { + return ResponseEntity.ok().build(); + } else { + return ResponseEntity.badRequest().build(); + } + } + +} diff --git a/karate-demo/src/test/java/demo/headers/HeadersRunner.java b/karate-demo/src/test/java/demo/headers/HeadersRunner.java new file mode 100644 index 000000000..6d5c283f3 --- /dev/null +++ b/karate-demo/src/test/java/demo/headers/HeadersRunner.java @@ -0,0 +1,11 @@ +package demo.headers; + +import demo.TestBase; + +/** + * + * @author pthomas3 + */ +public class HeadersRunner extends TestBase { + +} diff --git a/karate-demo/src/test/java/demo/headers/headers.feature b/karate-demo/src/test/java/demo/headers/headers.feature new file mode 100644 index 000000000..c56176390 --- /dev/null +++ b/karate-demo/src/test/java/demo/headers/headers.feature @@ -0,0 +1,22 @@ +Feature: greeting end-point + +Background: +* configure headers = read('classpath:headers.js') +* url demoBaseUrl + +Scenario: get auth token and cookie + + Given path 'headers' + When method get + Then status 200 + # the next two variables are used by 'headers.js' + And def token = response + And def time = cookies['time'] + + # there is a cookie sent automatically + # and an 'Authorization' header set by 'headers.js' + Given path 'headers', token + And param url = demoBaseUrl + When method get + Then status 200 + diff --git a/karate-demo/src/test/java/headers.js b/karate-demo/src/test/java/headers.js new file mode 100644 index 000000000..cb4ef4f51 --- /dev/null +++ b/karate-demo/src/test/java/headers.js @@ -0,0 +1,10 @@ +function() { + var token = karate.get('token'); + var time = karate.get('time'); + if (token && time) { + // demoBaseUrl is like a constant, since it was injected at the time of configuration / init + return { Authorization: token + time + demoBaseUrl }; + } else { + return {}; + } +} diff --git a/karate-junit4/pom.xml b/karate-junit4/pom.xml index 6a7eb23ec..24d8c1258 100755 --- a/karate-junit4/pom.xml +++ b/karate-junit4/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.2.5 + 0.2.6 karate-junit4 jar diff --git a/karate-testng/pom.xml b/karate-testng/pom.xml index b24d83dd6..d186478f4 100755 --- a/karate-testng/pom.xml +++ b/karate-testng/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 0.2.5 + 0.2.6 karate-testng jar diff --git a/pom.xml b/pom.xml index dc9fc4857..5e1ecf481 100755 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.intuit.karate karate-parent - 0.2.5 + 0.2.6 pom ${project.artifactId}