From 910e4226fb692b69a84dff1e9421b2aa424e5c65 Mon Sep 17 00:00:00 2001 From: Madalin Ilie Date: Thu, 19 Sep 2024 20:56:56 +0300 Subject: [PATCH] feat: Improve handling of complex regexes like email, password or uri Patterns used to validate email, password and uri can be quite complex. The current generators are struggling to generate valid values expecially when a fixed, large length is required. In this cases specific code is introduced in this commit to handle these 3 particular cases --- .../base/ExactValuesInFieldsFuzzer.java | 3 +- .../format/impl/PasswordGenerator.java | 3 +- .../cats/generator/simple/RegexFlattener.java | 45 +++++ .../generator/simple/StringGenerator.java | 156 ++++++++++++++---- .../com/endava/cats/model/FuzzingData.java | 4 +- .../com/endava/cats/util/CatsModelUtils.java | 55 ++++++ .../format/impl/PasswordGeneratorTest.java | 7 + .../generator/simple/StringGeneratorTest.java | 43 ++++- .../endava/cats/util/CatsModeLUtilsTest.java | 21 +++ 9 files changed, 304 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/endava/cats/generator/simple/RegexFlattener.java diff --git a/src/main/java/com/endava/cats/fuzzer/fields/base/ExactValuesInFieldsFuzzer.java b/src/main/java/com/endava/cats/fuzzer/fields/base/ExactValuesInFieldsFuzzer.java index 74bd79f31..eb7eb7744 100644 --- a/src/main/java/com/endava/cats/fuzzer/fields/base/ExactValuesInFieldsFuzzer.java +++ b/src/main/java/com/endava/cats/fuzzer/fields/base/ExactValuesInFieldsFuzzer.java @@ -93,7 +93,8 @@ private String generateWithAdjustedLength(Schema schema, int adjustedLength) { int fromSchemaLengthAdjusted = (fromSchemaLength.intValue() > Integer.MAX_VALUE / 100 - adjustedLength) ? Integer.MAX_VALUE / 100 : fromSchemaLength.intValue(); int generatedStringLength = fromSchemaLengthAdjusted + adjustedLength; - String generated = StringGenerator.generateExactLength(pattern, generatedStringLength); + String generated = StringGenerator.generateExactLength(schema, pattern, generatedStringLength); + if (CatsModelUtils.isByteArraySchema(schema)) { return Base64.getEncoder().encodeToString(generated.getBytes(StandardCharsets.UTF_8)); } diff --git a/src/main/java/com/endava/cats/generator/format/impl/PasswordGenerator.java b/src/main/java/com/endava/cats/generator/format/impl/PasswordGenerator.java index 5a62c094f..978c6a92d 100644 --- a/src/main/java/com/endava/cats/generator/format/impl/PasswordGenerator.java +++ b/src/main/java/com/endava/cats/generator/format/impl/PasswordGenerator.java @@ -7,6 +7,7 @@ import jakarta.inject.Singleton; import java.util.List; +import java.util.Locale; /** * A generator class implementing interfaces for generating valid and invalid password data formats. @@ -21,7 +22,7 @@ public Object generate(Schema schema) { @Override public boolean appliesTo(String format, String propertyName) { - return "password".equalsIgnoreCase(format); + return "password".equalsIgnoreCase(format) || propertyName.toLowerCase(Locale.ROOT).endsWith("password"); } @Override diff --git a/src/main/java/com/endava/cats/generator/simple/RegexFlattener.java b/src/main/java/com/endava/cats/generator/simple/RegexFlattener.java new file mode 100644 index 000000000..4643e60a1 --- /dev/null +++ b/src/main/java/com/endava/cats/generator/simple/RegexFlattener.java @@ -0,0 +1,45 @@ +package com.endava.cats.generator.simple; + +/** + * Flattens a regex by simplifying character classes, quantifiers, and removing unnecessary parentheses. + */ +public abstract class RegexFlattener { + + + /** + * Flattens a regex by simplifying character classes, quantifiers, and removing unnecessary parentheses. + * + * @param regex the regex to flatten + * @return the flattened regex + */ + public static String flattenRegex(String regex) { + regex = simplifyCharacterClasses(regex); + regex = simplifyQuantifiers(regex); +// regex = useNonCapturingGroups(regex); + + return regex; + } + + public static String useNonCapturingGroups(String regex) { + return regex.replaceAll("\\((?!\\?:)(?=[^()]*\\|)", "(?:"); + } + + private static String simplifyCharacterClasses(String regex) { + regex = regex.replaceAll("\\[a-zA-Z0-9_\\]", "\\\\w"); + regex = regex.replaceAll("\\[0-9\\]", "\\\\d"); + regex = regex.replaceAll("\\[\\s\\t\\r\\n\\f\\]", "\\\\s"); + + regex = regex.replaceAll("\\[^\\\\d\\]", "\\\\D"); + regex = regex.replaceAll("\\[^\\\\w\\]", "\\\\W"); + regex = regex.replaceAll("\\[^\\\\s\\]", "\\\\S"); + + return regex; + } + + private static String simplifyQuantifiers(String regex) { + regex = regex.replaceAll("\\{0,1\\}", "?"); + regex = regex.replaceAll("\\{1,\\}", "+"); + regex = regex.replaceAll("\\{0,\\}", "*"); + return regex; + } +} diff --git a/src/main/java/com/endava/cats/generator/simple/StringGenerator.java b/src/main/java/com/endava/cats/generator/simple/StringGenerator.java index 874d2410f..1ccbb48f2 100755 --- a/src/main/java/com/endava/cats/generator/simple/StringGenerator.java +++ b/src/main/java/com/endava/cats/generator/simple/StringGenerator.java @@ -1,5 +1,6 @@ package com.endava.cats.generator.simple; +import com.endava.cats.util.CatsModelUtils; import com.endava.cats.util.CatsUtil; import com.github.curiousoddman.rgxgen.RgxGen; import io.github.ludovicianul.prettylogger.PrettyLogger; @@ -14,12 +15,19 @@ import org.springframework.util.CollectionUtils; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Locale; +import java.util.Optional; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import static com.endava.cats.util.CatsModelUtils.isEmail; +import static com.endava.cats.util.CatsModelUtils.isPassword; +import static com.endava.cats.util.CatsModelUtils.isUri; + /** * Generates strings based on different criteria. */ @@ -49,6 +57,11 @@ public class StringGenerator { private static final Pattern LENGTH_INLINE_PATTERN = Pattern.compile("(\\^)?(\\[[^]]*]\\{\\d+}|\\(\\[[^]]*]\\{\\d+}\\)\\?)*(\\$)?"); + private static final String ALPHANUMERIC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static final String[] DOMAINS = {"example", "cats", "google", "yahoo"}; + private static final String[] TLDS = {".com", ".net", ".org", ".io"}; + private static final String[] URI_SCHEMES = {"http", "https", "ftp", "file"}; + /** * Represents an empty string. */ @@ -127,12 +140,17 @@ public static String generateLargeString(int times) { * @param length the desired length * @return a generated value of exact length provided */ - public static String generateExactLength(String regex, int length) { + public static String generateExactLength(Schema schema, String regex, int length) { if (length <= 0) { return EMPTY; } - StringBuilder initialValue = new StringBuilder(StringGenerator.sanitize(generate(cleanPattern(regex), length, length))); + String stringFromComplexRegex = generateComplexRegex(schema, length); + if (stringFromComplexRegex != null) { + return stringFromComplexRegex; + } + + StringBuilder initialValue = new StringBuilder(StringGenerator.sanitize(generate(regex, length, length))); if (initialValue.isEmpty()) { return EMPTY; @@ -162,9 +180,10 @@ public static String generateExactLength(String regex, int length) { */ public static String generate(String pattern, int min, int max) { LOGGER.debug("Generate for pattern {} min {} max {}", pattern, min, max); - pattern = cleanPattern(pattern); + String cleanedPattern = cleanPattern(pattern); + String flattenedPattern = RegexFlattener.flattenRegex(cleanedPattern); - GeneratorParams generatorParams = new GeneratorParams(pattern, min, max); + GeneratorParams generatorParams = new GeneratorParams(flattenedPattern, min, max, cleanedPattern); String generatedWithRgxGenerator = callGenerateTwice(StringGenerator::generateUsingRgxGenerator, generatorParams); if (generatedWithRgxGenerator != null) { @@ -187,8 +206,8 @@ public static String generate(String pattern, int min, int max) { public static String callGenerateTwice(Function generator, GeneratorParams generatorParams) { try { String initialVersion = generator.apply(generatorParams); - if (initialVersion.matches(generatorParams.pattern)) { - LOGGER.debug("Generated value " + initialVersion + " matched " + generatorParams.pattern); + if (initialVersion.matches(generatorParams.originalPattern())) { + LOGGER.debug("Generated value " + initialVersion + " matched " + generatorParams.originalPattern()); return initialVersion; } } catch (Exception e) { @@ -196,9 +215,9 @@ public static String callGenerateTwice(Function generat } try { - String secondVersion = generator.apply(new GeneratorParams(removeLookaheadAssertions(generatorParams.pattern), generatorParams.min, generatorParams.max)); - if (secondVersion.matches(generatorParams.pattern)) { - LOGGER.debug("Generated value with lookaheads removed " + secondVersion + " matched " + generatorParams.pattern); + String secondVersion = generator.apply(new GeneratorParams(removeLookaheadAssertions(generatorParams.cleanedPattern()), generatorParams.min, generatorParams.max, generatorParams.originalPattern())); + if (secondVersion.matches(generatorParams.originalPattern())) { + LOGGER.debug("Generated value with lookaheads removed " + secondVersion + " matched " + generatorParams.originalPattern()); return secondVersion; } } catch (Exception e) { @@ -236,7 +255,8 @@ public static String cleanPattern(String pattern) { } private static String generateUsingRegexpGen(GeneratorParams generatorParams) { - String pattern = generatorParams.pattern; + String pattern = generatorParams.cleanedPattern(); + String originalPattern = generatorParams.originalPattern(); int min = generatorParams.min; int max = generatorParams.max; @@ -248,7 +268,7 @@ private static String generateUsingRegexpGen(GeneratorParams generatorParams) { } String generated = generator.generate(REGEXP_RANDOM_GEN, min, max); - if (generated.matches(pattern)) { + if (generated.matches(originalPattern)) { LOGGER.debug("Generated using REGEXP {} matches {}", generated, pattern); return generated; } @@ -259,7 +279,8 @@ private static String generateUsingRegexpGen(GeneratorParams generatorParams) { } private static String generateUsingCatsRegexGenerator(GeneratorParams generatorParams) { - String pattern = generatorParams.pattern; + String pattern = generatorParams.cleanedPattern(); + String originalPattern = generatorParams.originalPattern(); int min = generatorParams.min; int max = generatorParams.max; @@ -267,13 +288,13 @@ private static String generateUsingCatsRegexGenerator(GeneratorParams generatorP Pattern compiledPattern = Pattern.compile(pattern); String secondVersionBase = RegexGenerator.generate(compiledPattern, EMPTY, min, max); - if (secondVersionBase.matches(pattern)) { + if (secondVersionBase.matches(originalPattern)) { LOGGER.debug("Generated using CATS generator {} and matches {}", secondVersionBase, pattern); return secondVersionBase; } String generatedString = composeString(secondVersionBase, min, max); - if (generatedString.matches(pattern)) { + if (generatedString.matches(originalPattern)) { LOGGER.debug("Generated using CATS generator {} and matches {}", generatedString, pattern); return generatedString; } @@ -284,19 +305,20 @@ private static String generateUsingCatsRegexGenerator(GeneratorParams generatorP private static String generateUsingRgxGenerator(GeneratorParams generatorParams) { int attempts = 0; String generatedValue; - String pattern = generatorParams.pattern; + String pattern = generatorParams.cleanedPattern(); + String originalPattern = generatorParams.originalPattern(); int min = generatorParams.min; int max = generatorParams.max; try { do { generatedValue = new RgxGen(pattern).generate(); - if ((hasLengthInline(pattern) || isSetOfAlternatives(pattern) || (min <= 0 && max <= 0)) && generatedValue.matches(pattern)) { + if ((hasLengthInline(pattern) || isSetOfAlternatives(pattern) || (min <= 0 && max <= 0)) && generatedValue.matches(originalPattern)) { return generatedValue; } generatedValue = composeString(generatedValue, min, max); attempts++; - } while (attempts < MAX_ATTEMPTS_GENERATE && !generatedValue.matches(pattern)); + } while (attempts < MAX_ATTEMPTS_GENERATE && !generatedValue.matches(originalPattern)); } catch (Exception e) { LOGGER.debug("RGX generator failed, returning empty.", e); return ALPHANUMERIC_VALUE; @@ -443,6 +465,11 @@ public static String generateValueBasedOnMinMax(Schema property) { maxLength = minLength; } + String complexRegexGenerated = generateComplexRegex(property, Math.max(1, maxLength)); + if (complexRegexGenerated != null) { + return complexRegexGenerated; + } + return StringGenerator.generate(pattern, minLength, maxLength); } @@ -481,30 +508,103 @@ public static String removeLookaheadAssertions(String regex) { return regex; } + public static String generateFixedLengthEmail(int length) { + String domain = DOMAINS[CatsUtil.random().nextInt(DOMAINS.length)]; + String tld = TLDS[CatsUtil.random().nextInt(TLDS.length)]; + + int localPartLength = length - domain.length() - tld.length() - 1; // -1 for '@' + + StringBuilder localPart = new StringBuilder(); + for (int i = 0; i < localPartLength; i++) { + localPart.append(ALPHANUMERIC.charAt(CatsUtil.random().nextInt(ALPHANUMERIC.length()))); + } + + return localPart + "@" + domain + tld; + } + + public static String generateFixedLengthUri(int length) { + String scheme = URI_SCHEMES[CatsUtil.random().nextInt(URI_SCHEMES.length)]; + + String domain = DOMAINS[CatsUtil.random().nextInt(DOMAINS.length)]; + String tld = TLDS[CatsUtil.random().nextInt(TLDS.length)]; + + String fixedPart = scheme + "://" + domain + tld; + + int pathLength = length - fixedPart.length(); + if (pathLength <= 0) { + return fixedPart.substring(0, length); + } + + StringBuilder path = new StringBuilder(); + path.append("/"); + for (int i = 0; i < pathLength - 1; i++) { + path.append(ALPHANUMERIC.charAt(CatsUtil.random().nextInt(ALPHANUMERIC.length()))); + } + + return fixedPart + path; + } + + /** + * There are complex regexes which will fail to generate a string of a given length, especially for a fixed and large length. + * This is particularly true for email addresses and URIs where patterns can be quite complex. + * Sometimes, for large generated strings, the match of the generated string against the given regex will fail with StackOverflowError. + *

+ * This method tries to generate a string of a given length for such complex regexes. It only supports URIs and emails for now. + * + * @param schema the schema + * @param length the length + * @return a string of given length matching patterns schema + */ + private static String generateComplexRegex(Schema schema, int length) { + if (StringUtils.isBlank(schema.getPattern())) { + return null; + } + + String lowerField = Optional.ofNullable(schema.getExtensions()).orElse(Collections.emptyMap()).getOrDefault(CatsModelUtils.X_CATS_FIELD_NAME, "").toString().toLowerCase(Locale.ROOT); + String pattern = schema.getPattern(); + + if (isUri(pattern, lowerField)) { + return generateFixedLengthUri(length); + } + if (isEmail(pattern, lowerField)) { + return generateFixedLengthEmail(length); + } + if (isPassword(pattern, lowerField)) { + return "catsISC00l#" + RandomStringUtils.secure().nextPrint(length - 11); + } + + return null; + } + /** * A record that holds the parameters for the string generator. * - * @param pattern the pattern to check - * @param min the minimum length - * @param max the maximum length + * @param cleanedPattern the pattern to check + * @param min the minimum length + * @param max the maximum length + * @param originalPattern the original pattern */ - public record GeneratorParams(String pattern, int min, int max) { + public record GeneratorParams(String cleanedPattern, int min, int max, String originalPattern) { /** * Instantiates a new Generator params. * - * @param pattern the pattern - * @param min the min - * @param max the max + * @param cleanedPattern the pattern + * @param min the min + * @param max the max + * @param originalPattern the original pattern */ - public GeneratorParams(String pattern, int min, int max) { + public GeneratorParams(String cleanedPattern, int min, int max, String originalPattern) { this.min = min; this.max = max; + this.originalPattern = inlineLengthIfNeeded(originalPattern, min, max); + this.cleanedPattern = inlineLengthIfNeeded(cleanedPattern, min, max); + } + private String inlineLengthIfNeeded(String pattern, int min, int max) { if (!hasLength(pattern) && (min > 0 || max > 0)) { - this.pattern = pattern + "{" + min + "," + max + "}"; - } else { - this.pattern = pattern; + return pattern + "{" + min + "," + max + "}"; } + return pattern; } } } diff --git a/src/main/java/com/endava/cats/model/FuzzingData.java b/src/main/java/com/endava/cats/model/FuzzingData.java index 75b3e8279..450d25e9d 100644 --- a/src/main/java/com/endava/cats/model/FuzzingData.java +++ b/src/main/java/com/endava/cats/model/FuzzingData.java @@ -1,8 +1,8 @@ package com.endava.cats.model; import com.endava.cats.http.HttpMethod; -import com.endava.cats.util.JsonUtils; import com.endava.cats.util.CatsModelUtils; +import com.endava.cats.util.JsonUtils; import io.github.ludovicianul.prettylogger.PrettyLogger; import io.github.ludovicianul.prettylogger.PrettyLoggerFactory; import io.swagger.v3.oas.models.OpenAPI; @@ -163,6 +163,8 @@ private Set getFields(Schema schema, String prefix) { return catsFields.stream() .filter(catsField -> this.getRequestPropertyTypes().get(catsField.getName()) != null) + //this is a bit of a hack that might be abused in the future to include a full object as extension. currently it only holds the field name + .peek(catsField -> catsField.getSchema().addExtension(CatsModelUtils.X_CATS_FIELD_NAME, catsField.getName())) .collect(Collectors.toSet()); } diff --git a/src/main/java/com/endava/cats/util/CatsModelUtils.java b/src/main/java/com/endava/cats/util/CatsModelUtils.java index 7371a551a..fed5cf791 100644 --- a/src/main/java/com/endava/cats/util/CatsModelUtils.java +++ b/src/main/java/com/endava/cats/util/CatsModelUtils.java @@ -5,9 +5,13 @@ import io.swagger.v3.oas.models.media.ObjectSchema; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.parser.util.SchemaTypeUtil; +import org.apache.commons.lang3.StringUtils; import org.openapitools.codegen.utils.ModelUtils; +import java.util.Collections; import java.util.List; +import java.util.Locale; +import java.util.Optional; import java.util.stream.Stream; /** @@ -15,6 +19,7 @@ * some particular conditions needed by CATS. */ public abstract class CatsModelUtils { + public static final String X_CATS_FIELD_NAME = "x-cats-field-name"; private CatsModelUtils() { //ntd @@ -142,4 +147,54 @@ public static String eliminateDuplicatePart(String input) { }) .orElse(""); } + + /** + * Checks if the field name contains a complex regex. For now, this is used to determine if the field is an email or a URI. + * This is of course not 100% accurate, but it's a good start. + * + * @param schema the schema to be checked + * @return true if the field name contains a complex regex, false otherwise + */ + public static boolean isComplexRegex(Schema schema) { + if (StringUtils.isBlank(schema.getPattern())) { + return false; + } + + String lowerField = Optional.ofNullable(schema.getExtensions()).orElse(Collections.emptyMap()).getOrDefault(X_CATS_FIELD_NAME, "").toString().toLowerCase(Locale.ROOT); + String pattern = schema.getPattern(); + return isEmail(pattern, lowerField) || isUri(pattern, lowerField) || isPassword(pattern, lowerField); + } + + /** + * Checks if the given combination or pattern and field name is a URI. + * + * @param pattern the pattern of the field + * @param lowerField the name of the field + * @return true if the field name contains a complex regex, false otherwise + */ + public static boolean isUri(String pattern, String lowerField) { + return (lowerField.contains("url") || lowerField.contains("uri")) && ("http://www.test.com".matches(pattern) || "https://www.test.com".matches(pattern)); + } + + /** + * Checks if the given combination or pattern and field name is an email. + * + * @param pattern the pattern of the field + * @param lowerField the name of the field + * @return true if the field name contains a complex regex, false otherwise + */ + public static boolean isEmail(String pattern, String lowerField) { + return lowerField.contains("email") && "test@test.com".matches(pattern); + } + + /** + * Checks if the given combination or pattern and field name is a password. + * + * @param pattern the pattern of the field + * @param lowerField the name of the field + * @return true if the field name contains a complex regex, false otherwise + */ + public static boolean isPassword(String pattern, String lowerField) { + return lowerField.contains("password") && "catsISc00l?!useIt#".matches(pattern); + } } diff --git a/src/test/java/com/endava/cats/generator/format/impl/PasswordGeneratorTest.java b/src/test/java/com/endava/cats/generator/format/impl/PasswordGeneratorTest.java index f62f6bc26..d5308bba6 100644 --- a/src/test/java/com/endava/cats/generator/format/impl/PasswordGeneratorTest.java +++ b/src/test/java/com/endava/cats/generator/format/impl/PasswordGeneratorTest.java @@ -23,6 +23,13 @@ void shouldApply(String format, boolean expected) { Assertions.assertThat(passwordGenerator.appliesTo(format, "")).isEqualTo(expected); } + @ParameterizedTest + @CsvSource({"password,true", "other,false"}) + void shouldApplyToPropertyName(String property, boolean expected) { + PasswordGenerator passwordGenerator = new PasswordGenerator(); + Assertions.assertThat(passwordGenerator.appliesTo("", property)).isEqualTo(expected); + } + @Test void givenAPasswordFormatGeneratorStrategy_whenGettingTheAlmostValidValue_thenTheValueIsReturnedAsExpected() { PasswordGenerator strategy = new PasswordGenerator(); diff --git a/src/test/java/com/endava/cats/generator/simple/StringGeneratorTest.java b/src/test/java/com/endava/cats/generator/simple/StringGeneratorTest.java index 76d6b25d3..51e939474 100644 --- a/src/test/java/com/endava/cats/generator/simple/StringGeneratorTest.java +++ b/src/test/java/com/endava/cats/generator/simple/StringGeneratorTest.java @@ -1,5 +1,6 @@ package com.endava.cats.generator.simple; +import com.endava.cats.util.CatsModelUtils; import io.quarkus.test.junit.QuarkusTest; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; @@ -114,7 +115,7 @@ void shouldSanitize(String input, String expected) { @ParameterizedTest @CsvSource(value = {"^\\+?[1-9]\\d{6,15}$;16", "[A-Z]+;20", "[A-Z0-9]{13,18};18", "[0-9]+;10", "^(?=[^\\s])(?=.*[^\\s]$)(?=^(?:(?!<|>|%3e|%3c).)*$).*$;2048", "M|F;1"}, delimiterString = ";") void shouldGenerateFixedLength(String pattern, int length) { - String fixedLengthGenerated = StringGenerator.generateExactLength(pattern, length); + String fixedLengthGenerated = StringGenerator.generateExactLength(new Schema(), pattern, length); Assertions.assertThat(fixedLengthGenerated).hasSize(length).matches(pattern); } @@ -148,7 +149,7 @@ void shouldGenerateLeftBoundaryForEnum() { @Test void shouldGenerateEmptyWhenLengthZero() { - String generated = StringGenerator.generateExactLength("^[A-Z]{3}$", 0); + String generated = StringGenerator.generateExactLength(new Schema(), "^[A-Z]{3}$", 0); Assertions.assertThat(generated).isEmpty(); } @@ -193,4 +194,42 @@ void shouldGenerateWhenPatternDoesNotHaveLengthButHasMinOrMax() { String generated = StringGenerator.generate("[A-Z]", 4, 4); Assertions.assertThat(generated).hasSize(4); } + + @Test + void shouldGenerateComplexEmailRegex() { + String regex = "^((([a-z]|\\d|[!#\\$%&'\\\\+\\-\\/=\\?\\^_`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+(\\.([a-z]|\\d|[!#\\$%&'\\\\+\\-\\/=\\?\\^`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+))|((\\x22)((((\\x20|\\x09)(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(\\\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]))))(((\\x20|\\x09)(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])([a-z]|\\d|-|||~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])*([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))\\.)+(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+|(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+([a-z]+|\\d|-|\\.{0,1}|_|~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])?([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))$"; + String generated = StringGenerator.generate(regex, 1, 6000); + Assertions.assertThat(generated).hasSizeBetween(1, 6000); + } + + @ParameterizedTest + @CsvSource(value = { + "[a-zA-Z0-9_]+@[a-zA-Z0-9_]+\\.[a-zA-Z]{2,4};\\w+@\\w+\\.[a-zA-Z]{2,4}", + "ab{1,}c{1}d{0,1};ab+c{1}d?"}, + delimiter = ';') + void shouldFlatten(String regex, String expected) { + String flattened = RegexFlattener.flattenRegex(regex); + Assertions.assertThat(flattened).isEqualTo(expected); + } + + @Test + void shouldGenerateEmail() { + Schema schema = new Schema<>(); + schema.addExtension(CatsModelUtils.X_CATS_FIELD_NAME, "email"); + schema.setPattern("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"); + String generated = StringGenerator.generateExactLength(schema, "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", 200); + + Assertions.assertThat(generated).hasSize(200); + } + + @Test + void shouldGenerateEmailBasedOnMixMax() { + Schema schema = new Schema<>(); + schema.setPattern("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"); + schema.setMaxLength(100); + schema.addExtension(CatsModelUtils.X_CATS_FIELD_NAME, "email"); + String generated = StringGenerator.generateValueBasedOnMinMax(schema); + + Assertions.assertThat(generated).hasSize(100); + } } diff --git a/src/test/java/com/endava/cats/util/CatsModeLUtilsTest.java b/src/test/java/com/endava/cats/util/CatsModeLUtilsTest.java index 1a9ea6268..599330dc1 100644 --- a/src/test/java/com/endava/cats/util/CatsModeLUtilsTest.java +++ b/src/test/java/com/endava/cats/util/CatsModeLUtilsTest.java @@ -1,6 +1,7 @@ package com.endava.cats.util; import io.quarkus.test.junit.QuarkusTest; +import io.swagger.v3.oas.models.media.Schema; import org.assertj.core.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -19,4 +20,24 @@ void shouldReturnSimpleReference(String ref, String expected) { Assertions.assertThat(result).isEqualTo(expected); } + + @ParameterizedTest + @CsvSource(value = {"email;[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,};true", + "email;[A-Z]+;false", + "uri;^(?:[a-z]+:)?//[^\\s]*;true", + "wrong;^(?:[a-z]+:)?//[^\\\\s]*;false", + "url;null;false", + "password;[a-zA-Z0-9._%+\\-\\!#\\?]+;true", + "wrong;[a-zA-Z0-9._%+\\-\\!#\\?]+;false", + "password;[a-zA-Z0-9._%+\\-\\!#]+;false", + "wrong;wrong;false"}, + delimiter = ';', nullValues = "null") + void shouldTestEmailAndUrlNameMatches(String name, String pattern, boolean expected) { + Schema schema = new Schema<>(); + schema.addExtension(CatsModelUtils.X_CATS_FIELD_NAME, name); + schema.setPattern(pattern); + boolean result = CatsModelUtils.isComplexRegex(schema); + + Assertions.assertThat(result).isEqualTo(expected); + } }