Skip to content

Commit

Permalink
possible breaking change, implemented #notpresent ref #270
Browse files Browse the repository at this point in the history
the behavior of match for #null and even the 'null' value has changed, the key is expected to be present in both cases
so upgrading may need to use #ignore instead of #null in some rare cases
  • Loading branch information
ptrthomas committed Dec 22, 2017
1 parent 9b85eb6 commit 5f6fc08
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 7 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1720,6 +1720,7 @@ Marker | Description
`#ignore` | Skip comparison for this field
`#null` | Expects actual value to be null
`#notnull` | Expects actual value to be not-null
`#notpresent` | Expects the JSON key to be not present
`#array` | Expects actual value to be a JSON array
`#object` | Expects actual value to be a JSON object
`#boolean` | Expects actual value to be a boolean `true` or `false`
Expand Down
37 changes: 30 additions & 7 deletions karate-core/src/main/java/com/intuit/karate/Script.java
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ private static ScriptValue evalKarateExpression(String text, ScriptContext conte
return evalXmlPathOnVarByName(ScriptValueMap.VAR_RESPONSE, text, context);
} else if (isStringExpression(text)) { // has to be above variableAndXml/JsonPath because of / in URL-s etc
return evalJsExpression(text, context);
// remove after 0.7.0 release feedback
// remove after 0.7.0 release feedback
// } else if (isVariableAndJsonPath(text)) {
// StringUtils.Pair pair = parseVariableAndPath(text);
// return evalJsonPathOnVarByName(pair.left, pair.right, context);
Expand Down Expand Up @@ -541,10 +541,10 @@ public static void evalXmlEmbeddedExpressions(Node node, ScriptContext context,
public static ScriptValue copy(String name, String exp, ScriptContext context, boolean validateName) {
return assign(AssignType.COPY, name, exp, context, validateName);
}

public static ScriptValue assign(String name, String exp, ScriptContext context) {
return assign(AssignType.AUTO, name, exp, context, true);
}
}

public static ScriptValue assign(String name, String exp, ScriptContext context, boolean validateName) {
return assign(AssignType.AUTO, name, exp, context, validateName);
Expand Down Expand Up @@ -1122,7 +1122,7 @@ private static Object quoteIfString(Object o) {
return o;
}
}

private static boolean isNegation(MatchType type) {
switch (type) {
case EACH_NOT_CONTAINS:
Expand All @@ -1135,15 +1135,15 @@ private static boolean isNegation(MatchType type) {
}
}

public static AssertionResult matchFailed(MatchType matchType, String path,
public static AssertionResult matchFailed(MatchType matchType, String path,
Object actObject, Object expObject, String reason) {
if (path.startsWith("/")) {
String leafName = getLeafNameFromXmlPath(path);
actObject = toXmlString(leafName, actObject);
expObject = toXmlString(leafName, expObject);
path = path.replace("/@/", "/@");
}
String message = String.format("path: %s, actual: %s, %sexpected: %s, reason: %s",
String message = String.format("path: %s, actual: %s, %sexpected: %s, reason: %s",
path, quoteIfString(actObject), isNegation(matchType) ? "NOT " : "", quoteIfString(expObject), reason);
return AssertionResult.fail(message);
}
Expand Down Expand Up @@ -1197,9 +1197,32 @@ public static AssertionResult matchNestedObject(char delimiter, String path, Mat
}
for (Map.Entry<String, Object> expEntry : expMap.entrySet()) {
String key = expEntry.getKey();
Object childExp = expEntry.getValue();
String childPath = delimiter == '.' ? JsonUtils.buildPath(path, key) : path + delimiter + key;
if (!actMap.containsKey(key)) {
boolean equal = false;
if (childExp instanceof String) {
String childMacro = (String) childExp;
if (isOptionalMacro(childMacro)
|| childMacro.equals("#ignore")
|| childMacro.equals("#notpresent")) { // logical match
if (matchType == MatchType.NOT_CONTAINS) {
return matchFailed(matchType, childPath, "(not present)", childExp, "actual value contains expected");
}
equal = true;
}
}
if (!equal) {
if (matchType == MatchType.NOT_EQUALS) {
return AssertionResult.PASS; // exit early
}
if (matchType != MatchType.NOT_CONTAINS) {
return matchFailed(matchType, childPath, "(not present)", childExp, "actual value does not contain expected");
}
}
continue; // end edge case for key not present
}
Object childAct = actMap.get(key);
Object childExp = expEntry.getValue();
AssertionResult ar = matchNestedObject(delimiter, childPath, MatchType.EQUALS, actRoot, actMap, childAct, childExp, context);
if (ar.pass) { // values for this key match
if (matchType == MatchType.NOT_CONTAINS) {
Expand Down
13 changes: 13 additions & 0 deletions karate-core/src/test/java/com/intuit/karate/ScriptTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1499,6 +1499,19 @@ public void testSchemaLikeAndOptionalKeys() {
assertTrue(Script.matchNamed(MatchType.EQUALS, "json", null, "'##string'", ctx).pass);
}

@Test
public void testNotPresentMacro() {
ScriptContext ctx = getContext();
Script.assign("json", "{ }", ctx);
assertTrue(Script.matchNamed(MatchType.EQUALS, "json", null, "{ a: '#notpresent' }", ctx).pass);
assertTrue(Script.matchNamed(MatchType.CONTAINS, "json", null, "{ a: '#notpresent' }", ctx).pass);
assertFalse(Script.matchNamed(MatchType.EQUALS, "json", null, "{ a: '#null' }", ctx).pass);
assertFalse(Script.matchNamed(MatchType.EQUALS, "json", null, "{ a: null }", ctx).pass);
Script.assign("json", "{ a: 1 }", ctx);
assertTrue(Script.matchNamed(MatchType.NOT_EQUALS, "json", null, "{ a: '#notpresent' }", ctx).pass);
assertTrue(Script.matchNamed(MatchType.NOT_CONTAINS, "json", null, "{ a: '#notpresent' }", ctx).pass);
}

@Test
public void testReplace() {
ScriptContext ctx = getContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ Scenario: optional json values
* def response = [{a: 'one', b: 'two'}, { a: 'one' }]
* match each response contains { a: 'one', b: '##("two")' }

Scenario: #null and #notpresent
* def foo = { }
* match foo == { a: '#notpresent' }
* match foo == { a: '#ignore' }
* match foo != { a: '#null' }
* match foo != { a: null }

Scenario: get and json path
* def foo = { bar: { baz: 'ban' } }
* def res = get foo $..bar[?(@.baz)]
Expand Down

0 comments on commit 5f6fc08

Please sign in to comment.