diff --git a/README.md b/README.md index 1e096b87d..89f3b7062 100755 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ And you don't need to create Java objects (or POJO-s) for any of the payloads th # Features * Scripts are plain-text files and require no compilation step or IDE * Java knowledge is not required to write tests -* Syntax 'natively' supports JSON and XML - including [JsonPath](https://github.com/jayway/JsonPath) and XPath expressions +* Syntax 'natively' supports JSON and XML - including [JsonPath](https://github.com/jayway/JsonPath) and [XPath](https://www.w3.org/TR/xpath/) expressions * Embedded JavaScript engine that enables you to build a library of re-usable functions that suit your specific environment * Re-use of payload-data and user-defined functions across tests is so easy - that it becomes natural for the test-developer * Built-in support for switching configuration across different environments (e.g. dev, QA, pre-prod) @@ -147,7 +147,7 @@ When method get Then status 200 # variant of the 'match' syntax to compare file contents -And match response * == read('test.pdf') +And match response == read('test.pdf') ``` # Getting Started @@ -1139,7 +1139,7 @@ Given def cat = ] } """ -# normal 'equality' match. note the wildcard '*' in the JSONPath (returns an array) +# normal 'equality' match. note the wildcard '*' in the JsonPath (returns an array) Then match cat.rivals[*].id == [23, 42] # when inspecting a json array, 'contains' just checks if the expected items exist @@ -1163,6 +1163,20 @@ When you use Karate, all your data assertions can be done in pure JSON and witho forest of companion Java objects. And when you [`read`](#read) your JSON objects from (re-usable) files, even complex response payload assertions can be accomplished in just a single line of Karate-script. +#### `match contains only` +For those cases where you need to assert that **all** array elements are present but in **any order** +you can do this: + +```cucumber +* def data = { foo: [1, 2, 3] } +* match data.foo contains [1] +* match data.foo contains [3, 2] +* match data.foo contains only [3, 2, 1] +* match data.foo contains only [2, 3, 1] +# this will fail +# * match data.foo contains only [2, 3] +``` + ## Validate every element in a JSON array ### `match each` Karate has syntax sugar that can iterate over all elements in a JSON array. Here's how it works: @@ -1378,7 +1392,7 @@ special object in a variable named: `karate`. This provides the following metho * `url`: URL of the HTTP call to be made * `method`: HTTP method, can be lower-case * `body`: JSON payload -* `karate.set(key, value)` - set the value of a variable immediately, which ensures that any active [`headers`](#headers) routine does the right thing for future HTTP calls (even those made by this function) +* `karate.set(key, value)` - set the value of a variable immediately, which ensures that any active [`headers`](#headers) routine does the right thing for future HTTP calls (even those made by this function being `call`-ed) * `karate.get(key)` - get the value of a variable by name, if not found - this returns `null` which is easier to handle in JavaScript (than `undefined`) * `karate.log(... args)` - log to the same logger being used by the parent process * `karate.env` - gets the value (read-only) of the environment setting 'karate.env' used for bootstrapping [configuration](#configuration) @@ -1434,7 +1448,7 @@ function(creds) { } ``` And here's how it works in a test-script. Note that you need to do this only once within a `Scenario:`, -perhaps at the beginning. +perhaps at the beginning, or within the `Background:` section. ```cucumber * header Authorization = call read('basic-auth.js') { username: 'john', password: 'secret' } diff --git a/karate-core/src/main/java/com/intuit/karate/MatchType.java b/karate-core/src/main/java/com/intuit/karate/MatchType.java index 97f0f58fa..e90315138 100644 --- a/karate-core/src/main/java/com/intuit/karate/MatchType.java +++ b/karate-core/src/main/java/com/intuit/karate/MatchType.java @@ -8,6 +8,7 @@ public enum MatchType { EQUALS, CONTAINS, + CONTAINS_ONLY, EACH_EQUALS, EACH_CONTAINS 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 1e2f2ba6c..50d2b0e7b 100755 --- a/karate-core/src/main/java/com/intuit/karate/Script.java +++ b/karate-core/src/main/java/com/intuit/karate/Script.java @@ -422,12 +422,19 @@ public static AssertionResult matchStringOrPattern(MatchType matchType, ScriptVa } } else { String actual = actValue.getAsString(); - if (matchType == MatchType.CONTAINS) { - if (!actual.contains(expected)) { - return matchFailed(path, actual, expected + " (not a sub-string)"); - } - } else if (!expected.equals(actual)) { - return matchFailed(path, actual, expected); + switch (matchType) { + case CONTAINS: + if (!actual.contains(expected)) { + return matchFailed(path, actual, expected + " (not a sub-string)"); + } + break; + case EQUALS: + if (!expected.equals(actual)) { + return matchFailed(path, actual, expected); + } + break; + default: + throw new RuntimeException("unsupported match type for string: " + matchType); } } return AssertionResult.PASS; @@ -455,6 +462,14 @@ public static AssertionResult matchXmlPath(MatchType matchType, ScriptValue actu } return matchNestedObject('/', path, matchType, actObject, expObject, context); } + + private static MatchType getInnerMatchType(MatchType outerMatchType) { + switch (outerMatchType) { + case EACH_CONTAINS: return MatchType.CONTAINS; + case EACH_EQUALS: return MatchType.EQUALS; + default: throw new RuntimeException("unexpected outer match type: " + outerMatchType); + } + } public static AssertionResult matchJsonPath(MatchType matchType, ScriptValue actual, String path, String expression, ScriptContext context) { DocumentContext actualDoc; @@ -492,14 +507,15 @@ public static AssertionResult matchJsonPath(MatchType matchType, ScriptValue act expObject = expected.getValue(); } switch (matchType) { + case CONTAINS_ONLY: case CONTAINS: case EQUALS: return matchNestedObject('.', path, matchType, actObject, expObject, context); - case EACH_EQUALS: case EACH_CONTAINS: + case EACH_EQUALS: if (actObject instanceof List) { List actList = (List) actObject; - MatchType listMatchType = matchType == MatchType.EACH_CONTAINS ? MatchType.CONTAINS : MatchType.EQUALS; + MatchType listMatchType = getInnerMatchType(matchType); int actSize = actList.size(); for (int i = 0; i < actSize; i++) { Object actListObject = actList.get(i); @@ -511,7 +527,7 @@ public static AssertionResult matchJsonPath(MatchType matchType, ScriptValue act } return AssertionResult.PASS; } else { - throw new RuntimeException("'match all' failed, not a json array: + " + actual + ", path: " + path); + throw new RuntimeException("'match each' failed, not a json array: + " + actual + ", path: " + path); } default: // dead code @@ -553,7 +569,7 @@ public static AssertionResult matchNestedObject(char delimiter, String path, Mat if (matchType != MatchType.CONTAINS && actMap.size() > expMap.size()) { // > is because of the chance of #ignore return matchFailed(path, actObject, expObject); } - for (Map.Entry expEntry : expMap.entrySet()) { + for (Map.Entry expEntry : expMap.entrySet()) { // TDDO should we assert order, maybe XML needs this ? String key = expEntry.getKey(); String childPath = path + delimiter + key; AssertionResult ar = matchNestedObject(delimiter, childPath, MatchType.EQUALS, actMap.get(key), expEntry.getValue(), context); @@ -570,7 +586,7 @@ public static AssertionResult matchNestedObject(char delimiter, String path, Mat if (matchType != MatchType.CONTAINS && actCount != expCount) { return matchFailed(path, actObject, expObject); } - if (matchType == MatchType.CONTAINS) { // just checks for existence + if (matchType == MatchType.CONTAINS || matchType == MatchType.CONTAINS_ONLY) { // just checks for existence for (Object expListObject : expList) { // for each expected item in the list boolean found = false; for (int i = 0; i < actCount; i++) { @@ -641,14 +657,14 @@ public static void setValueByPath(String name, String path, String exp, ScriptCo } else if (isXmlPath(path)) { Document doc = context.vars.get(name, Document.class); ScriptValue sv = preEval(exp, context); - switch(sv.getType()) { + switch (sv.getType()) { case XML: Node node = sv.getValue(Node.class); XmlUtils.setByPath(doc, path, node); break; default: XmlUtils.setByPath(doc, path, sv.getAsString()); - } + } } else { throw new RuntimeException("unexpected path: " + path); } diff --git a/karate-core/src/main/java/com/intuit/karate/SslUtils.java b/karate-core/src/main/java/com/intuit/karate/SslUtils.java index 81b174096..0a0d9e29c 100644 --- a/karate-core/src/main/java/com/intuit/karate/SslUtils.java +++ b/karate-core/src/main/java/com/intuit/karate/SslUtils.java @@ -5,7 +5,6 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.security.cert.CertificateException; -import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 9d397675a..0f3060eb6 100755 --- a/karate-core/src/main/java/com/intuit/karate/StepDefs.java +++ b/karate-core/src/main/java/com/intuit/karate/StepDefs.java @@ -19,7 +19,6 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; -import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.media.multipart.BodyPart; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.MultiPart; @@ -396,10 +395,20 @@ public void status(int status) { assertEquals(status, response.getStatus()); } - private static MatchType toMatchType(String each, boolean contains) { - return each == null - ? contains ? MatchType.CONTAINS : MatchType.EQUALS - : contains ? MatchType.EACH_CONTAINS : MatchType.EACH_EQUALS; + private static MatchType toMatchType(String each, String only, boolean contains) { + if (each == null) { + if (contains) { + return only == null ? MatchType.CONTAINS : MatchType.CONTAINS_ONLY; + } else { + return MatchType.EQUALS; + } + } else { + if (contains) { + return MatchType.EACH_CONTAINS; + } else { + return MatchType.EACH_EQUALS; + } + } } @Then("^match (each )?([^\\s]+)( .+)? ==$") @@ -407,21 +416,21 @@ public void matchEqualsDocString(String each, String name, String path, String e matchEquals(each, name, path, expected); } - @Then("^match (each )?([^\\s]+)( .+)? contains$") - public void matchContainsDocString(String each, String name, String path, String expected) { - matchContains(each, name, path, expected); + @Then("^match (each )?([^\\s]+)( .+)? contains( only)?$") + public void matchContainsDocString(String each, String name, String path, String only, String expected) { + matchContains(each, name, path, only, expected); } @Then("^match (each )?([^\\s]+)( .+)? == (.+)") public void matchEquals(String each, String name, String path, String expected) { - MatchType mt = toMatchType(each, false); + MatchType mt = toMatchType(each, null, false); matchNamed(mt, name, path, expected); } - @Then("^match (each )?([^\\s]+)( .+)? contains (.+)") - public void matchContains(String each, String name, String path, String expected) { - MatchType mt = toMatchType(each, true); + @Then("^match (each )?([^\\s]+)( .+)? contains( only)?(.+)") + public void matchContains(String each, String name, String path, String only, String expected) { + MatchType mt = toMatchType(each, only, true); matchNamed(mt, name, path, expected); } diff --git a/karate-core/src/main/java/com/intuit/karate/XmlUtils.java b/karate-core/src/main/java/com/intuit/karate/XmlUtils.java index e48ecc3ca..cccb78e0c 100755 --- a/karate-core/src/main/java/com/intuit/karate/XmlUtils.java +++ b/karate-core/src/main/java/com/intuit/karate/XmlUtils.java @@ -23,7 +23,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; -import org.w3c.dom.DocumentFragment; import org.w3c.dom.Node; /** 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 94791358c..d51f6155f 100755 --- a/karate-core/src/test/java/com/intuit/karate/ScriptTest.java +++ b/karate-core/src/test/java/com/intuit/karate/ScriptTest.java @@ -266,6 +266,11 @@ public void testMatchAllJsonPath() { ctx.vars.put("myJson", doc); ScriptValue myJson = ctx.vars.get("myJson"); assertTrue(Script.matchJsonPath(MatchType.EQUALS, myJson, "$.foo", "[{bar: 1, baz: 'a'}, {bar: 2, baz: 'b'}, {bar:3, baz: 'c'}]", ctx).pass); + assertTrue(Script.matchJsonPath(MatchType.CONTAINS, myJson, "$.foo", "[{bar: 1, baz: 'a'}, {bar: 2, baz: 'b'}, {bar:3, baz: 'c'}]", ctx).pass); + assertTrue(Script.matchJsonPath(MatchType.CONTAINS_ONLY, myJson, "$.foo", "[{bar: 1, baz: 'a'}, {bar: 2, baz: 'b'}, {bar:3, baz: 'c'}]", ctx).pass); + // shuffle + assertTrue(Script.matchJsonPath(MatchType.CONTAINS_ONLY, myJson, "$.foo", "[{bar: 2, baz: 'b'}, {bar:3, baz: 'c'}, {bar: 1, baz: 'a'}]", ctx).pass); + assertFalse(Script.matchJsonPath(MatchType.CONTAINS_ONLY, myJson, "$.foo", "[{bar: 1, baz: 'a'}, {bar: 2, baz: 'b'}]", ctx).pass); assertTrue(Script.matchJsonPath(MatchType.EACH_EQUALS, myJson, "$.foo", "{bar:'#number', baz:'#string'}", ctx).pass); assertTrue(Script.matchJsonPath(MatchType.EACH_CONTAINS, myJson, "$.foo", "{bar:'#number'}", ctx).pass); assertTrue(Script.matchJsonPath(MatchType.EACH_CONTAINS, myJson, "$.foo", "{baz:'#string'}", ctx).pass); diff --git a/karate-core/src/test/java/com/intuit/karate/syntax/syntax.feature b/karate-core/src/test/java/com/intuit/karate/syntax/syntax.feature index bbf71df75..c73ce507b 100755 --- a/karate-core/src/test/java/com/intuit/karate/syntax/syntax.feature +++ b/karate-core/src/test/java/com/intuit/karate/syntax/syntax.feature @@ -296,6 +296,14 @@ Then match pdf == read('test.pdf') * match foo contains { bar:1, baz: 'hello' } # * match foo == { bar:1, baz: 'hello' } +# match contains only +* def data = { foo: [1, 2, 3] } +* match data.foo contains [1] +* match data.foo contains [3, 2] +* match data.foo contains only [3, 2, 1] +* match data.foo contains only [2, 3, 1] +# * match data.foo contains only [2, 3] + # match each * def data = { foo: [{ bar: 1, baz: 'a' }, { bar: 2, baz: 'b' }, { bar: 3, baz: 'c' }]} * match each data.foo == { bar: '#number', baz: '#string' }