diff --git a/src/manual/validation-hamcrest.adoc b/src/manual/validation-hamcrest.adoc index 48b919db02..8aa7814e77 100644 --- a/src/manual/validation-hamcrest.adoc +++ b/src/manual/validation-hamcrest.adoc @@ -29,6 +29,17 @@ receive("someEndpoint") .expression("node-set:/TestRequest/OrderType", hasSize(3)); ---- +NOTE: If you want to match text containing any of the following characters: ' , ( ) +You need to enclose the respective string in quotation marks when defining your matcher. +If you intend to match an actual single quote, it should be escaped with a backslash (\'). + +For example: +[source,xml] +---- +anyOf(equalTo('text containing a \\' (quote) and a , (comma) '), anyOf(isEmptyOrNullString())) +anyOf(equalTo('text containing a backslash and quote \\\\' and a , (comma) '), anyOf(isEmptyOrNullString())) +---- + .XML [source,xml,indent=0,role="secondary"] ---- diff --git a/validation/citrus-validation-hamcrest/src/main/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcher.java b/validation/citrus-validation-hamcrest/src/main/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcher.java index fd69b14474..4fa347f2d4 100644 --- a/validation/citrus-validation-hamcrest/src/main/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcher.java +++ b/validation/citrus-validation-hamcrest/src/main/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcher.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Optional; import java.util.Properties; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.citrusframework.context.TestContext; @@ -84,11 +85,9 @@ public void validate(String fieldName, String value, List controlParamet } String matcherName = matcherExpression.trim().substring(0, matcherExpression.trim().indexOf("(")); - String[] matcherParameter = matcherExpression.trim().substring(matcherName.length() + 1, matcherExpression.trim().length() - 1).split(","); - for (int i = 0; i < matcherParameter.length; i++) { - matcherParameter[i] = VariableUtils.cutOffSingleQuotes(matcherParameter[i].trim()); - } + String[] matcherParameter = determineNestedMatcherParameters(matcherExpression.trim() + .substring(matcherName.length() + 1, matcherExpression.trim().length() - 1)); try { Matcher matcher = getMatcher(matcherName, matcherParameter, context); @@ -125,6 +124,7 @@ public void validate(String fieldName, String value, List controlParamet * @return */ private Matcher getMatcher(String matcherName, String[] matcherParameter, TestContext context) { + try { if (context.getReferenceResolver().isResolvable(matcherName, HamcrestMatcherProvider.class) || HamcrestMatcherProvider.canResolve(matcherName)) { @@ -160,7 +160,11 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes if (matcherExpression.contains("(") && matcherExpression.contains(")")) { String nestedMatcherName = matcherExpression.trim().substring(0, matcherExpression.trim().indexOf("(")); - String[] nestedMatcherParameter = matcherExpression.trim().substring(nestedMatcherName.length() + 1, matcherExpression.trim().length() - 1).split(","); + String[] nestedMatcherParameter = matcherExpression.trim() + .substring( + nestedMatcherName.length() + 1, + matcherExpression.trim().length() - 1) + .split(","); return (Matcher) matcherMethod.invoke(null, getMatcher(nestedMatcherName, nestedMatcherParameter,context)); } @@ -174,8 +178,13 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes List> nestedMatchers = new ArrayList<>(); for (String matcherExpression : matcherParameter) { String nestedMatcherName = matcherExpression.trim().substring(0, matcherExpression.trim().indexOf("(")); - String nestedMatcherParameter = matcherExpression.trim().substring(nestedMatcherName.length() + 1, matcherExpression.trim().length() - 1); - nestedMatchers.add(getMatcher(nestedMatcherName, new String[] { nestedMatcherParameter }, context)); + String[] nestedMatcherParameters = determineNestedMatcherParameters( + matcherExpression.trim(). + substring( + nestedMatcherName.length() + 1, + matcherExpression.trim().length() - 1)); + + nestedMatchers.add(getMatcher(nestedMatcherName, nestedMatcherParameters, context)); } return (Matcher) matcherMethod.invoke(null, nestedMatchers); @@ -183,6 +192,9 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes } if (matchers.contains(matcherName)) { + + unescapeQuotes(matcherParameter); + Method matcherMethod = ReflectionUtils.findMethod(Matchers.class, matcherName, String.class); if (matcherMethod == null) { @@ -198,7 +210,9 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes Method matcherMethod = ReflectionUtils.findMethod(Matchers.class, matcherName, double.class, double.class); if (matcherMethod != null) { - return (Matcher) matcherMethod.invoke(null, Double.valueOf(matcherParameter[0]), matcherParameter.length > 1 ? Double.parseDouble(matcherParameter[1]) : 0.0D); + return (Matcher) matcherMethod.invoke( + null, + Double.valueOf(matcherParameter[0]), matcherParameter.length > 1 ? Double.parseDouble(matcherParameter[1]) : 0.0D); } matcherMethod = ReflectionUtils.findMethod(Matchers.class, matcherName, Comparable.class); @@ -209,6 +223,9 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes } if (collectionMatchers.contains(matcherName)) { + + unescapeQuotes(matcherParameter); + Method matcherMethod = ReflectionUtils.findMethod(Matchers.class, matcherName, int.class); if (matcherMethod != null) { @@ -229,6 +246,9 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes } if (mapMatchers.contains(matcherName)) { + + unescapeQuotes(matcherParameter); + Method matcherMethod = ReflectionUtils.findMethod(Matchers.class, matcherName, Object.class); if (matcherMethod != null) { @@ -243,6 +263,9 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes } if (optionMatchers.contains(matcherName)) { + + unescapeQuotes(matcherParameter); + Method matcherMethod = ReflectionUtils.findMethod(Matchers.class, matcherName, Object[].class); if (matcherMethod != null) { @@ -252,7 +275,9 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes matcherMethod = ReflectionUtils.findMethod(Matchers.class, matcherName, Collection.class); if (matcherMethod != null) { - return (Matcher) matcherMethod.invoke(null, new Object[] { getCollection(StringUtils.arrayToCommaDelimitedString(matcherParameter)) }); + return (Matcher) matcherMethod.invoke( + null, + new Object[] { getCollection(StringUtils.arrayToCommaDelimitedString(matcherParameter)) }); } } } catch (InvocationTargetException | IllegalAccessException e) { @@ -262,6 +287,18 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes throw new CitrusRuntimeException("Unsupported matcher: " + matcherName); } + /** + * Unescape the quotes in search expressions (\\' -> '). + * @param matcherParameters to unescape + */ + private static void unescapeQuotes(String[] matcherParameters) { + if (matcherParameters != null) { + for (int i=0; i< matcherParameters.length; i++) { + matcherParameters[i] = matcherParameters[i].replace("\\'","'"); + } + } + } + /** * Try to find matcher provider using different lookup strategies. Looks into reference resolver and resource path for matcher provider. * @param matcherName @@ -270,7 +307,8 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes */ private Optional lookupMatcherProvider(String matcherName, TestContext context) { // try to find matcher provider via reference - Optional matcherProvider = context.getReferenceResolver().resolveAll(HamcrestMatcherProvider.class) + Optional matcherProvider = context.getReferenceResolver() + .resolveAll(HamcrestMatcherProvider.class) .values() .stream() .filter(provider -> provider.getName().equals(matcherName)) @@ -362,6 +400,35 @@ public List extractControlValues(String controlExpression, Character del } } + /** + * Extracts parameters for a matcher from the raw parameter expression. + * Parameters refer to the contained parameters and matchers (first level), + * excluding nested ones. + *

+ * For example, given the expression:
+ * {@code "oneOf(greaterThan(5.0), allOf(lessThan(-1.0), greaterThan(-2.0)))"} + *

+ * The extracted parameters are:
+ * {@code "greaterThan(5.0)", "allOf(lessThan(-1.0), greaterThan(-2.0))"}. + *

+ * Note that nested container expressions "allOf(lessThan(-1.0), greaterThan(-2.0))" in + * the second parameter are treated as a single expression. They need to be treated + * separately in a recursive call to this method, when the parameters for the + * respective allOf() expression are extracted. + * + * @param rawExpression the full parameter expression of a container matcher + */ + public String[] determineNestedMatcherParameters(final String rawExpression) { + if (!StringUtils.hasText(rawExpression)) { + return new String[0]; + } + + Tokenizer tokenizer = new Tokenizer(); + String tokenizedExpression = tokenizer.tokenize(rawExpression); + return tokenizer.restoreInto(tokenizedExpression.split(",")); + + } + /** * Numeric value comparable automatically converts types to numeric values for * comparison. @@ -418,4 +485,92 @@ public String toString() { } } + /** + * Class that provides functionality to replace expressions that match + * {@link Tokenizer#TEXT_PARAMETER_PATTERN} with simple tokens of the form $$n$$. + * The reason for this is, that complex nested expressions + * may contain characters that interfere with further processing - e.g. ''', '(' and ')' + */ + private static class Tokenizer { + + private static final String START_TOKEN = "_TOKEN-"; + + private static final String END_TOKEN = "-TOKEN_"; + + /** + * Regular expression with three alternative parts (ored) to match: + *

    + *
  1. `(sometext)` - Quoted parameter block of a matcher.
  2. + *
  3. 'sometext' - Quoted text used as a parameter to a string matcher.
  4. + *
  5. (unquotedtext) - Unquoted text used as a parameter to a string matcher. This expression is non-greedy, meaning the first closing bracket will terminate the match.
  6. + *
+ *

+ * Please note: + * - 'sometext' may contain an escaped quote. + * - 'unquotedtext' must not contain brackets or commas. + *

+ * To match quotes, commas, or brackets, you must quote the text. To match a quote, it should be escaped with a backslash. + * Therefore, the regex expressions explicitly match the escaped quote -> \\\\' + */ + private static final Pattern TEXT_PARAMETER_PATTERN = Pattern.compile( + "(?\\('(?:[^']|\\\\')*[^\\\\]'\\))" + + "|(?('(?:[^']|\\\\')*[^\\\\]'))" + + "|(?\\(((?:[^']|\\\\')*?)[^\\\\]?\\))" + ); + + private final List originalTokenValues = new ArrayList<>(); + + /** + * Tokenize the given raw expression + * + * @param rawExpression + * @return the expression with all relevant subexpressions replaced by tokens + */ + public String tokenize(String rawExpression) { + java.util.regex.Matcher matcher = TEXT_PARAMETER_PATTERN.matcher(rawExpression); + StringBuilder builder = new StringBuilder(); + + while (matcher.find()) { + String matchedValue = findMatchedValue(matcher); + originalTokenValues.add(matchedValue); + matcher.appendReplacement(builder, START_TOKEN + originalTokenValues.size() + END_TOKEN); + } + + matcher.appendTail(builder); + return builder.toString(); + } + + /** + * @param matcher the matcher that was used to match + * @return the value of the group, that was actually matched + */ + private String findMatchedValue(java.util.regex.Matcher matcher) { + String matchedValue = matcher.group("quoted1"); + matchedValue = matchedValue != null ? matchedValue : matcher.group("quoted2"); + return matchedValue != null ? matchedValue : matcher.group("unquoted"); + } + + /** + * Restore the tokens back into the given expressions. + * + * @param expressions containing strings with tokens, generated by this tokenizer. + * @return expressions with the tokens being replaced with their original values. + */ + public String[] restoreInto(String[] expressions) { + + for (int i = 0; i < expressions.length; i++) { + expressions[i] = VariableUtils.cutOffSingleQuotes( + replaceTokens(expressions[i], originalTokenValues).trim()); + } + + return expressions; + } + + private String replaceTokens(String expression, List params) { + for (int i = 0; i < params.size(); i++) { + expression = expression.replace(START_TOKEN + (i + 1) + END_TOKEN, params.get(i)); + } + return expression; + } + } } diff --git a/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcherTest.java b/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcherTest.java index d850fe748b..7d20faa75e 100644 --- a/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcherTest.java +++ b/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcherTest.java @@ -35,7 +35,7 @@ */ public class HamcrestValidationMatcherTest extends AbstractTestNGUnitTest { - private HamcrestValidationMatcher validationMatcher = new HamcrestValidationMatcher(); + private final HamcrestValidationMatcher validationMatcher = new HamcrestValidationMatcher(); @Override protected TestContextFactory createTestContextFactory() { @@ -49,7 +49,7 @@ public String getName() { @Override public Matcher provideMatcher(String predicate) { - return new CustomMatcher(String.format("path matching %s", predicate)) { + return new CustomMatcher<>(String.format("path matching %s", predicate)) { @Override public boolean matches(Object item) { return ((item instanceof String) && new AntPathMatcher().match(predicate, (String) item)); @@ -63,7 +63,7 @@ public boolean matches(Object item) { } @Test(dataProvider = "testData") - public void testValidate(String path, String value, List params) throws Exception { + public void testValidate(String path, String value, List params) { validationMatcher.validate( path, value, params, context); } @@ -72,22 +72,35 @@ public Object[][] testData() { return new Object[][] { new Object[]{ "foo", "value", Collections.singletonList("equalTo(value)") }, new Object[]{ "foo", "value", Collections.singletonList("equalTo('value')") }, + new Object[]{ "foo", "value with ' quote", Collections.singletonList("equalTo('value with \\' quote')") }, + new Object[]{ "foo", "value with ' quote", Collections.singletonList("equalTo(value with \\' quote)") }, + new Object[]{ "foo", "value", Collections.singletonList("equalTo('value')") }, new Object[]{"foo", "value", Collections.singletonList("not(equalTo(other))")}, new Object[]{"foo", "value", Collections.singletonList("is(not(other))")}, new Object[]{"foo", "value", Collections.singletonList("not(is(other))")}, new Object[]{"foo", "value", Collections.singletonList("equalToIgnoringCase(VALUE)")}, + new Object[]{"foo", "value with ' quote", Collections.singletonList("equalToIgnoringCase(VALUE WITH \\' QUOTE)")}, new Object[]{"foo", "value", Collections.singletonList("containsString(lue)")}, + new Object[]{"foo", "value with ' quote", Collections.singletonList("containsString(with \\')")}, + new Object[]{"foo", "value with ' quote", Collections.singletonList("containsString(\\')")}, + new Object[]{"foo", "value with ' quote", Collections.singletonList("containsString(value with \\' qu)")}, new Object[]{"foo", "value", Collections.singletonList("not(containsString(other))")}, new Object[]{"foo", "value", Collections.singletonList("startsWith(val)")}, + new Object[]{"foo", "value with ' quote", Collections.singletonList("startsWith(value with \\' q)")}, + new Object[]{"foo", "value with ' quote", Collections.singletonList("startsWith('value with \\' q')")}, new Object[]{"foo", "value", Collections.singletonList("endsWith(lue)")}, + new Object[]{"foo", "value with ' quote", Collections.singletonList("endsWith(th \\' quote)")}, + new Object[]{"foo", "value with ' quote", Collections.singletonList("endsWith('th \\' quote')")}, new Object[]{"foo", "value", Collections.singletonList("anyOf(startsWith(val), endsWith(lue))")}, new Object[]{"foo", "value", Collections.singletonList("allOf(startsWith(val), endsWith(lue))")}, new Object[]{"foo", "value/12345", Collections.singletonList("matchesPath(value/{id})")}, new Object[]{"foo", "value/12345/test", Collections.singletonList("matchesPath(value/{id}/test)")}, new Object[]{"foo", "value", Collections.singletonList("isOneOf(value, other)")}, + new Object[]{"foo", "value with ' quote", Collections.singletonList("isOneOf('value with \\' quote', 'other')")}, new Object[]{"foo", "test value", Collections.singletonList("isOneOf('test value', 'other ')")}, new Object[]{"foo", "9.0", Collections.singletonList("isOneOf(9, 9.0)")}, new Object[]{"foo", "value", Collections.singletonList("isIn(value, other)")}, + new Object[]{"foo", "value with ' quote", Collections.singletonList("isIn('value with \\' quote', 'other')")}, new Object[]{"foo", "test value", Collections.singletonList("isIn('test value', 'other ')")}, new Object[]{"foo", "9.0", Collections.singletonList("isIn(9, 9.0)")}, new Object[]{"foo", "", Collections.singletonList("isEmptyString()")}, @@ -116,23 +129,49 @@ public Object[][] testData() { new Object[]{"foo", "", Arrays.asList("9", "lessThanOrEqualTo(9)")}, new Object[]{"foo", "{value1=value2,value4=value5}", Collections.singletonList("hasSize(2)") }, new Object[]{"foo", "{value1=value2,value4=value5}", Collections.singletonList("hasEntry(value1,value2)") }, + new Object[]{"foo", "{value1=value2 with ' quote,value4=value5}", Collections.singletonList("hasEntry(value1,value2 with ' quote)") }, new Object[]{"foo", "{value1=value2,value4=value5}", Collections.singletonList("hasKey(value1)") }, new Object[]{"foo", "{\"value1\"=\"value2\",\"value4\"=\"value5\"}", Collections.singletonList("hasKey(value1)") }, - new Object[]{"foo", "{value1=value2,value4=value5}", Collections.singletonList("hasValue(value2)") }, + new Object[]{"foo", "{value1=value2 with ' quote,value4=value5}", Collections.singletonList("hasValue(value2 with \\' quote)") }, new Object[]{"foo", "[value1,value2,value3,value4,value5]", Collections.singletonList("hasSize(5)") }, new Object[]{"foo", "[value1,value2,value3,value4,value5]", Collections.singletonList("everyItem(startsWith(value))") }, new Object[]{"foo", "[value1,value2,value3,value4,value5]", Collections.singletonList("hasItem(value2)") }, new Object[]{"foo", "[value1,value2,value3,value4,value5]", Collections.singletonList("hasItems(value2,value5)") }, + new Object[]{"foo", "[a,b,c,d,e]", Collections.singletonList("hasItems('a','b','c')") }, + new Object[]{"foo", "[a,b,c,d,e]", Collections.singletonList("hasItems(a, b, c)") }, + new Object[]{"foo", "[a'a,b'b,c'c,d'd,e'e]", Collections.singletonList("hasItems('a\\'a','b\\'b','c\\'c')") }, + new Object[]{"foo", "[a\\'a,b\\'b,c\\'c,d\\'d,e\\'e]", Collections.singletonList("hasItems('a\\\\'a','b\\\\'b','c\\\\'c')") }, new Object[]{"foo", "[\"value1\",\"value2\",\"value3\",\"value4\",\"value5\"]", Collections.singletonList("hasItems(value2,value5)") }, new Object[]{"foo", "[value1,value2,value3,value4,value5]", Collections.singletonList("contains(value1,value2,value3,value4,value5)") }, new Object[]{"foo", "[value1,value2,value3,value4,value5]", Collections.singletonList("containsInAnyOrder(value2,value4,value1,value3,value5)") }, + new Object[]{"foo", "[a,b,c,d,e]", Collections.singletonList("contains('a','b','c','d','e')") }, + new Object[]{"foo", "[a,b,c,d,e]", Collections.singletonList("contains(a,b,c,d,e)") }, + new Object[]{"foo", "[a'a,b'b,c'c,d'd,e'e]", Collections.singletonList("contains('a\\'a','b\\'b','c\\'c','d\\'d','e\\'e')") }, + new Object[]{"foo", "[a\\'a,b\\'b,c\\'c,d\\'d,e\\'e]", Collections.singletonList("contains('a\\\\'a','b\\\\'b','c\\\\'c','d\\\\'d','e\\\\'e')") }, new Object[]{"foo", "[\"unique_value\",\"different_unique_value\"]", Collections.singletonList("hasSize(2)") }, - new Object[]{"foo", "[\"duplicate_value\",\"duplicate_value\"]", Collections.singletonList("hasSize(2)") } + new Object[]{"foo", "[\"duplicate_value\",\"duplicate_value\"]", Collections.singletonList("hasSize(2)") }, + new Object[]{"foo", "text containing a , (comma) ", Collections.singletonList("anyOf(equalTo('text containing a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "", Collections.singletonList("anyOf(equalTo('text containing a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", null, Collections.singletonList("anyOf(equalTo('text containing a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "text-equalTo(QA, Max", Collections.singletonList("anyOf(equalTo('text-equalTo(QA, Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "", Collections.singletonList("anyOf(equalTo('text-equalTo(QA, Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", null, Collections.singletonList("anyOf(equalTo('text-equalTo(QA, Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "QA-equalTo(HH), Max", Collections.singletonList("anyOf(equalTo('QA-equalTo(HH), Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "text containing a ' (quote) and a , (comma) ", Collections.singletonList("anyOf(equalTo('text containing a \\' (quote) and a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "text containing a \\' (backslashquote) and a , (comma) ", Collections.singletonList("anyOf(equalTo('text containing a \\\\' (backslashquote) and a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "unquoted text may not include brackets or commas", Collections.singletonList("anyOf(equalTo(unquoted text may not include brackets or commas), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "quoted \\' text may not include brackets or commas", Collections.singletonList("anyOf(equalTo(quoted \\\\' text may not include brackets or commas), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "value1", Collections.singletonList("anyOf(isEmptyOrNullString(),equalTo(value1))")}, + + new Object[]{"foo", "INSERT INTO todo_entries (id, title, description, done) values (1, 'Invite for meeting', 'Invite the group for a lunch meeting', 'false')", + Collections.singletonList("allOf(startsWith('INSERT INTO todo_entries (id, title, description, done)'))")}, + + }; } @Test(dataProvider = "testDataFailed", expectedExceptions = ValidationException.class) - public void testValidateFailed(String path, String value, List params) throws Exception { + public void testValidateFailed(String path, String value, List params) { validationMatcher.validate( path, value, params, context); } @@ -142,6 +181,7 @@ public Object[][] testDataFailed() { new Object[]{ "foo", "value", Collections.singletonList("equalTo(wrong)") }, new Object[]{"foo", "value", Collections.singletonList("not(equalTo(value))")}, new Object[]{"foo", "value", Collections.singletonList("is(not(value))")}, + new Object[]{"foo", "val with quote ' ue", Collections.singletonList("is(not(val with quote \\' ue))")}, new Object[]{"foo", "value", Collections.singletonList("not(is(value))")}, new Object[]{"foo", "value", Collections.singletonList("equalToIgnoringCase(WRONG)")}, new Object[]{"foo", "value", Collections.singletonList("containsString(wrong)")}, @@ -190,7 +230,14 @@ public Object[][] testDataFailed() { new Object[]{"foo", "[value1,value2]", Collections.singletonList("hasItem(value5)") }, new Object[]{"foo", "[value1,value2]", Collections.singletonList("hasItems(value1,value2,value5)") }, new Object[]{"foo", "[value1,value2]", Collections.singletonList("contains(value1)") }, - new Object[]{"foo", "[value1,value2]", Collections.singletonList("containsInAnyOrder(value2,value4)") } + new Object[]{"foo", "[value1,value2]", Collections.singletonList("containsInAnyOrder(value2,value4)") }, + new Object[]{"foo", "notext-equalTo(QA, Max", Collections.singletonList("anyOf(equalTo('text-equalTo(QA, Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "aa", Collections.singletonList("anyOf(equalTo('text-equalTo(QA, Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "VA-equalTo(HH), Max", Collections.singletonList("anyOf(equalTo('QA-equalTo(HH), Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "notext containing a ' (quote) and a , (comma) ", Collections.singletonList("anyOf(equalTo('text containing a \\' (quote) and a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "notext containing a \\' (quote) and a , (comma) ", Collections.singletonList("anyOf(equalTo('text containing a \\\\' (quote) and a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "nounquoted text may not include brackets or commas", Collections.singletonList("anyOf(equalTo(unquoted text may not include brackets or commas), anyOf(isEmptyOrNullString()))")}, + }; } }