diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java index 74deef213f..106d06fd36 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java @@ -17,230 +17,43 @@ package org.citrusframework.openapi; import io.apicurio.datamodels.openapi.models.OasSchema; -import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; import org.citrusframework.CitrusSettings; import org.citrusframework.context.TestContext; -import org.citrusframework.openapi.model.OasModelHelper; -import org.citrusframework.openapi.util.OpenApiUtils; -import org.citrusframework.openapi.util.RandomModelBuilder; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.Collectors; - -import static java.lang.Boolean.TRUE; -import static java.lang.String.format; -import static org.citrusframework.openapi.OpenApiConstants.TYPE_INTEGER; -import static org.citrusframework.util.StringUtils.hasText; -import static org.citrusframework.util.StringUtils.quote; -import static org.springframework.util.CollectionUtils.isEmpty; +import org.citrusframework.openapi.random.RandomContext; /** * Generates proper payloads and validation expressions based on Open API specification rules. */ public abstract class OpenApiTestDataGenerator { - public static final BigDecimal THOUSAND = new BigDecimal(1000); - public static final BigDecimal HUNDRED = BigDecimal.valueOf(100); - public static final BigDecimal MINUS_THOUSAND = new BigDecimal(-1000); - private OpenApiTestDataGenerator() { // Static access only } - private static final Map SPECIAL_FORMATS = Map.of( - "email", "[a-z]{5,15}\\.?[a-z]{5,15}\\@[a-z]{5,15}\\.[a-z]{2}", - "uri", - "((http|https)://[a-zA-Z0-9-]+(\\.[a-zA-Z]{2,})+(/[a-zA-Z0-9-]+){1,6})|(file:///[a-zA-Z0-9-]+(/[a-zA-Z0-9-]+){1,6})", - "hostname", - "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])", - "ipv4", - "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)", - "ipv6", - "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); - /** * Creates payload from schema for outbound message. */ public static String createOutboundPayload(OasSchema schema, OpenApiSpecification specification) { - return createOutboundPayload(schema, - OasModelHelper.getSchemaDefinitions(specification.getOpenApiDoc(null)), specification); - } - - /** - * Creates payload from schema for outbound message. - */ - public static String createOutboundPayload(OasSchema schema, Map definitions, - OpenApiSpecification specification) { - return createOutboundPayload(schema, definitions, specification, new HashSet<>()); - } - - /** - * Creates payload from schema for outbound message. - */ - private static String createOutboundPayload(OasSchema schema, - Map definitions, - OpenApiSpecification specification, Set visitedRefSchemas) { - RandomModelBuilder randomModelBuilder = new RandomModelBuilder(); - createOutboundPayloadAsMap(randomModelBuilder, schema, definitions, specification, - visitedRefSchemas); - return randomModelBuilder.toString(); - } - - private static void createOutboundPayloadAsMap(RandomModelBuilder randomModelBuilder, - OasSchema schema, - Map definitions, - OpenApiSpecification specification, Set visitedRefSchemas) { - - if (hasText(schema.$ref) && visitedRefSchemas.contains(schema)) { - // Avoid recursion - return; - } - - if (OasModelHelper.isReferenceType(schema)) { - OasSchema resolved = definitions.get(OasModelHelper.getReferenceName(schema.$ref)); - createOutboundPayloadAsMap(randomModelBuilder, resolved, definitions, specification, - visitedRefSchemas); - return; - } - - if (OasModelHelper.isCompositeSchema(schema)) { - createComposedSchema(randomModelBuilder, schema, true, specification, - visitedRefSchemas); - return; - } - - switch (schema.type) { - case OpenApiConstants.TYPE_OBJECT -> - createRandomObjectSchemeMap(randomModelBuilder, schema, specification, - visitedRefSchemas); - case OpenApiConstants.TYPE_ARRAY -> - createRandomArrayValueMap(randomModelBuilder, schema, specification, - visitedRefSchemas); - case OpenApiConstants.TYPE_STRING, TYPE_INTEGER, OpenApiConstants.TYPE_NUMBER, OpenApiConstants.TYPE_BOOLEAN -> - createRandomValueExpressionMap(randomModelBuilder, schema, true); - default -> randomModelBuilder.appendSimple("\"\""); - } + RandomContext randomContext = new RandomContext(specification, true); + randomContext.generate(schema); + return randomContext.getRandomModelBuilder().write(); } /** * Use test variable with given name if present or create value from schema with random values */ - public static String createRandomValueExpression(String name, OasSchema schema, - Map definitions, - boolean quotes, OpenApiSpecification specification, TestContext context) { + public static String createRandomValueExpression(String name, OasSchema schema, OpenApiSpecification specification, + TestContext context) { + if (context.getVariables().containsKey(name)) { return CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX; } - return createRandomValueExpression(schema, definitions, quotes, specification); - } - - /** - * Create payload from schema with random values. - */ - public static String createRandomValueExpression(OasSchema schema, - Map definitions, boolean quotes, - OpenApiSpecification specification) { - if (OasModelHelper.isReferenceType(schema)) { - OasSchema resolved = definitions.get(OasModelHelper.getReferenceName(schema.$ref)); - return createRandomValueExpression(resolved, definitions, quotes, specification); - } - - StringBuilder payload = new StringBuilder(); - if (OasModelHelper.isObjectType(schema) || OasModelHelper.isArrayType(schema)) { - payload.append(createOutboundPayload(schema, definitions, specification)); - } else if (OpenApiConstants.TYPE_STRING.equals(schema.type)) { - if (quotes) { - payload.append("\""); - } - if (OpenApiConstants.FORMAT_DATE.equals(schema.format)) { - payload.append("citrus:currentDate('yyyy-MM-dd')"); - } else if (OpenApiConstants.FORMAT_DATE_TIME.equals(schema.format)) { - payload.append("citrus:currentDate('yyyy-MM-dd'T'hh:mm:ssZ')"); - } else if (hasText(schema.pattern)) { - payload.append("citrus:randomValue(").append(schema.pattern).append(")"); - } else if (!isEmpty(schema.enum_)) { - payload.append("citrus:randomEnumValue(").append( - schema.enum_.stream().map(value -> "'" + value + "'") - .collect(Collectors.joining(","))).append(")"); - } else if (OpenApiConstants.FORMAT_UUID.equals(schema.format)) { - payload.append("citrus:randomUUID()"); - } else { - if (schema.format != null && SPECIAL_FORMATS.containsValue(schema.format)) { - payload.append("citrus:randomValue('") - .append(SPECIAL_FORMATS.get(schema.format)).append("')"); - } else { - int length = 10; - if (schema.maxLength != null && schema.maxLength.intValue() > 0) { - length = schema.maxLength.intValue(); - } else if (schema.minLength != null && schema.minLength.intValue() > 0) { - length = schema.minLength.intValue(); - } - - payload.append("citrus:randomString(").append(length).append(")"); - } - } - - if (quotes) { - payload.append("\""); - } - } else if (OpenApiUtils.isAnyNumberScheme(schema)) { - payload.append("citrus:randomNumber(8)"); - } else if (OpenApiConstants.TYPE_BOOLEAN.equals(schema.type)) { - payload.append("citrus:randomEnumValue('true', 'false')"); - } else if (quotes) { - payload.append("\"\""); - } - - return payload.toString(); - } - - public static T createRawRandomValueExpression(OasSchema schema, - Map definitions, boolean quotes, - OpenApiSpecification specification, TestContext context) { - if (OasModelHelper.isReferenceType(schema)) { - OasSchema resolved = definitions.get(OasModelHelper.getReferenceName(schema.$ref)); - return createRawRandomValueExpression(resolved, definitions, quotes, specification, - context); - } - - StringBuilder payload = new StringBuilder(); - if (OpenApiConstants.TYPE_STRING.equals(schema.type) || OasModelHelper.isObjectType(schema) - || OasModelHelper.isArrayType(schema)) { - return (T) createRandomValueExpression(schema, definitions, quotes, specification); - } else if (OpenApiConstants.TYPE_NUMBER.equals(schema.type)) { - return (T) Double.valueOf( - context.replaceDynamicContentInString("citrus:randomNumber(8,2)")); - } else if ("integer".equals(schema.type)) { - return (T) Double.valueOf( - context.replaceDynamicContentInString("citrus:randomNumber(8)")); - } else if ("boolean".equals(schema.type)) { - return (T) Boolean.valueOf( - context.replaceDynamicContentInString("citrus:randomEnumValue('true', 'false')")); - } else if (quotes) { - payload.append("\"\""); - } - - return (T) payload.toString(); - } - - /** - * Checks if given field name is in list of required fields for this schema. - */ - private static boolean isRequired(OasSchema schema, String field) { - if (schema.required == null) { - return true; - } + RandomContext randomContext = new RandomContext(specification, false); + randomContext.generate(schema); + return randomContext.getRandomModelBuilder().write(); - return schema.required.contains(field); } /** @@ -249,398 +62,20 @@ private static boolean isRequired(OasSchema schema, String field) { */ public static String createRandomValueExpression(String name, OasSchema schema, TestContext context) { + if (context.getVariables().containsKey(name)) { return CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX; } - RandomModelBuilder randomModelBuilder = new RandomModelBuilder(); - createRandomValueExpressionMap(randomModelBuilder, schema, false); - return randomModelBuilder.toString(); - } - - public static String createRandomValueExpression(OasSchema schema, boolean quotes) { - RandomModelBuilder randomModelBuilder = new RandomModelBuilder(); - createRandomValueExpressionMap(randomModelBuilder, schema, quotes); - return randomModelBuilder.toString(); - } - - /** - * Create random value expression using functions according to schema type and format. - */ - private static void createRandomValueExpressionMap(RandomModelBuilder randomModelBuilder, - OasSchema schema, boolean quotes) { - - switch (schema.type) { - case OpenApiConstants.TYPE_STRING -> { - if (OpenApiConstants.FORMAT_DATE.equals(schema.format)) { - randomModelBuilder.appendSimple( - quote("citrus:currentDate('yyyy-MM-dd')", quotes)); - } else if (OpenApiConstants.FORMAT_DATE_TIME.equals(schema.format)) { - randomModelBuilder.appendSimple( - quote("citrus:currentDate('yyyy-MM-dd'T'hh:mm:ssZ')", quotes)); - } else if (hasText(schema.pattern)) { - randomModelBuilder.appendSimple( - quote("citrus:randomValue('" + schema.pattern + "')", quotes)); - } else if (!isEmpty(schema.enum_)) { - randomModelBuilder.appendSimple( - quote("citrus:randomEnumValue(" + (java.lang.String.join(",", schema.enum_)) - + ")", quotes)); - } else if (OpenApiConstants.FORMAT_UUID.equals(schema.format)) { - randomModelBuilder.appendSimple(quote("citrus:randomUUID()", quotes)); - } else { - - if (schema.format != null && SPECIAL_FORMATS.containsKey(schema.format)) { - randomModelBuilder.appendSimple(quote( - "citrus:randomValue('" + SPECIAL_FORMATS.get(schema.format) + "')", - quotes)); - } else { - long minLength = - schema.minLength != null && schema.minLength.longValue() > 0 - ? schema.minLength.longValue() : 10L; - long maxLength = - schema.maxLength != null && schema.maxLength.longValue() > 0 - ? schema.maxLength.longValue() : 10L; - long length = ThreadLocalRandom.current() - .nextLong(minLength, maxLength + 1); - randomModelBuilder.appendSimple( - quote("citrus:randomString(%s)".formatted(length), quotes)); - } - } - } - case OpenApiConstants.TYPE_NUMBER, TYPE_INTEGER -> - // No quotes for numbers - randomModelBuilder.appendSimple(createRandomNumber(schema)); - case OpenApiConstants.TYPE_BOOLEAN -> - // No quotes for boolean - randomModelBuilder.appendSimple("citrus:randomEnumValue('true', 'false')"); - default -> randomModelBuilder.appendSimple(""); - } - } - - private static String createRandomNumber(OasSchema schema) { - Number multipleOf = schema.multipleOf; - - boolean exclusiveMaximum = TRUE.equals(schema.exclusiveMaximum); - boolean exclusiveMinimum = TRUE.equals(schema.exclusiveMinimum); - - BigDecimal[] bounds = determineBounds(schema); - - BigDecimal minimum = bounds[0]; - BigDecimal maximum = bounds[1]; - - if (multipleOf != null) { - minimum = exclusiveMinimum ? incrementToExclude(minimum) : minimum; - maximum = exclusiveMaximum ? decrementToExclude(maximum) : maximum; - return createMultipleOf(minimum, maximum, new BigDecimal(multipleOf.toString())); - } - - return format( - "citrus:randomNumberGenerator('%d', '%s', '%s', '%s', '%s')", - determineDecimalPlaces(schema, minimum, maximum), - minimum, - maximum, - exclusiveMinimum, - exclusiveMaximum - ); + RandomContext randomContext = new RandomContext(); + randomContext.generate(schema); + return randomContext.getRandomModelBuilder().write(); } - /** - * Determines the number of decimal places to use based on the given schema and minimum/maximum values. - * For integer types, it returns 0. For other types, it returns the maximum number of decimal places - * found between the minimum and maximum values, with a minimum of 2 decimal places. - */ - private static int determineDecimalPlaces(OasSchema schema, BigDecimal minimum, - BigDecimal maximum) { - if (TYPE_INTEGER.equals(schema.type)) { - return 0; - } else { - return - Math.max(2, Math.max(findLeastSignificantDecimalPlace(minimum), - findLeastSignificantDecimalPlace(maximum))); - } + public static String createRandomValueExpression(OasSchema schema) { + RandomContext randomContext = new RandomContext(); + randomContext.generate(schema); + return randomContext.getRandomModelBuilder().write(); } - /** - * Determine some reasonable bounds for a random number - */ - private static BigDecimal[] determineBounds(OasSchema schema) { - Number maximum = schema.maximum; - Number minimum = schema.minimum; - Number multipleOf = schema.multipleOf; - - BigDecimal bdMinimum; - BigDecimal bdMaximum; - if (minimum == null && maximum == null) { - bdMinimum = MINUS_THOUSAND; - bdMaximum = THOUSAND; - } else if (minimum == null) { - // Determine min relative to max - bdMaximum = new BigDecimal(maximum.toString()); - - if (multipleOf != null) { - bdMinimum = bdMaximum.subtract(new BigDecimal(multipleOf.toString()).abs().multiply( - HUNDRED)); - } else { - bdMinimum = bdMaximum.subtract(bdMaximum.multiply(BigDecimal.valueOf(2)).max( - THOUSAND)); - } - } else if (maximum == null) { - // Determine max relative to min - bdMinimum = new BigDecimal(minimum.toString()); - if (multipleOf != null) { - bdMaximum = bdMinimum.add(new BigDecimal(multipleOf.toString()).abs().multiply( - HUNDRED)); - } else { - bdMaximum = bdMinimum.add(bdMinimum.multiply(BigDecimal.valueOf(2)).max(THOUSAND)); - } - } else { - bdMaximum = new BigDecimal(maximum.toString()); - bdMinimum = new BigDecimal(minimum.toString()); - } - - return new BigDecimal[]{bdMinimum, bdMaximum}; - } - - /** - * Create a random schema value - * - * @param schema the type to create - * @param visitedRefSchemas the schemas already created during descent, used to avoid recursion - */ - private static void createRandomValue(RandomModelBuilder randomModelBuilder, OasSchema schema, - boolean quotes, - OpenApiSpecification specification, Set visitedRefSchemas) { - if (hasText(schema.$ref) && visitedRefSchemas.contains(schema)) { - // Avoid recursion - return; - } - - if (OasModelHelper.isReferenceType(schema)) { - OasSchema resolved = OasModelHelper.getSchemaDefinitions( - specification.getOpenApiDoc(null)) - .get(OasModelHelper.getReferenceName(schema.$ref)); - createRandomValue(randomModelBuilder, resolved, quotes, specification, - visitedRefSchemas); - return; - } - - if (OasModelHelper.isCompositeSchema(schema)) { - createComposedSchema(randomModelBuilder, schema, quotes, specification, - visitedRefSchemas); - return; - } - - switch (schema.type) { - case OpenApiConstants.TYPE_OBJECT -> - createRandomObjectSchemeMap(randomModelBuilder, schema, specification, - visitedRefSchemas); - case OpenApiConstants.TYPE_ARRAY -> - createRandomArrayValueMap(randomModelBuilder, schema, specification, - visitedRefSchemas); - case OpenApiConstants.TYPE_STRING, TYPE_INTEGER, OpenApiConstants.TYPE_NUMBER, OpenApiConstants.TYPE_BOOLEAN -> - createRandomValueExpressionMap(randomModelBuilder, schema, quotes); - default -> { - if (quotes) { - randomModelBuilder.appendSimple("\"\""); - } else { - randomModelBuilder.appendSimple(""); - } - } - } - } - - private static void createRandomObjectSchemeMap(RandomModelBuilder randomModelBuilder, - OasSchema objectSchema, - OpenApiSpecification specification, Set visitedRefSchemas) { - - randomModelBuilder.object(() -> { - if (objectSchema.properties != null) { - for (Map.Entry entry : objectSchema.properties.entrySet()) { - if (specification.isGenerateOptionalFields() || isRequired(objectSchema, - entry.getKey())) { - randomModelBuilder.property(entry.getKey(), () -> - createRandomValue(randomModelBuilder, entry.getValue(), true, - specification, - visitedRefSchemas)); - } - } - } - }); - } - - private static void createComposedSchema(RandomModelBuilder randomModelBuilder, - OasSchema schema, boolean quotes, - OpenApiSpecification specification, Set visitedRefSchemas) { - - if (!isEmpty(schema.allOf)) { - createAllOff(randomModelBuilder, schema, quotes, specification, visitedRefSchemas); - } else if (schema instanceof Oas30Schema oas30Schema && !isEmpty(oas30Schema.anyOf)) { - createAnyOf(randomModelBuilder, oas30Schema, quotes, specification, visitedRefSchemas); - } else if (schema instanceof Oas30Schema oas30Schema && !isEmpty(oas30Schema.oneOf)) { - createOneOf(randomModelBuilder, oas30Schema.oneOf, quotes, specification, - visitedRefSchemas); - } - } - - private static void createOneOf(RandomModelBuilder randomModelBuilder, List schemas, - boolean quotes, - OpenApiSpecification specification, Set visitedRefSchemas) { - int schemaIndex = ThreadLocalRandom.current().nextInt(schemas.size()); - randomModelBuilder.object(() -> - createRandomValue(randomModelBuilder, schemas.get(schemaIndex), quotes, specification, - visitedRefSchemas)); - } - - private static void createAnyOf(RandomModelBuilder randomModelBuilder, Oas30Schema schema, - boolean quotes, - OpenApiSpecification specification, Set visitedRefSchemas) { - - randomModelBuilder.object(() -> { - boolean anyAdded = false; - for (OasSchema oneSchema : schema.anyOf) { - if (ThreadLocalRandom.current().nextBoolean()) { - createRandomValue(randomModelBuilder, oneSchema, quotes, specification, - visitedRefSchemas); - anyAdded = true; - } - } - - // Add at least one - if (!anyAdded) { - createOneOf(randomModelBuilder, schema.anyOf, quotes, specification, - visitedRefSchemas); - } - }); - } - - private static Map createAllOff(RandomModelBuilder randomModelBuilder, - OasSchema schema, boolean quotes, - OpenApiSpecification specification, Set visitedRefSchemas) { - Map allOf = new HashMap<>(); - - randomModelBuilder.object(() -> { - for (OasSchema oneSchema : schema.allOf) { - createRandomValue(randomModelBuilder, oneSchema, quotes, specification, - visitedRefSchemas); - } - }); - - return allOf; - } - - private static String createMultipleOf( - BigDecimal minimum, - BigDecimal maximum, - BigDecimal multipleOf - ) { - - BigDecimal lowestMultiple = lowestMultipleOf(minimum, multipleOf); - BigDecimal largestMultiple = largestMultipleOf(maximum, multipleOf); - - // Check if there are no valid multiples in the range - if (lowestMultiple.compareTo(largestMultiple) > 0) { - return null; - } - - BigDecimal range = largestMultiple.subtract(lowestMultiple) - .divide(multipleOf, RoundingMode.DOWN); - - // Don't go for incredible large numbers - if (range.compareTo(BigDecimal.valueOf(11)) > 0) { - range = BigDecimal.valueOf(10); - } - - long factor = 0; - if (range.compareTo(BigDecimal.ZERO) != 0) { - factor = ThreadLocalRandom.current().nextLong(1, range.longValue() + 1); - } - BigDecimal randomMultiple = lowestMultiple.add( - multipleOf.multiply(BigDecimal.valueOf(factor))); - randomMultiple = randomMultiple.setScale(findLeastSignificantDecimalPlace(multipleOf), - RoundingMode.HALF_UP); - - return randomMultiple.toString(); - } - - /** - * Create a random array value. - * - * @param schema the type to create - * @param visitedRefSchemas the schemas already created during descent, used to avoid recursion - */ - @SuppressWarnings("rawtypes") - private static void createRandomArrayValueMap(RandomModelBuilder randomModelBuilder, - OasSchema schema, - OpenApiSpecification specification, Set visitedRefSchemas) { - Object items = schema.items; - - if (items instanceof OasSchema itemsSchema) { - createRandomArrayValueWithSchemaItem(randomModelBuilder, schema, itemsSchema, - specification, - visitedRefSchemas); - } else { - throw new UnsupportedOperationException( - "Random array creation for an array with items having different schema is currently not supported!"); - } - } - - private static void createRandomArrayValueWithSchemaItem(RandomModelBuilder randomModelBuilder, - OasSchema schema, - OasSchema itemsSchema, OpenApiSpecification specification, - Set visitedRefSchemas) { - Number minItems = schema.minItems; - minItems = minItems != null ? minItems : 1; - Number maxItems = schema.maxItems; - maxItems = maxItems != null ? maxItems : 9; - - int nItems = ThreadLocalRandom.current() - .nextInt(minItems.intValue(), maxItems.intValue() + 1); - - randomModelBuilder.array(() -> { - for (int i = 0; i < nItems; i++) { - createRandomValue(randomModelBuilder, itemsSchema, true, specification, - visitedRefSchemas); - } - }); - } - - static BigDecimal largestMultipleOf(BigDecimal highest, BigDecimal multipleOf) { - RoundingMode roundingMode = - highest.compareTo(BigDecimal.ZERO) < 0 ? RoundingMode.UP : RoundingMode.DOWN; - BigDecimal factor = highest.divide(multipleOf, 0, roundingMode); - return multipleOf.multiply(factor); - } - - static BigDecimal lowestMultipleOf(BigDecimal lowest, BigDecimal multipleOf) { - RoundingMode roundingMode = - lowest.compareTo(BigDecimal.ZERO) < 0 ? RoundingMode.DOWN : RoundingMode.UP; - BigDecimal factor = lowest.divide(multipleOf, 0, roundingMode); - return multipleOf.multiply(factor); - } - - static BigDecimal incrementToExclude(BigDecimal val) { - return val.add(determineIncrement(val)) - .setScale(findLeastSignificantDecimalPlace(val), RoundingMode.HALF_DOWN); - } - - static BigDecimal decrementToExclude(BigDecimal val) { - return val.subtract(determineIncrement(val)) - .setScale(findLeastSignificantDecimalPlace(val), RoundingMode.HALF_DOWN); - } - - static BigDecimal determineIncrement(BigDecimal number) { - return BigDecimal.valueOf(1.0d / (Math.pow(10d, findLeastSignificantDecimalPlace(number)))); - } - - static int findLeastSignificantDecimalPlace(BigDecimal number) { - number = number.stripTrailingZeros(); - - String[] parts = number.toPlainString().split("\\."); - - if (parts.length == 1) { - return 0; - } - - return parts[1].length(); - } } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestValidationDataGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestValidationDataGenerator.java index 3f9e123679..95033bb26c 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestValidationDataGenerator.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestValidationDataGenerator.java @@ -175,7 +175,7 @@ private static String createValidationExpression(OasSchema schema) { } switch (schema.type) { - case "string" : + case OpenApiConstants.TYPE_STRING : if (schema.format != null && schema.format.equals("date")) { return "@matchesDatePattern('yyyy-MM-dd')@"; } else if (schema.format != null && schema.format.equals("date-time")) { @@ -190,7 +190,7 @@ private static String createValidationExpression(OasSchema schema) { } case OpenApiConstants.TYPE_NUMBER, OpenApiConstants.TYPE_INTEGER: return "@isNumber()@"; - case "boolean" : + case OpenApiConstants.TYPE_BOOLEAN : return "@matches(true|false)@"; default: return "@ignore@"; @@ -219,7 +219,7 @@ public static String createValidationRegex(@Nullable OasSchema schema) { } switch (schema.type) { - case "string" : + case OpenApiConstants.TYPE_STRING: if (schema.format != null && schema.format.equals("date")) { return "\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])"; } else if (schema.format != null && schema.format.equals("date-time")) { @@ -227,7 +227,7 @@ public static String createValidationRegex(@Nullable OasSchema schema) { } else if (hasText(schema.pattern)) { return schema.pattern; } else if (!isEmpty(schema.enum_)) { - return "(" + (String.join("|", schema.enum_)) + ")"; + return "(" + String.join("|", schema.enum_) + ")"; } else if (schema.format != null && schema.format.equals("uuid")) { return "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"; } else { @@ -235,9 +235,9 @@ public static String createValidationRegex(@Nullable OasSchema schema) { } case OpenApiConstants.TYPE_NUMBER: return "[0-9]+\\.?[0-9]*"; - case "integer" : + case OpenApiConstants.TYPE_INTEGER: return "[0-9]+"; - case "boolean" : + case OpenApiConstants.TYPE_BOOLEAN: return "(true|false)"; default: return ""; diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientRequestActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientRequestActionBuilder.java index 616e5b119b..a7722559f1 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientRequestActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientRequestActionBuilder.java @@ -132,7 +132,7 @@ private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter if (context.getVariables().containsKey(parameter.getName())) { parameterValue = "\\" + CitrusSettings.VARIABLE_PREFIX + parameter.getName() + CitrusSettings.VARIABLE_SUFFIX; } else { - parameterValue = OpenApiTestDataGenerator.createRandomValueExpression((OasSchema) parameter.schema, false); + parameterValue = OpenApiTestDataGenerator.createRandomValueExpression((OasSchema) parameter.schema); } randomizedPath = Pattern.compile("\\{" + parameter.getName() + "}") .matcher(randomizedPath) @@ -151,8 +151,7 @@ private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter private void setSpecifiedBody(TestContext context, OasOperation operation) { Optional body = OasModelHelper.getRequestBodySchema( openApiSpec.getOpenApiDoc(context), operation); - body.ifPresent(oasSchema -> httpMessage.setPayload(OpenApiTestDataGenerator.createOutboundPayload(oasSchema, - OasModelHelper.getSchemaDefinitions(openApiSpec.getOpenApiDoc(context)), openApiSpec))); + body.ifPresent(oasSchema -> httpMessage.setPayload(OpenApiTestDataGenerator.createOutboundPayload(oasSchema, openApiSpec))); } private void setSpecifiedQueryParameters(TestContext context, OasOperation operation) { @@ -179,9 +178,7 @@ private void setSpecifiedHeaders(TestContext context, OasOperation operation) { .forEach(param -> { if(httpMessage.getHeader(param.getName()) == null && !configuredHeaders.contains(param.getName())) { httpMessage.setHeader(param.getName(), - OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), (OasSchema) param.schema, - OasModelHelper.getSchemaDefinitions(openApiSpec.getOpenApiDoc( - context)), false, openApiSpec, context)); + OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), (OasSchema) param.schema, openApiSpec, context)); } }); } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerResponseActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerResponseActionBuilder.java index d5914d336e..3b4a522f92 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerResponseActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerResponseActionBuilder.java @@ -24,7 +24,6 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; -import io.apicurio.datamodels.openapi.models.OasDocument; import io.apicurio.datamodels.openapi.models.OasOperation; import io.apicurio.datamodels.openapi.models.OasResponse; import io.apicurio.datamodels.openapi.models.OasSchema; @@ -160,26 +159,24 @@ public Message build(TestContext context, String messageType) { } private void fillRandomData(OperationPathAdapter operationPathAdapter, TestContext context) { - OasDocument oasDocument = openApiSpec.getOpenApiDoc(context); if (operationPathAdapter.operation().responses != null) { - buildResponse(context, operationPathAdapter.operation(), oasDocument); + buildResponse(context, operationPathAdapter.operation()); } } - private void buildResponse(TestContext context, OasOperation operation, - OasDocument oasDocument) { + private void buildResponse(TestContext context, OasOperation operation) { Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration( openApiSpec.getOpenApiDoc(context), operation, statusCode, null); if (responseForRandomGeneration.isPresent()) { - buildRandomHeaders(context, oasDocument, responseForRandomGeneration.get()); - buildRandomPayload(operation, oasDocument, responseForRandomGeneration.get()); + buildRandomHeaders(context, responseForRandomGeneration.get()); + buildRandomPayload(operation, responseForRandomGeneration.get()); } } - private void buildRandomHeaders(TestContext context, OasDocument oasDocument, OasResponse response) { + private void buildRandomHeaders(TestContext context, OasResponse response) { Set filteredHeaders = new HashSet<>(getMessage().getHeaders().keySet()); Predicate> filteredHeadersPredicate = entry -> !filteredHeaders.contains( entry.getKey()); @@ -191,7 +188,6 @@ private void buildRandomHeaders(TestContext context, OasDocument oasDocument, Oa .forEach(entry -> addHeaderBuilder(new DefaultHeaderBuilder( singletonMap(entry.getKey(), createRandomValueExpression(entry.getKey(), entry.getValue(), - OasModelHelper.getSchemaDefinitions(oasDocument), false, openApiSpec, context)))) ); @@ -209,8 +205,7 @@ private void buildRandomHeaders(TestContext context, OasDocument oasDocument, Oa + CitrusSettings.VARIABLE_SUFFIX))))); } - private void buildRandomPayload(OasOperation operation, OasDocument oasDocument, - OasResponse response) { + private void buildRandomPayload(OasOperation operation, OasResponse response) { Optional> schemaForMediaTypeOptional; if (statusCode.startsWith("2")) { @@ -227,7 +222,7 @@ private void buildRandomPayload(OasOperation operation, OasDocument oasDocument, OasAdapter schemaForMediaType = schemaForMediaTypeOptional.get(); if (getMessage().getPayload() == null || ( getMessage().getPayload() instanceof String string && string.isEmpty())) { - createRandomPayload(getMessage(), oasDocument, schemaForMediaType); + createRandomPayload(getMessage(), schemaForMediaType); } // If we have a schema and a media type and the content type has not yet been set, do it. @@ -238,7 +233,7 @@ private void buildRandomPayload(OasOperation operation, OasDocument oasDocument, } } - private void createRandomPayload(HttpMessage message, OasDocument oasDocument, OasAdapter schemaForMediaType) { + private void createRandomPayload(HttpMessage message, OasAdapter schemaForMediaType) { if (schemaForMediaType.node() == null) { // No schema means no payload, no type @@ -246,13 +241,11 @@ private void createRandomPayload(HttpMessage message, OasDocument oasDocument, O } else { if (TEXT_PLAIN_VALUE.equals(schemaForMediaType.adapted())) { // Schema but plain text - message.setPayload(createOutboundPayload(schemaForMediaType.node(), - OasModelHelper.getSchemaDefinitions(oasDocument), openApiSpec)); + message.setPayload(createOutboundPayload(schemaForMediaType.node(), openApiSpec)); message.setHeader(HttpMessageHeaders.HTTP_CONTENT_TYPE, TEXT_PLAIN_VALUE); } else if (APPLICATION_JSON_VALUE.equals(schemaForMediaType.adapted())) { // Json Schema - message.setPayload(createOutboundPayload(schemaForMediaType.node(), - OasModelHelper.getSchemaDefinitions(oasDocument), openApiSpec)); + message.setPayload(createOutboundPayload(schemaForMediaType.node(), openApiSpec)); message.setHeader(HttpMessageHeaders.HTTP_CONTENT_TYPE, APPLICATION_JSON_VALUE); } } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomArrayGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomArrayGenerator.java new file mode 100644 index 0000000000..afee6f3f68 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomArrayGenerator.java @@ -0,0 +1,51 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import java.util.concurrent.ThreadLocalRandom; +import org.citrusframework.openapi.model.OasModelHelper; + +/** + * A generator for producing random arrays based on an OpenAPI schema. This class extends the + * {@link RandomGenerator} and provides a specific implementation for generating random arrays + * with constraints defined in the schema. + * + *

The generator supports arrays with items of a single schema type. If the array's items have + * different schemas, an {@link UnsupportedOperationException} will be thrown.

s + * + */ +public class RandomArrayGenerator extends RandomGenerator { + + @Override + public boolean handles(OasSchema other) { + return OasModelHelper.isArrayType(other); + } + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + Object items = schema.items; + + if (items instanceof OasSchema itemsSchema) { + createRandomArrayValueWithSchemaItem(randomContext, schema, itemsSchema); + } else { + throw new UnsupportedOperationException( + "Random array creation for an array with items having different schema is currently not supported!"); + } + } + + private static void createRandomArrayValueWithSchemaItem(RandomContext randomContext, + OasSchema schema, + OasSchema itemsSchema) { + + Number minItems = schema.minItems != null ? schema.minItems : 1; + Number maxItems = schema.maxItems != null ? schema.maxItems : 10; + + int nItems = ThreadLocalRandom.current() + .nextInt(minItems.intValue(), maxItems.intValue() + 1); + + randomContext.getRandomModelBuilder().array(() -> { + for (int i = 0; i < nItems; i++) { + randomContext.generate(itemsSchema); + } + }); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomCompositeGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomCompositeGenerator.java new file mode 100644 index 0000000000..6a7877ca39 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomCompositeGenerator.java @@ -0,0 +1,74 @@ +package org.citrusframework.openapi.random; + +import static org.springframework.util.CollectionUtils.isEmpty; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; +import org.citrusframework.openapi.model.OasModelHelper; + +/** + * A generator for producing random composite schemas based on an OpenAPI schema. This class extends + * the {@link RandomGenerator} and provides a specific implementation for generating composite schemas + * with constraints defined in the schema. + * + *

The generator supports composite schemas, which include `allOf`, `anyOf`, and `oneOf` constructs.

+ */ +public class RandomCompositeGenerator extends RandomGenerator { + + @Override + public boolean handles(OasSchema other) { + return OasModelHelper.isCompositeSchema(other); + } + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + + if (!isEmpty(schema.allOf)) { + createAllOff(randomContext, schema); + } else if (schema instanceof Oas30Schema oas30Schema && !isEmpty(oas30Schema.anyOf)) { + createAnyOf(randomContext, oas30Schema); + } else if (schema instanceof Oas30Schema oas30Schema && !isEmpty(oas30Schema.oneOf)) { + createOneOf(randomContext, oas30Schema.oneOf); + } + } + + private static void createOneOf(RandomContext randomContext, List schemas) { + int schemaIndex = ThreadLocalRandom.current().nextInt(schemas.size()); + randomContext.getRandomModelBuilder().object(() -> + randomContext.generate(schemas.get(schemaIndex))); + } + + private static void createAnyOf(RandomContext randomContext, Oas30Schema schema) { + + randomContext.getRandomModelBuilder().object(() -> { + boolean anyAdded = false; + for (OasSchema oneSchema : schema.anyOf) { + if (ThreadLocalRandom.current().nextBoolean()) { + randomContext.generate(oneSchema); + anyAdded = true; + } + } + + // Add at least one + if (!anyAdded) { + createOneOf(randomContext, schema.anyOf); + } + }); + } + + private static Map createAllOff(RandomContext randomContext, OasSchema schema) { + Map allOf = new HashMap<>(); + + randomContext.getRandomModelBuilder().object(() -> { + for (OasSchema oneSchema : schema.allOf) { + randomContext.generate(oneSchema); + } + }); + + return allOf; + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomConfiguration.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomConfiguration.java new file mode 100644 index 0000000000..c986a16eb3 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomConfiguration.java @@ -0,0 +1,63 @@ +package org.citrusframework.openapi.random; + +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_DATE; +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_DATE_TIME; +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_UUID; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_BOOLEAN; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_STRING; +import static org.citrusframework.openapi.random.RandomGenerator.ANY; +import static org.citrusframework.openapi.random.RandomGenerator.NULL_GENERATOR; +import static org.citrusframework.openapi.random.RandomGeneratorBuilder.builder; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Configuration class that initializes and manages a list of random generators + * for producing random data based on an OpenAPI schema. This class is a singleton + * and provides a static instance {@code RANDOM_CONFIGURATION} for global access. + */ +public class RandomConfiguration { + + private static final String EMAIL_PATTERN = "[a-z]{5,15}\\.?[a-z]{5,15}\\@[a-z]{5,15}\\.[a-z]{2}"; + private static final String URI_PATTERN = "((http|https)://[a-zA-Z0-9-]+(\\.[a-zA-Z]{2,})+(/[a-zA-Z0-9-]+){1,6})|(file:///[a-zA-Z0-9-]+(/[a-zA-Z0-9-]+){1,6})"; + private static final String HOSTNAME_PATTERN = "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])"; + private static final String IPV4_PATTERN = "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"; + private static final String IPV6_PATTERN = "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"; + + private final List randomGenerators; + + public static final RandomConfiguration RANDOM_CONFIGURATION = new RandomConfiguration(); + + private RandomConfiguration() { + List generators = new ArrayList<>(); + + // Note that the order of generators in the list is relevant, as the list is traversed from start to end, to find the first matching generator for a schema, and some generators match for less significant schemas. + generators.add(new RandomEnumGenerator()); + generators.add(builder(TYPE_STRING, FORMAT_DATE).build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:currentDate('yyyy-MM-dd')"))); + generators.add(builder(TYPE_STRING, FORMAT_DATE_TIME).build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:currentDate('yyyy-MM-dd'T'hh:mm:ssZ')"))); + generators.add(builder(TYPE_STRING, FORMAT_UUID).build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomUUID()"))); + generators.add(builder(TYPE_STRING, "email").build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('"+EMAIL_PATTERN+"')"))); + generators.add(builder(TYPE_STRING, "uri").build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('"+URI_PATTERN+"')"))); + generators.add(builder(TYPE_STRING, "hostname").build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('"+HOSTNAME_PATTERN+"')"))); + generators.add(builder(TYPE_STRING, "ipv4").build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('"+IPV4_PATTERN+"')"))); + generators.add(builder(TYPE_STRING,"ipv6").build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('"+IPV6_PATTERN+"')"))); + generators.add(builder().withType(TYPE_STRING).withPattern(ANY).build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('"+schema.pattern+"')"))); + generators.add(builder().withType(TYPE_BOOLEAN).build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimple("citrus:randomEnumValue('true', 'false')"))); + generators.add(new RandomStringGenerator()); + generators.add(new RandomCompositeGenerator()); + generators.add(new RandomNumberGenerator()); + generators.add(new RandomObjectGenerator()); + generators.add(new RandomArrayGenerator()); + + randomGenerators = Collections.unmodifiableList(generators); + } + + public RandomGenerator getGenerator(OasSchema oasSchema) { + return randomGenerators.stream().filter(generator -> generator.handles(oasSchema)) + .findFirst() + .orElse(NULL_GENERATOR); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomContext.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomContext.java new file mode 100644 index 0000000000..978d3b666b --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomContext.java @@ -0,0 +1,120 @@ +package org.citrusframework.openapi.random; + +import static org.citrusframework.openapi.random.RandomConfiguration.RANDOM_CONFIGURATION; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.model.OasModelHelper; + +/** + * Context class for generating random values based on an OpenAPI specification. + * This class manages the state and configuration needed to generate random values + * for various schemas defined in the OpenAPI specification. + */ +public class RandomContext { + + private final OpenApiSpecification specification; + + private Map schemaDefinitions; + + private final RandomModelBuilder randomModelBuilder; + + /** + * Cache for storing variable during random value generation. + */ + private final Map contextVariables = new HashMap<>(); + + /** + * Constructs a default RandomContext backed by no specification. Note, that this context can not + * resolve referenced schemas, as no specification is available. + * + */ + public RandomContext() { + this.randomModelBuilder = new RandomModelBuilder(false); + this.specification = null; + } + + /** + * Constructs a new RandomContext with the specified OpenAPI specification and quote option. + * + * @param specification the OpenAPI specification + * @param quote whether to quote the generated random values + */ + public RandomContext(OpenApiSpecification specification, boolean quote) { + this.specification = specification; + this.randomModelBuilder = new RandomModelBuilder(quote); + } + + /** + * Generates random values based on the specified schema. + * + * @param schema the schema to generate random values for + */ + public void generate(OasSchema schema) { + doGenerate(resolveSchema(schema)); + } + + void doGenerate(OasSchema resolvedSchema) { + RANDOM_CONFIGURATION.getGenerator(resolvedSchema).generate(this, resolvedSchema); + } + + /** + * Resolves a schema, handling reference schemas by fetching the referenced schema definition. + * + * @param schema the schema to resolve + * @return the resolved schema + */ + OasSchema resolveSchema(OasSchema schema) { + if (OasModelHelper.isReferenceType(schema)) { + if (schemaDefinitions == null) { + schemaDefinitions = getSchemaDefinitions(); + } + schema = schemaDefinitions.get(OasModelHelper.getReferenceName(schema.$ref)); + } + return schema; + } + + /** + * Returns the RandomModelBuilder associated with this context. + * + * @return the RandomModelBuilder + */ + public RandomModelBuilder getRandomModelBuilder() { + return randomModelBuilder; + } + + /** + * Returns the OpenAPI specification associated with this context. + * + * @return the OpenAPI specification + */ + public OpenApiSpecification getSpecification() { + return specification; + } + + /** + * Returns the schema definitions from the specified OpenAPI document. + * + * @return a map of schema definitions + */ + Map getSchemaDefinitions() { + return specification != null ?OasModelHelper.getSchemaDefinitions(specification.getOpenApiDoc(null)) : Collections.emptyMap(); + } + + /** + * Retrieves a context variable by key, computing its value if necessary using the provided mapping function. + * + * @param the type of the context variable + * @param key the key of the context variable + * @param mappingFunction the function to compute the value if it is not present + * @return the context variable value + */ + public T get(String key, Function mappingFunction) { + //noinspection unchecked + return (T) contextVariables.computeIfAbsent(key, mappingFunction); + } +} \ No newline at end of file diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/RandomElement.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomElement.java similarity index 98% rename from connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/RandomElement.java rename to connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomElement.java index 4a31733459..4c515a4256 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/RandomElement.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomElement.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.citrusframework.openapi.util; +package org.citrusframework.openapi.random; import java.util.ArrayList; import java.util.LinkedHashMap; diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomEnumGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomEnumGenerator.java new file mode 100644 index 0000000000..67b46e8be7 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomEnumGenerator.java @@ -0,0 +1,25 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import java.util.List; +import java.util.stream.Collectors; + +public class RandomEnumGenerator extends RandomGenerator { + + + @Override + public boolean handles(OasSchema other) { + return other.enum_ != null; + } + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + List anEnum = schema.enum_; + if (anEnum != null) { + String enumValues = schema.enum_.stream().map(value -> "'" + value + "'") + .collect(Collectors.joining(",")); + randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomEnumValue(%s)".formatted(enumValues)); + } + } + +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGenerator.java new file mode 100644 index 0000000000..9f74422e48 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGenerator.java @@ -0,0 +1,61 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import java.util.Objects; + +/** + * Abstract base class for generators that produce random data based on an OpenAPI schema. + * Subclasses must implement the {@link #generate} method to provide specific random data generation logic. + * + *

The class provides methods for determining if a generator can handle a given schema, + * based on the schema type, format, pattern, and enum constraints. + */ +public abstract class RandomGenerator { + + public static final String ANY = "$ANY$"; + + private final OasSchema schema; + + protected RandomGenerator() { + this.schema = null; + } + + protected RandomGenerator(OasSchema schema) { + this.schema = schema; + } + + public boolean handles(OasSchema other) { + if (other == null || schema == null) { + return false; + } + + if (ANY.equals(schema.type) || Objects.equals(schema.type, other.type)) { + if (schema.format != null) { + return (ANY.equals(schema.format) && other.format != null)|| Objects.equals(schema.format, other.format); + } + + if (schema.pattern != null) { + return (ANY.equals(schema.pattern) && other.pattern != null) || Objects.equals(schema.pattern, other.pattern); + } + + if (schema.enum_ != null && other.enum_ != null) { + return true; + } + + return true; + } + + return false; + } + + abstract void generate(RandomContext randomContext, OasSchema schema); + + public static final RandomGenerator NULL_GENERATOR = new RandomGenerator() { + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + // Do nothing + } + }; + +} \ No newline at end of file diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGeneratorBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGeneratorBuilder.java new file mode 100644 index 0000000000..8fbeac3ef4 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGeneratorBuilder.java @@ -0,0 +1,62 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import java.util.Collections; +import java.util.function.BiConsumer; + +/** + * A simple builder for building {@link java.util.random.RandomGenerator}s. + */ +public class RandomGeneratorBuilder { + + private final OasSchema schema = new Oas30Schema(); + + private RandomGeneratorBuilder() { + } + + static RandomGeneratorBuilder builder() { + return new RandomGeneratorBuilder(); + } + + static RandomGeneratorBuilder builder(String type, String format) { + return new RandomGeneratorBuilder().with(type, format); + } + + RandomGeneratorBuilder with(String type, String format) { + schema.type = type; + schema.format = format; + return this; + } + + + RandomGeneratorBuilder withType(String type) { + schema.type = type; + return this; + } + + RandomGeneratorBuilder withFormat(String format) { + schema.format = format; + return this; + } + + RandomGeneratorBuilder withPattern(String pattern) { + schema.pattern = pattern; + return this; + } + + RandomGeneratorBuilder withEnum() { + schema.enum_ = Collections.emptyList(); + return this; + } + + RandomGenerator build(BiConsumer consumer) { + return new RandomGenerator(schema) { + @Override + void generate(RandomContext randomContext, OasSchema schema) { + consumer.accept(randomContext, schema); + } + }; + } + +} \ No newline at end of file diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/RandomModelBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelBuilder.java similarity index 62% rename from connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/RandomModelBuilder.java rename to connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelBuilder.java index 43346b388a..c2ff6bbfea 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/RandomModelBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelBuilder.java @@ -14,22 +14,21 @@ * limitations under the License. */ -package org.citrusframework.openapi.util; +package org.citrusframework.openapi.random; import java.util.ArrayDeque; import java.util.Deque; -import org.citrusframework.openapi.util.RandomElement.RandomList; -import org.citrusframework.openapi.util.RandomElement.RandomObject; -import org.citrusframework.openapi.util.RandomElement.RandomValue; +import org.citrusframework.openapi.random.RandomElement.RandomList; +import org.citrusframework.openapi.random.RandomElement.RandomObject; +import org.citrusframework.openapi.random.RandomElement.RandomValue; /** - * RandomModelBuilder is a class for building random JSON models. It supports adding - * simple values, objects, properties, and arrays to the JSON structure. The final - * model can be converted to a JSON string using the `writeToJson` method. I + * RandomModelBuilder is a class for building random JSON models. It supports adding simple values, + * objects, properties, and arrays to the JSON structure. The final model can be converted to a JSON + * string using the `writeToJson` method. I *

- * The builder is able to build nested structures and can also handle native string, - * number, and boolean elements, represented as functions for later dynamic string - * conversion by Citrus. + * The builder is able to build nested structures and can also handle native string, number, and + * boolean elements, represented as functions for later dynamic string conversion by Citrus. *

* Example usage: *

@@ -48,22 +47,45 @@ public class RandomModelBuilder {
 
     final Deque deque = new ArrayDeque<>();
 
-    public RandomModelBuilder() {
+    private final boolean quote;
+
+    /**
+     * Creates a {@link RandomModelBuilder} in respective quoting mode.
+     * Quoting should be activated in case an object is created by the builder. In this case,
+     * all properties added by respective "quoted" methods, will be quoted.
+     *
+     * @param quote whether to run the builder in quoting mode or not.
+     */
+    public RandomModelBuilder(boolean quote) {
         deque.push(new RandomValue());
+        this.quote = quote;
     }
 
-    public String toString() {
+    public String write() {
         return RandomModelWriter.toString(this);
     }
 
-    public void appendSimple(String nativeValue) {
+    /**
+     * Append the simpleValue as is, no quoting
+     */
+    public void appendSimple(String simpleValue) {
         if (deque.isEmpty()) {
-            deque.push(new RandomValue(nativeValue));
+            deque.push(new RandomValue(simpleValue));
         } else {
-            deque.peek().push(nativeValue);
+            deque.peek().push(simpleValue);
         }
     }
 
+    /**
+     * If the builder is in quoting mode, the native value will be quoted, otherwise it will be
+     * added as ist.
+     *s
+     * @param simpleValue
+     */
+    public void appendSimpleQuoted(String simpleValue) {
+        appendSimple(quote(simpleValue));
+    }
+
     public void object(Runnable objectBuilder) {
         if (deque.isEmpty()) {
             throwIllegalState();
@@ -106,4 +128,8 @@ public void array(Runnable arrayBuilder) {
         deque.pop();
     }
 
+    public String quote(String text) {
+        return quote ? String.format("\"%s\"", text) : text;
+    }
+
 }
diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/RandomModelWriter.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelWriter.java
similarity index 97%
rename from connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/RandomModelWriter.java
rename to connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelWriter.java
index 11960d0973..2c37e621e5 100644
--- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/RandomModelWriter.java
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelWriter.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.citrusframework.openapi.util;
+package org.citrusframework.openapi.random;
 
 import static org.citrusframework.util.StringUtils.trimTrailingComma;
 
@@ -22,7 +22,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import org.citrusframework.openapi.util.RandomElement.RandomValue;
+import org.citrusframework.openapi.random.RandomElement.RandomValue;
 
 /**
  * Utility class for converting a {@link RandomModelBuilder} to its string representation.
diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomNumberGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomNumberGenerator.java
new file mode 100644
index 0000000000..a9c0742259
--- /dev/null
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomNumberGenerator.java
@@ -0,0 +1,155 @@
+package org.citrusframework.openapi.random;
+
+import static java.lang.Boolean.TRUE;
+import static java.lang.String.format;
+import static org.citrusframework.openapi.OpenApiConstants.TYPE_INTEGER;
+
+import io.apicurio.datamodels.openapi.models.OasSchema;
+import java.math.BigDecimal;
+import org.citrusframework.openapi.util.OpenApiUtils;
+
+/**
+ * A generator for producing random numbers based on an OpenAPI schema. This class extends the
+ * {@link RandomGenerator} and provides a specific implementation for generating random numbers with
+ * constraints defined in the schema.
+ *
+ * 

Supported constraints: + *

    + *
  • minimum: The minimum value for the generated number.
  • + *
  • maximum: The maximum value for the generated number.
  • + *
  • exclusiveMinimum: If true, the generated number will be strictly greater than the minimum.
  • + *
  • exclusiveMaximum: If true, the generated number will be strictly less than the maximum.
  • + *
  • multipleOf: The generated number will be a multiple of this value.
  • + *
+ * + *

The generator supports generating numbers for both integer and floating-point types, including + * int32, int64, double, and float. This support + * extends to the multipleOf constraint, ensuring that the generated numbers can be precise + * multiples of the specified value. + * + *

The generator determines the appropriate bounds and constraints based on the provided schema + * and generates a random number accordingly. + */ +public class RandomNumberGenerator extends RandomGenerator { + + public static final BigDecimal THOUSAND = new BigDecimal(1000); + public static final BigDecimal HUNDRED = java.math.BigDecimal.valueOf(100); + public static final BigDecimal MINUS_THOUSAND = new BigDecimal(-1000); + + @Override + public boolean handles(OasSchema other) { + return OpenApiUtils.isAnyNumberScheme(other); + } + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + + boolean exclusiveMaximum = TRUE.equals(schema.exclusiveMaximum); + boolean exclusiveMinimum = TRUE.equals(schema.exclusiveMinimum); + + BigDecimal[] bounds = determineBounds(schema); + + BigDecimal minimum = bounds[0]; + BigDecimal maximum = bounds[1]; + + if (schema.multipleOf != null) { + randomContext.getRandomModelBuilder().appendSimple(format( + "citrus:randomNumberGenerator('%d', '%s', '%s', '%s', '%s', '%s')", + determineDecimalPlaces(schema, minimum, maximum), + minimum, + maximum, + exclusiveMinimum, + exclusiveMaximum, + schema.multipleOf + )); + } else { + randomContext.getRandomModelBuilder().appendSimple(format( + "citrus:randomNumberGenerator('%d', '%s', '%s', '%s', '%s')", + determineDecimalPlaces(schema, minimum, maximum), + minimum, + maximum, + exclusiveMinimum, + exclusiveMaximum + )); + } + } + + /** + * Determines the number of decimal places to use based on the given schema and + * minimum/maximum/multipleOf values. For integer types, it returns 0. For other types, it + * returns the maximum number of decimal places found between the minimum and maximum values, + * with a minimum of 2 decimal places. + */ + private int determineDecimalPlaces(OasSchema schema, BigDecimal minimum, + BigDecimal maximum) { + if (TYPE_INTEGER.equals(schema.type)) { + return 0; + } else { + Number multipleOf = schema.multipleOf; + if (multipleOf != null) { + return findLeastSignificantDecimalPlace(new BigDecimal(multipleOf.toString())); + } + + return Math.max(2, Math.max(findLeastSignificantDecimalPlace(minimum), + findLeastSignificantDecimalPlace(maximum))); + + } + } + + /** + * Determine some reasonable bounds for a random number + */ + private static BigDecimal[] determineBounds(OasSchema schema) { + Number maximum = schema.maximum; + Number minimum = schema.minimum; + Number multipleOf = schema.multipleOf; + + BigDecimal bdMinimum; + BigDecimal bdMaximum; + + if (minimum == null && maximum == null) { + bdMinimum = MINUS_THOUSAND; + bdMaximum = THOUSAND; + } else if (minimum == null) { + bdMaximum = new BigDecimal(maximum.toString()); + bdMinimum = calculateMinRelativeToMax(bdMaximum, multipleOf); + } else if (maximum == null) { + bdMinimum = new BigDecimal(minimum.toString()); + bdMaximum = calculateMaxRelativeToMin(bdMinimum, multipleOf); + } else { + bdMinimum = new BigDecimal(minimum.toString()); + bdMaximum = new BigDecimal(maximum.toString()); + } + + return new BigDecimal[]{bdMinimum, bdMaximum}; + } + + static BigDecimal calculateMinRelativeToMax(BigDecimal max, Number multipleOf) { + if (multipleOf != null) { + return max.subtract(new BigDecimal(multipleOf.toString()).abs().multiply(HUNDRED)); + } else { + return max.subtract(max.multiply(BigDecimal.valueOf(2)).max(THOUSAND)); + } + } + + static BigDecimal calculateMaxRelativeToMin(BigDecimal min, Number multipleOf) { + if (multipleOf != null) { + return min.add(new BigDecimal(multipleOf.toString()).abs().multiply(HUNDRED)); + } else { + return min.add(min.multiply(BigDecimal.valueOf(2)).max(THOUSAND)); + } + } + + int findLeastSignificantDecimalPlace(BigDecimal number) { + number = number.stripTrailingZeros(); + + String[] parts = number.toPlainString().split("\\."); + + if (parts.length == 1) { + return 0; + } + + return parts[1].length(); + } + +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomObjectGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomObjectGenerator.java new file mode 100644 index 0000000000..8b7ef2b9d4 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomObjectGenerator.java @@ -0,0 +1,58 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import org.citrusframework.openapi.OpenApiConstants; +import org.citrusframework.openapi.util.OpenApiUtils; + +/** + * A generator for producing random objects based on an OpenAPI schema. This class extends + * the {@link RandomGenerator} and provides a specific implementation for generating objects + * with properties defined in the schema. + * + *

The generator supports object schemas and prevents recursion by keeping track of the + * schemas being processed.

+ */ +public class RandomObjectGenerator extends RandomGenerator { + + private static final String OBJECT_STACK = "OBJECT_STACK"; + + private static final OasSchema OBJECT_SCHEMA = new Oas30Schema(); + + static { + OBJECT_SCHEMA.type = OpenApiConstants.TYPE_OBJECT; + } + + public RandomObjectGenerator() { + super(OBJECT_SCHEMA); + } + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + + Deque objectStack = randomContext.get(OBJECT_STACK, k -> new ArrayDeque<>()); + + if (objectStack.contains(schema)) { + // If we have already created this schema, we are very likely in a recursion and need to stop. + return; + } + + objectStack.push(schema); + randomContext.getRandomModelBuilder().object(() -> { + if (schema.properties != null) { + for (Map.Entry entry : schema.properties.entrySet()) { + if (randomContext.getSpecification().isGenerateOptionalFields() || OpenApiUtils.isRequired(schema, + entry.getKey())) { + randomContext.getRandomModelBuilder().property(entry.getKey(), () -> + randomContext.generate(entry.getValue())); + } + } + } + }); + objectStack.pop(); + } + +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomStringGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomStringGenerator.java new file mode 100644 index 0000000000..cc30c220eb --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomStringGenerator.java @@ -0,0 +1,39 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.citrusframework.openapi.OpenApiConstants; + +/** + * A generator for producing random strings based on an OpenAPI schema. + * This class extends the {@link RandomGenerator} and provides a specific implementation + * for generating random strings with constraints defined in the schema. + */ +public class RandomStringGenerator extends RandomGenerator { + + private static final OasSchema STRING_SCHEMA = new Oas30Schema(); + + static { + STRING_SCHEMA.type = OpenApiConstants.TYPE_STRING; + } + + public RandomStringGenerator() { + super(STRING_SCHEMA); + } + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + int min = 1; + int max = 10; + + if (schema.minLength != null && schema.minLength.intValue() > 0) { + min = schema.minLength.intValue(); + } + + if (schema.maxLength != null && schema.maxLength.intValue() > 0) { + max = schema.maxLength.intValue(); + } + + randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomString(%s,MIXED,true,%s)".formatted(max, min)); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java index dd6e48deb7..ae4008111a 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java @@ -62,4 +62,15 @@ public static boolean isAnyNumberScheme(OasSchema schema) { ); } + /** + * Checks if given field name is in list of required fields for this schema. + */ + public static boolean isRequired(OasSchema schema, String field) { + if (schema.required == null) { + return true; + } + + return schema.required.contains(field); + } + } diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiTestDataGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiTestDataGeneratorTest.java index 59227c5cd7..6ac0dd9d60 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiTestDataGeneratorTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiTestDataGeneratorTest.java @@ -72,64 +72,13 @@ public static void beforeClass() { .getSchemaValidator(); } - @DataProvider(name = "findLeastSignificantDecimalPlace") - public static Object[][] findLeastSignificantDecimalPlace() { - return new Object[][]{ - {new BigDecimal("1234.5678"), 4}, - {new BigDecimal("123.567"), 3}, - {new BigDecimal("123.56"), 2}, - {new BigDecimal("123.5"), 1}, - {new BigDecimal("123.0"), 0}, - {new BigDecimal("123"), 0} - }; - } - - @Test(dataProvider = "findLeastSignificantDecimalPlace") - void findLeastSignificantDecimalPlace(BigDecimal number, int expectedSignificance) { - assertEquals(OpenApiTestDataGenerator.findLeastSignificantDecimalPlace(number), - expectedSignificance); - } - - @DataProvider(name = "incrementToExclude") - public static Object[][] incrementToExclude() { - return new Object[][]{ - {new BigDecimal("1234.678"), new BigDecimal("1234.679")}, - {new BigDecimal("1234.78"), new BigDecimal("1234.79")}, - {new BigDecimal("1234.8"), new BigDecimal("1234.9")}, - {new BigDecimal("1234.0"), new BigDecimal("1235")}, - {new BigDecimal("1234"), new BigDecimal("1235")}, - }; - } - - @Test(dataProvider = "incrementToExclude") - void incrementToExclude(BigDecimal value, BigDecimal expectedValue) { - assertEquals(OpenApiTestDataGenerator.incrementToExclude(value), expectedValue); - } - - @DataProvider(name = "decrementToExclude") - public static Object[][] decrementToExclude() { - return new Object[][]{ - {new BigDecimal("1234.678"), new BigDecimal("1234.677")}, - {new BigDecimal("1234.78"), new BigDecimal("1234.77")}, - {new BigDecimal("1234.8"), new BigDecimal("1234.7")}, - {new BigDecimal("1234.0"), new BigDecimal("1233")}, - {new BigDecimal("1234"), new BigDecimal("1233")}, - }; - } - - @Test(dataProvider = "decrementToExclude") - void decrementToExclude(BigDecimal value, BigDecimal expectedValue) { - assertEquals(OpenApiTestDataGenerator.decrementToExclude(value), expectedValue); - } - @Test void testUuidFormat() { Oas30Schema stringSchema = new Oas30Schema(); stringSchema.type = TYPE_STRING; stringSchema.format = FORMAT_UUID; - String uuidRandomValue = OpenApiTestDataGenerator.createRandomValueExpression(stringSchema, - false); + String uuidRandomValue = OpenApiTestDataGenerator.createRandomValueExpression(stringSchema); String finalUuidRandomValue = testContext.replaceDynamicContentInString(uuidRandomValue); Pattern uuidPattern = Pattern.compile( "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); @@ -300,8 +249,7 @@ void testPattern() { String exp = "[0-3]([a-c]|[e-g]{1,2})"; stringSchema.pattern = exp; - String randomValue = OpenApiTestDataGenerator.createRandomValueExpression(stringSchema, - false); + String randomValue = OpenApiTestDataGenerator.createRandomValueExpression(stringSchema); String finalRandomValue = testContext.replaceDynamicContentInString(randomValue); assertTrue(finalRandomValue.matches(exp), "Value '%s' does not match expression '%s'".formatted(finalRandomValue, exp)); @@ -315,18 +263,17 @@ public static Object[][] testPingApiSchemas() { //{"AnyOfType"}, //{"AllOfType"}, //{"PingRespType"}, - {"OneOfType"}, {"StringsType"}, {"DatesType"}, {"NumbersType"}, - {"MultipleOfType"}, {"PingReqType"}, {"Detail1"}, {"Detail2"}, {"BooleanType"}, {"EnumType"}, {"NestedType"}, + {"MultipleOfType"}, {"SimpleArrayType"}, {"ComplexArrayType"}, {"ArrayOfArraysType"}, @@ -354,13 +301,11 @@ void testPingApiSchemas(String schemaType) throws IOException { assertNotNull(randomValue); String finalJsonAsText = testContext.replaceDynamicContentInString(randomValue); - try { JsonNode valueNode = new ObjectMapper().readTree( testContext.replaceDynamicContentInString(finalJsonAsText)); ValidationReport validationReport = schemaValidator.validate(() -> valueNode, - swaggerValidationSchema, - "response.body"); + swaggerValidationSchema, null); String message = """ Json is invalid according to schema. @@ -430,7 +375,7 @@ void testArrayMaxItems() { arraySchema.items = stringSchema; - Pattern pattern = Pattern.compile("citrus:randomString\\(1[0-5]\\)"); + Pattern pattern = Pattern.compile("citrus:randomString\\(1[0-5],MIXED,true,10\\)"); for (int i = 0; i < 100; i++) { String randomArrayValue = OpenApiTestDataGenerator.createOutboundPayload(arraySchema, openApiSpecification); @@ -447,61 +392,4 @@ void testArrayMaxItems() { } } - @Test - public void testLowestMultipleOf() { - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(10)), BigDecimal.valueOf(-1000)); - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(-10)), BigDecimal.valueOf(-1000)); - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(11)), BigDecimal.valueOf(-990)); - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(-11)), BigDecimal.valueOf(-990)); - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(11.1234)), BigDecimal.valueOf(-989.9826)); - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(-11.1234)), BigDecimal.valueOf(-989.9826)); - - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(10)), BigDecimal.valueOf(1000)); - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(-10)), BigDecimal.valueOf(1000)); - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(11)), BigDecimal.valueOf(1001)); - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(-11)), BigDecimal.valueOf(1001)); - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(11.1234)), new BigDecimal("1001.1060")); - assertEquals(OpenApiTestDataGenerator.lowestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(-11.1234)), new BigDecimal("1001.1060")); - } - - @Test - public void testLargestMultipleOf() { - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(10)), BigDecimal.valueOf(-1000)); - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(-10)), BigDecimal.valueOf(-1000)); - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(11)), BigDecimal.valueOf(-1001)); - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(-11)), BigDecimal.valueOf(-1001)); - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(11.1234)), new BigDecimal("-1001.1060")); - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(-1000), - BigDecimal.valueOf(-11.1234)), new BigDecimal("-1001.1060")); - - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(10)), BigDecimal.valueOf(1000)); - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(-10)), BigDecimal.valueOf(1000)); - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(11)), BigDecimal.valueOf(990)); - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(-11)), BigDecimal.valueOf(990)); - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(11.1234)), new BigDecimal("989.9826")); - assertEquals(OpenApiTestDataGenerator.largestMultipleOf(BigDecimal.valueOf(1000), - BigDecimal.valueOf(-11.1234)), new BigDecimal("989.9826")); - } } diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/OasRandomConfigurationTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/OasRandomConfigurationTest.java new file mode 100644 index 0000000000..43fd6d628b --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/OasRandomConfigurationTest.java @@ -0,0 +1,179 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.List; + +import static org.citrusframework.openapi.OpenApiConstants.*; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; + +public class OasRandomConfigurationTest { + + private RandomConfiguration randomConfiguration; + + @BeforeClass + public void setUp() { + randomConfiguration = RandomConfiguration.RANDOM_CONFIGURATION; + } + + @Test + public void testGetGeneratorForDateFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = FORMAT_DATE; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForDateTimeFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = FORMAT_DATE_TIME; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForUUIDFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = FORMAT_UUID; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForEmailFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = "email"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForURIFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = "uri"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForHostnameFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = "hostname"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForIPv4Format() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = "ipv4"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForIPv6Format() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = "ipv6"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForBooleanType() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_BOOLEAN; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForStringType() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForNumberType() { + OasSchema schema = new Oas30Schema(); + schema.type = "number"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForObjectType() { + OasSchema schema = new Oas30Schema(); + schema.type = "object"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForArrayType() { + OasSchema schema = new Oas30Schema(); + schema.type = "array"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForEnum() { + OasSchema schema = new Oas30Schema(); + schema.enum_ = List.of("value1", "value2"); + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForNullSchema() { + OasSchema schema = new Oas30Schema(); + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertSame(generator, RandomGenerator.NULL_GENERATOR); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomArrayGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomArrayGeneratorTest.java new file mode 100644 index 0000000000..c361e0f1f6 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomArrayGeneratorTest.java @@ -0,0 +1,106 @@ +package org.citrusframework.openapi.random; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.citrusframework.openapi.OpenApiConstants; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class RandomArrayGeneratorTest { + + private RandomArrayGenerator generator; + private RandomContext mockContext; + private RandomModelBuilder builderSpy; + + @BeforeMethod + public void setUp() { + generator = new RandomArrayGenerator(); + mockContext = mock(); + + builderSpy = spy(new RandomModelBuilder(true)); + + when(mockContext.getRandomModelBuilder()).thenReturn(builderSpy); + } + + @Test + public void testGenerateArrayWithDefaultItems() { + Oas30Schema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_ARRAY; + + Oas30Schema itemsSchema = new Oas30Schema(); + itemsSchema.type = OpenApiConstants.TYPE_STRING; + schema.items = itemsSchema; + + generator.generate(mockContext, schema); + + verify(builderSpy, atLeastOnce()).array(any()); + verify(mockContext, atLeastOnce()).generate(any(OasSchema.class)); + } + + @Test + public void testGenerateArrayWithMinItems() { + Oas30Schema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_ARRAY; + schema.minItems = 5; + + Oas30Schema itemsSchema = new Oas30Schema(); + itemsSchema.type = OpenApiConstants.TYPE_STRING; + schema.items = itemsSchema; + + generator.generate(mockContext, schema); + + verify(builderSpy, atLeastOnce()).array(any()); + verify(mockContext, atLeast(5)).generate(any(OasSchema.class)); + } + + @Test + public void testGenerateArrayWithMaxItems() { + Oas30Schema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_ARRAY; + schema.maxItems = 3; + + Oas30Schema itemsSchema = new Oas30Schema(); + itemsSchema.type = OpenApiConstants.TYPE_STRING; + schema.items = itemsSchema; + + generator.generate(mockContext, schema); + + verify(builderSpy, atLeastOnce()).array(any()); + verify(mockContext, atMost(3)).generate(any(OasSchema.class)); + } + + @Test + public void testGenerateArrayWithMinMaxItems() { + Oas30Schema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_ARRAY; + schema.minItems = 2; + schema.maxItems = 5; + + Oas30Schema itemsSchema = new Oas30Schema(); + itemsSchema.type = OpenApiConstants.TYPE_STRING; + schema.items = itemsSchema; + + generator.generate(mockContext, schema); + + verify(builderSpy, atLeastOnce()).array(any()); + verify(mockContext, atLeast(2)).generate(any(OasSchema.class)); + verify(mockContext, atMost(5)).generate(any(OasSchema.class)); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void testGenerateArrayWithUnsupportedItems() { + Oas30Schema schema = new Oas30Schema(); + schema.items = new Object(); // Unsupported items type + + generator.generate(mockContext, schema); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomCompositeGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomCompositeGeneratorTest.java new file mode 100644 index 0000000000..ab2da578e7 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomCompositeGeneratorTest.java @@ -0,0 +1,90 @@ +package org.citrusframework.openapi.random; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.assertArg; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertTrue; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import java.util.Collections; +import java.util.List; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class RandomCompositeGeneratorTest { + + private RandomCompositeGenerator generator; + private RandomContext mockContext; + private RandomModelBuilder builderSpy; + + @BeforeMethod + public void setUp() { + generator = new RandomCompositeGenerator(); + mockContext = mock(RandomContext.class); + builderSpy = spy(new RandomModelBuilder(true)); + + when(mockContext.getRandomModelBuilder()).thenReturn(builderSpy); + } + + @Test + public void testHandlesCompositeSchema() { + Oas30Schema schema = new Oas30Schema(); + schema.allOf = Collections.singletonList(new Oas30Schema()); + + assertTrue(generator.handles(schema)); + } + + @Test + public void testGenerateAllOf() { + Oas30Schema schema = new Oas30Schema(); + schema.allOf = List.of(new Oas30Schema(), new Oas30Schema(), new Oas30Schema()); + + generator.generate(mockContext, schema); + + verify(builderSpy).object(any()); + verify(mockContext).generate(schema.allOf.get(0)); + verify(mockContext).generate(schema.allOf.get(1)); + verify(mockContext).generate(schema.allOf.get(2)); + } + + @Test + public void testGenerateAnyOf() { + Oas30Schema schema = new Oas30Schema(); + schema.anyOf = List.of(new Oas30Schema(), new Oas30Schema(), new Oas30Schema()); + + generator.generate(mockContext, schema); + + verify(builderSpy).object(any()); + verify(mockContext, atLeast(1)).generate(assertArg(arg -> schema.anyOf.contains(arg))); + verify(mockContext, atMost(3)).generate(assertArg(arg -> schema.anyOf.contains(arg))); + } + + @Test + public void testGenerateOneOf() { + Oas30Schema schema = new Oas30Schema(); + schema.oneOf = List.of(new Oas30Schema(), new Oas30Schema(), new Oas30Schema()); + + generator.generate(mockContext, schema); + + verify(builderSpy, atLeastOnce()).object(any()); + verify(mockContext).generate(any(OasSchema.class)); + } + + @Test + public void testGenerateWithNoCompositeSchema() { + Oas30Schema schema = new Oas30Schema(); + + generator.generate(mockContext, schema); + + verify(builderSpy, never()).object(any()); + verify(mockContext, never()).generate(any(OasSchema.class)); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomContextTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomContextTest.java new file mode 100644 index 0000000000..78d47bb52e --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomContextTest.java @@ -0,0 +1,76 @@ +package org.citrusframework.openapi.random; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertSame; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import java.util.HashMap; +import java.util.Map; +import org.citrusframework.openapi.OpenApiSpecification; +import org.springframework.test.util.ReflectionTestUtils; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class RandomContextTest { + + private OpenApiSpecification specificationMock; + + private RandomContext randomContext; + + private Map schemaDefinitions; + + @BeforeMethod + public void setUp() { + RandomModelBuilder randomModelBuilderMock = mock(); + specificationMock = mock(); + + schemaDefinitions =new HashMap<>(); + + randomContext = spy(new RandomContext(specificationMock, true)); + ReflectionTestUtils.setField(randomContext, "randomModelBuilder", randomModelBuilderMock); + + doReturn(schemaDefinitions).when(randomContext).getSchemaDefinitions(); + } + + @Test + public void testGenerateWithResolvedSchema() { + OasSchema oasSchema = new Oas30Schema(); + randomContext.generate(oasSchema); + verify(randomContext).doGenerate(oasSchema); + } + + @Test + public void testGenerateWithReferencedSchema() { + OasSchema referencedSchema = new Oas30Schema(); + schemaDefinitions.put("reference", referencedSchema); + OasSchema oasSchema = new Oas30Schema(); + oasSchema.$ref = "reference"; + + randomContext.generate(oasSchema); + verify(randomContext).doGenerate(referencedSchema); + } + + @Test + public void testGetRandomModelBuilder() { + assertNotNull(randomContext.getRandomModelBuilder()); + } + + @Test + public void testGetSpecification() { + assertEquals(randomContext.getSpecification(), specificationMock); + } + + @Test + public void testCacheVariable() { + HashMap cachedValue1 = randomContext.get("testKey", k -> new HashMap<>()); + HashMap cachedValue2 = randomContext.get("testKey", k -> new HashMap<>()); + + assertSame(cachedValue1, cachedValue2); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/util/RandomElementTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomElementTest.java similarity index 98% rename from connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/util/RandomElementTest.java rename to connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomElementTest.java index 2add7086c8..0f17bd143e 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/util/RandomElementTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomElementTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.citrusframework.openapi.util; +package org.citrusframework.openapi.random; import static org.testng.Assert.assertEquals; diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomEnumGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomEnumGeneratorTest.java new file mode 100644 index 0000000000..8d4d7a14e9 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomEnumGeneratorTest.java @@ -0,0 +1,77 @@ +package org.citrusframework.openapi.random; + +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import java.util.List; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class RandomEnumGeneratorTest { + + private RandomEnumGenerator generator; + private RandomContext mockContext; + private RandomModelBuilder mockBuilder; + private OasSchema mockSchema; + + @BeforeMethod + public void setUp() { + generator = new RandomEnumGenerator(); + mockContext = mock(RandomContext.class); + mockBuilder = mock(RandomModelBuilder.class); + mockSchema = mock(OasSchema.class); + + when(mockContext.getRandomModelBuilder()).thenReturn(mockBuilder); + } + + @Test + public void testHandlesWithEnum() { + mockSchema.enum_ = List.of("value1", "value2", "value3"); + + boolean result = generator.handles(mockSchema); + + assertTrue(result); + } + + @Test + public void testHandlesWithoutEnum() { + mockSchema.enum_ = null; + + boolean result = generator.handles(mockSchema); + + assertFalse(result); + } + + @Test + public void testGenerateWithEnum() { + mockSchema.enum_ = List.of("value1", "value2", "value3"); + + generator.generate(mockContext, mockSchema); + + verify(mockBuilder).appendSimpleQuoted("citrus:randomEnumValue('value1','value2','value3')"); + } + + @Test + public void testGenerateWithEmptyEnum() { + mockSchema.enum_ = List.of(); + + generator.generate(mockContext, mockSchema); + + verify(mockBuilder).appendSimpleQuoted("citrus:randomEnumValue()"); + } + + @Test + public void testGenerateWithNullEnum() { + mockSchema.enum_ = null; + + generator.generate(mockContext, mockSchema); + + verify(mockBuilder, never()).appendSimpleQuoted(anyString()); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorBuilderTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorBuilderTest.java new file mode 100644 index 0000000000..01d1c253e6 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorBuilderTest.java @@ -0,0 +1,92 @@ +package org.citrusframework.openapi.random; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import java.util.function.BiConsumer; +import org.springframework.test.util.ReflectionTestUtils; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + + +public class RandomGeneratorBuilderTest { + + private BiConsumer consumerMock; + private RandomContext contextMock; + private OasSchema schemaMock; + + @BeforeMethod + public void setUp() { + consumerMock = mock(); + contextMock = mock(); + schemaMock = mock(); + } + + @Test + public void testBuilderWithTypeAndFormat() { + String type = "type1"; + String format = "format1"; + + RandomGenerator generator = RandomGeneratorBuilder.builder(type, format).build(consumerMock); + OasSchema schema = (OasSchema) ReflectionTestUtils.getField(generator, "schema"); + assertNotNull(schema); + assertEquals(schema.type, type); + assertEquals(schema.format, format); + } + + @Test + public void testBuilderWithType() { + String type = "type1"; + + RandomGenerator generator = RandomGeneratorBuilder.builder().withType(type).build( + consumerMock); + OasSchema schema = (OasSchema) ReflectionTestUtils.getField(generator, "schema"); + assertNotNull(schema); + assertEquals(schema.type, type); + } + + @Test + public void testBuilderWithFormat() { + String format = "format1"; + + RandomGenerator generator = RandomGeneratorBuilder.builder().withFormat(format).build( + consumerMock); + OasSchema schema = (OasSchema) ReflectionTestUtils.getField(generator, "schema"); + assertNotNull(schema); + assertEquals(schema.format, format); + } + + @Test + public void testBuilderWithPattern() { + String pattern = "pattern1"; + + RandomGenerator generator = RandomGeneratorBuilder.builder().withPattern(pattern).build( + consumerMock); + OasSchema schema = (OasSchema) ReflectionTestUtils.getField(generator, "schema"); + assertNotNull(schema); + assertEquals(schema.pattern, pattern); + } + + @Test + public void testBuilderWithEnum() { + RandomGenerator generator = RandomGeneratorBuilder.builder().withEnum().build(consumerMock); + OasSchema schema = (OasSchema) ReflectionTestUtils.getField(generator, "schema"); + assertNotNull(schema); + assertNotNull(schema.enum_); + assertTrue(schema.enum_.isEmpty()); + } + + @Test + public void testBuildGenerator() { + RandomGenerator generator = RandomGeneratorBuilder.builder().build(consumerMock); + + generator.generate(contextMock, schemaMock); + + verify(consumerMock).accept(contextMock, schemaMock); + } + +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorTest.java new file mode 100644 index 0000000000..c788fff730 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorTest.java @@ -0,0 +1,152 @@ +package org.citrusframework.openapi.random; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import java.util.List; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class RandomGeneratorTest { + + private RandomGenerator generator; + private OasSchema mockSchema; + + @BeforeMethod + public void setUp() { + mockSchema = mock(OasSchema.class); + generator = new RandomGenerator(mockSchema) { + @Override + void generate(RandomContext randomContext, OasSchema schema) { + // Implementation not needed for this test + } + }; + } + + @Test + public void testHandlesWithMatchingTypeAndFormat() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.format = "format1"; + + mockSchema.type = "type1"; + mockSchema.format = "format1"; + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithTypeAny() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.format = "format1"; + + mockSchema.type = RandomGenerator.ANY; + mockSchema.format = "format1"; + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithFormatAny() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.format = "format1"; + + mockSchema.type = "type1"; + mockSchema.format = RandomGenerator.ANY; + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithPatternAny() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.pattern = "pattern1"; + + mockSchema.type = "type1"; + mockSchema.pattern = RandomGenerator.ANY; + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithMatchingPattern() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.pattern = "pattern1"; + + mockSchema.type = "type1"; + mockSchema.pattern = "pattern1"; + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithMatchingEnum() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.enum_ = List.of("value1", "value2"); + + mockSchema.type = "type1"; + mockSchema.enum_ = List.of("value1", "value2"); + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithNonMatchingType() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type2"; + otherSchema.format = "format1"; + + mockSchema.type = "type1"; + mockSchema.format = "format1"; + + assertFalse(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithNonMatchingFormat() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.format = "format2"; + + mockSchema.type = "type1"; + mockSchema.format = "format1"; + + assertFalse(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithNullSchema() { + assertFalse(generator.handles(null)); + } + + @Test + public void testHandlesWithNullGeneratorSchema() { + RandomGenerator generatorWithNullSchema = new RandomGenerator() { + @Override + void generate(RandomContext randomContext, OasSchema schema) { + // Do nothing + } + }; + + assertFalse(generatorWithNullSchema.handles(mockSchema)); + } + + @Test + public void testNullGenerator() { + RandomContext mockContext = mock(RandomContext.class); + + RandomGenerator.NULL_GENERATOR.generate(mockContext, mockSchema); + + verify(mockContext, never()).getRandomModelBuilder(); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/util/RandomModelBuilderTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomModelBuilderTest.java similarity index 74% rename from connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/util/RandomModelBuilderTest.java rename to connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomModelBuilderTest.java index f7570c2083..ef4deb3abc 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/util/RandomModelBuilderTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomModelBuilderTest.java @@ -14,32 +14,58 @@ * limitations under the License. */ -package org.citrusframework.openapi.util; +package org.citrusframework.openapi.random; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.expectThrows; + +import java.util.ArrayDeque; +import org.springframework.test.util.ReflectionTestUtils; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import static org.testng.Assert.*; - public class RandomModelBuilderTest { private RandomModelBuilder builder; @BeforeMethod public void setUp() { - builder = new RandomModelBuilder(); + builder = new RandomModelBuilder(true); } @Test public void testInitialState() { - String text = builder.toString(); + String text = builder.write(); assertEquals(text, ""); } @Test public void testAppendSimple() { builder.appendSimple("testValue"); - String json = builder.toString(); + String json = builder.write(); + assertEquals(json, "testValue"); + } + + @Test + public void testAppendSimpleToEmptyQueue() { + ReflectionTestUtils.setField(builder, "deque", new ArrayDeque<>()); + builder.appendSimple("testValue"); + String json = builder.write(); + assertEquals(json, "testValue"); + } + + @Test + public void testAppendSimpleQuoted() { + builder.appendSimpleQuoted("testValue"); + String json = builder.write(); + assertEquals(json, "\"testValue\""); + } + + @Test + public void testAppendSimpleQuotedIfNotQuoting() { + ReflectionTestUtils.setField(builder,"quote", false); + builder.appendSimpleQuoted("testValue"); + String json = builder.write(); assertEquals(json, "testValue"); } @@ -49,7 +75,7 @@ public void testObjectWithProperties() { builder.property("key1", () -> builder.appendSimple("\"value1\"")); builder.property("key2", () -> builder.appendSimple("\"value2\"")); }); - String json = builder.toString(); + String json = builder.write(); assertEquals(json, "{\"key1\": \"value1\",\"key2\": \"value2\"}"); } @@ -60,7 +86,7 @@ public void testNestedObject() { builder.property("innerKey", () -> builder.appendSimple("\"innerValue\"")) )) ); - String json = builder.toString(); + String json = builder.write(); assertEquals(json, "{\"outerKey\": {\"innerKey\": \"innerValue\"}}"); } @@ -71,7 +97,7 @@ public void testArray() { builder.appendSimple("\"value2\""); builder.appendSimple("\"value3\""); }); - String json = builder.toString(); + String json = builder.write(); assertEquals(json, "[\"value1\",\"value2\",\"value3\"]"); } @@ -85,7 +111,7 @@ public void testNestedArray() { }); builder.appendSimple("\"value2\""); }); - String json = builder.toString(); + String json = builder.write(); assertEquals(json, "[\"value1\",[\"nestedValue1\",\"nestedValue2\"],\"value2\"]"); } @@ -100,7 +126,7 @@ public void testMixedStructure() { })); builder.property("key2", () -> builder.appendSimple("\"value2\"")); }); - String json = builder.toString(); + String json = builder.write(); assertEquals(json, "{\"key1\": [\"value1\",{\"nestedKey\": \"nestedValue\"}],\"key2\": \"value2\"}"); } diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomNumberGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomNumberGeneratorTest.java new file mode 100644 index 0000000000..fbcb4f0479 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomNumberGeneratorTest.java @@ -0,0 +1,217 @@ +package org.citrusframework.openapi.random; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import java.math.BigDecimal; +import org.citrusframework.openapi.OpenApiConstants; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class RandomNumberGeneratorTest { + + private RandomNumberGenerator generator; + private RandomContext mockContext; + private RandomModelBuilder mockBuilder; + private OasSchema schema; + + @BeforeMethod + public void setUp() { + generator = new RandomNumberGenerator(); + mockContext = mock(RandomContext.class); + mockBuilder = mock(RandomModelBuilder.class); + schema = new Oas30Schema(); + + when(mockContext.getRandomModelBuilder()).thenReturn(mockBuilder); + } + + @Test + public void testGenerateDefaultBounds() { + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '-1000', '1000', 'false', 'false')"); + } + + @Test + public void testGenerateWithMinimum() { + schema.minimum = BigDecimal.valueOf(5); + generator.generate(mockContext, schema); + // Max is because of guessing a reasonable range + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '5', '1005', 'false', 'false')"); + } + + @Test + public void testGenerateWithMaximum() { + schema.maximum = BigDecimal.valueOf(15); + generator.generate(mockContext, schema); + // Min is because of guessing a reasonable range + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '-985', '15', 'false', 'false')"); + } + + @Test + public void testGenerateWithMinimumAndMaximum() { + schema.minimum = BigDecimal.valueOf(5); + schema.maximum = BigDecimal.valueOf(15); + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '5', '15', 'false', 'false')"); + } + + @Test + public void testGenerateWithExclusiveMinimum() { + schema.minimum = BigDecimal.valueOf(5); + schema.exclusiveMinimum = true; + generator.generate(mockContext, schema); + // Max is because of guessing a reasonable range + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '5', '1005', 'true', 'false')"); + } + + @Test + public void testGenerateWithExclusiveMaximum() { + schema.maximum = BigDecimal.valueOf(15); + schema.exclusiveMaximum = true; + generator.generate(mockContext, schema); + // Min is because of guessing a reasonable range + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '-985', '15', 'false', 'true')"); + } + + @Test + public void testGenerateWithMultipleOf() { + schema.multipleOf = BigDecimal.valueOf(5); + schema.minimum = BigDecimal.valueOf(10); + schema.maximum = BigDecimal.valueOf(50); + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('0', '10', '50', 'false', 'false', '5')"); + } + + @Test + public void testGenerateWithIntegerType() { + schema.type = "integer"; + schema.minimum = BigDecimal.valueOf(1); + schema.maximum = BigDecimal.valueOf(10); + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('0', '1', '10', 'false', 'false')"); + } + + @Test + public void testGenerateWithFloatType() { + schema.type = "number"; + schema.minimum = BigDecimal.valueOf(1.5); + schema.maximum = BigDecimal.valueOf(10.5); + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '1.5', '10.5', 'false', 'false')"); + } + + @Test + public void testGenerateWithMultipleOfFloat() { + schema.type = "number"; + schema.multipleOf = BigDecimal.valueOf(0.5); + schema.minimum = BigDecimal.valueOf(1.0); + schema.maximum = BigDecimal.valueOf(5.0); + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('1', '1.0', '5.0', 'false', 'false', '0.5')"); + } + + @Test + public void testCalculateMinRelativeToMaxWithMultipleOf() { + BigDecimal max = new BigDecimal("1000"); + Number multipleOf = new BigDecimal("10"); + + BigDecimal result = RandomNumberGenerator.calculateMinRelativeToMax(max, multipleOf); + + BigDecimal expected = max.subtract(new BigDecimal(multipleOf.toString()).abs().multiply(RandomNumberGenerator.HUNDRED)); + assertEquals(result, expected); + } + + @Test + public void testCalculateMinRelativeToMaxWithoutMultipleOf() { + BigDecimal max = new BigDecimal("1000"); + + BigDecimal result = RandomNumberGenerator.calculateMinRelativeToMax(max, null); + + BigDecimal expected = max.subtract(max.multiply(BigDecimal.valueOf(2)).max(RandomNumberGenerator.THOUSAND)); + assertEquals(result, expected); + } + + @Test + public void testCalculateMaxRelativeToMinWithMultipleOf() { + BigDecimal min = new BigDecimal("1000"); + Number multipleOf = new BigDecimal("10"); + + BigDecimal result = RandomNumberGenerator.calculateMaxRelativeToMin(min, multipleOf); + + BigDecimal expected = min.add(new BigDecimal(multipleOf.toString()).abs().multiply(RandomNumberGenerator.HUNDRED)); + assertEquals(result, expected); + } + + @Test + public void testCalculateMaxRelativeToMinWithoutMultipleOf() { + BigDecimal min = new BigDecimal("1000"); + + BigDecimal result = RandomNumberGenerator.calculateMaxRelativeToMin(min, null); + + BigDecimal expected = min.add(min.multiply(BigDecimal.valueOf(2)).max(RandomNumberGenerator.THOUSAND)); + assertEquals(result, expected); + } + + @Test + public void testHandlesWithIntegerType() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_INTEGER; + + assertTrue(generator.handles(schema)); + } + + @Test + public void testHandlesWithNumberType() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_NUMBER; + + assertTrue(generator.handles(schema)); + } + + @Test + public void testHandlesWithOtherType() { + OasSchema schema = new Oas30Schema(); + schema.type = "string"; + + assertFalse(generator.handles(schema)); + } + + @Test + public void testHandlesWithNullType() { + OasSchema schema = new Oas30Schema(); + schema.type = null; + + assertFalse(generator.handles(schema)); + } + + @Test + public void testHandlesWithNullSchema() { + assertFalse(generator.handles(null)); + } + + @DataProvider(name = "findLeastSignificantDecimalPlace") + public static Object[][] findLeastSignificantDecimalPlace() { + return new Object[][]{ + {new BigDecimal("1234.5678"), 4}, + {new BigDecimal("123.567"), 3}, + {new BigDecimal("123.56"), 2}, + {new BigDecimal("123.5"), 1}, + {new BigDecimal("123.0"), 0}, + {new BigDecimal("123"), 0} + }; + } + + @Test(dataProvider = "findLeastSignificantDecimalPlace") + void findLeastSignificantDecimalPlace(BigDecimal number, int expectedSignificance) { + assertEquals(generator.findLeastSignificantDecimalPlace(number), + expectedSignificance); + } + +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomObjectGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomObjectGeneratorTest.java new file mode 100644 index 0000000000..a32809f2f9 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomObjectGeneratorTest.java @@ -0,0 +1,134 @@ +package org.citrusframework.openapi.random; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import org.citrusframework.openapi.OpenApiConstants; +import org.citrusframework.openapi.OpenApiSpecification; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class RandomObjectGeneratorTest { + + private RandomObjectGenerator generator; + private RandomContext contextMock; + private RandomModelBuilder randomModelBuilderSpy; + private OpenApiSpecification specificationMock; + + @BeforeMethod + public void setUp() { + generator = new RandomObjectGenerator(); + contextMock = mock(); + specificationMock = mock(); + + randomModelBuilderSpy = spy(new RandomModelBuilder(true)); + when(contextMock.getRandomModelBuilder()).thenReturn(randomModelBuilderSpy); + when(contextMock.getSpecification()).thenReturn(specificationMock); + when(contextMock.get(eq("OBJECT_STACK"), any())).thenReturn(new ArrayDeque<>()); + + } + + @Test + public void testHandlesObjectType() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + + assertTrue(generator.handles(schema)); + } + + @Test + public void testDoesNotHandleNonObjectType() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_STRING; + + assertFalse(generator.handles(schema)); + } + + @Test + public void testGenerateObjectWithoutProperties() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + + generator.generate(contextMock, schema); + + verify(randomModelBuilderSpy).object(any()); + } + + @Test + public void testGenerateObjectWithProperties() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + schema.properties = new HashMap<>(); + OasSchema propertySchema = new Oas30Schema(); + schema.properties.put("property1", propertySchema); + + when(specificationMock.isGenerateOptionalFields()).thenReturn(true); + + generator.generate(contextMock, schema); + + verify(randomModelBuilderSpy).object(any()); + verify(randomModelBuilderSpy).property(eq("property1"), any()); + verify(contextMock).generate(propertySchema); + } + + @Test + public void testGenerateObjectWithRequiredProperties() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + schema.properties = new HashMap<>(); + OasSchema propertySchema = new Oas30Schema(); + schema.properties.put("property1", propertySchema); + schema.required = List.of("property1"); + + when(specificationMock.isGenerateOptionalFields()).thenReturn(false); + + generator.generate(contextMock, schema); + + verify(randomModelBuilderSpy).object(any()); + verify(randomModelBuilderSpy).property(eq("property1"), any()); + verify(contextMock).generate(propertySchema); + } + + @Test + public void testGenerateObjectWithOptionalProperties() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + schema.properties = new HashMap<>(); + OasSchema propertySchema = new Oas30Schema(); + schema.properties.put("property1", propertySchema); + schema.required = List.of(); + when(specificationMock.isGenerateOptionalFields()).thenReturn(false); + generator.generate(contextMock, schema); + + verify(randomModelBuilderSpy).object(any()); + verify(randomModelBuilderSpy, never()).property(eq("property1"), any()); + verify(contextMock, never()).generate(propertySchema); + } + + @Test + public void testGenerateObjectWithRecursion() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + Deque objectStack = new ArrayDeque<>(); + objectStack.push(schema); + + when(contextMock.get(eq("OBJECT_STACK"), any())).thenReturn(objectStack); + + generator.generate(contextMock, schema); + + verify(randomModelBuilderSpy, never()).object(any()); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomStringGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomStringGeneratorTest.java new file mode 100644 index 0000000000..445a318bd9 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomStringGeneratorTest.java @@ -0,0 +1,70 @@ +package org.citrusframework.openapi.random; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class RandomStringGeneratorTest { + + private RandomStringGenerator generator; + private RandomContext mockContext; + private RandomModelBuilder mockBuilder; + private OasSchema schema; + + @BeforeMethod + public void setUp() { + generator = new RandomStringGenerator(); + mockContext = mock(RandomContext.class); + mockBuilder = mock(RandomModelBuilder.class); + schema = new Oas30Schema(); + + when(mockContext.getRandomModelBuilder()).thenReturn(mockBuilder); + } + + @Test + public void testGenerateDefaultLength() { + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(10,MIXED,true,1)"); + } + + @Test + public void testGenerateWithMinLength() { + schema.minLength = 5; + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(10,MIXED,true,5)"); + } + + @Test + public void testGenerateWithMaxLength() { + schema.maxLength = 15; + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(15,MIXED,true,1)"); + } + + @Test + public void testGenerateWithMinAndMaxLength() { + schema.minLength = 3; + schema.maxLength = 8; + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(8,MIXED,true,3)"); + } + + @Test + public void testGenerateWithZeroMinLength() { + schema.minLength = 0; + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(10,MIXED,true,1)"); + } + + @Test + public void testGenerateWithZeroMaxLength() { + schema.maxLength = 0; + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(10,MIXED,true,1)"); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiRequestValidationProcessorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiRequestValidationProcessorTest.java index 80d901395d..04a0d47086 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiRequestValidationProcessorTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiRequestValidationProcessorTest.java @@ -85,7 +85,7 @@ public void shouldValidateHttpMessage() { processor.validate(httpMessageMock, contextMock); - verify(openApiRequestValidatorSpy, times(1)).validateRequest(operationPathAdapterMock, httpMessageMock); + verify(openApiRequestValidatorSpy).validateRequest(operationPathAdapterMock, httpMessageMock); } @Test @@ -100,7 +100,7 @@ public void shouldCallValidateRequest() { processor.validate(httpMessageMock, contextMock); - verify(openApiSpecificationMock, times(1)).getOperation(anyString(), + verify(openApiSpecificationMock).getOperation(anyString(), any(TestContext.class)); verify(openApiRequestValidatorSpy, times(0)).validateRequest(operationPathAdapterMock, httpMessageMock); } diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiRequestValidatorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiRequestValidatorTest.java index f79a8a9887..3716b503c7 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiRequestValidatorTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiRequestValidatorTest.java @@ -19,7 +19,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; @@ -113,8 +112,8 @@ public void shouldValidateRequestWithNoErrors() { openApiRequestValidator.validateRequest(operationPathAdapterMock, httpMessageMock); // Then - verify(openApiInteractionValidatorMock, times(1)).validateRequest(any(Request.class)); - verify(validationReportMock, times(1)).hasErrors(); + verify(openApiInteractionValidatorMock).validateRequest(any(Request.class)); + verify(validationReportMock).hasErrors(); } @Test(expectedExceptions = ValidationException.class) @@ -131,8 +130,8 @@ public void shouldValidateRequestWithErrors() { openApiRequestValidator.validateRequest(operationPathAdapterMock, httpMessageMock); // Then - verify(openApiInteractionValidatorMock, times(1)).validateRequest(any(Request.class)); - verify(validationReportMock, times(1)).hasErrors(); + verify(openApiInteractionValidatorMock).validateRequest(any(Request.class)); + verify(validationReportMock).hasErrors(); } @Test diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessorTest.java index 671560ba9f..2058af2558 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessorTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessorTest.java @@ -85,7 +85,7 @@ public void shouldCallValidateResponse() { processor.validate(httpMessageMock, contextMock); - verify(openApiResponseValidatorSpy, times(1)).validateResponse(operationPathAdapterMock, httpMessageMock); + verify(openApiResponseValidatorSpy).validateResponse(operationPathAdapterMock, httpMessageMock); } @Test @@ -100,7 +100,7 @@ public void shouldNotValidateWhenNoOperation() { processor.validate(httpMessageMock, contextMock); - verify(openApiSpecificationMock, times(1)).getOperation(anyString(), + verify(openApiSpecificationMock).getOperation(anyString(), any(TestContext.class)); verify(openApiResponseValidatorSpy, times(0)).validateResponse(operationPathAdapterMock, httpMessageMock); } diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidatorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidatorTest.java index e59f1f2821..cfccf76d92 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidatorTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidatorTest.java @@ -16,11 +16,24 @@ package org.citrusframework.openapi.validation; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + import com.atlassian.oai.validator.OpenApiInteractionValidator; import com.atlassian.oai.validator.model.Request.Method; import com.atlassian.oai.validator.model.Response; import com.atlassian.oai.validator.report.ValidationReport; import io.apicurio.datamodels.openapi.models.OasOperation; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; import org.citrusframework.exceptions.ValidationException; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.openapi.OpenApiSpecification; @@ -34,21 +47,6 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Map; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertTrue; - public class OpenApiResponseValidatorTest { @Mock @@ -120,8 +118,8 @@ public void shouldValidateWithNoErrors() { openApiResponseValidator.validateResponse(operationPathAdapterMock, httpMessageMock); // Then - verify(openApiInteractionValidatorMock, times(1)).validateResponse(anyString(), any(Method.class), any(Response.class)); - verify(validationReportMock, times(1)).hasErrors(); + verify(openApiInteractionValidatorMock).validateResponse(anyString(), any(Method.class), any(Response.class)); + verify(validationReportMock).hasErrors(); } @Test(expectedExceptions = ValidationException.class) @@ -141,8 +139,8 @@ public void shouldValidateWithErrors() { openApiResponseValidator.validateResponse(operationPathAdapterMock, httpMessageMock); // Then - verify(openApiInteractionValidatorMock, times(1)).validateResponse(anyString(), any(Method.class), any(Response.class)); - verify(validationReportMock, times(1)).hasErrors(); + verify(openApiInteractionValidatorMock).validateResponse(anyString(), any(Method.class), any(Response.class)); + verify(validationReportMock).hasErrors(); } @Test diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/DefaultFunctionLibrary.java b/core/citrus-base/src/main/java/org/citrusframework/functions/DefaultFunctionLibrary.java index eb70e1e543..caf65a1d31 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/DefaultFunctionLibrary.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/DefaultFunctionLibrary.java @@ -67,7 +67,7 @@ public DefaultFunctionLibrary() { getMembers().put("randomNumber", new RandomNumberFunction()); getMembers().put("randomNumberGenerator", new AdvancedRandomNumberFunction()); getMembers().put("randomString", new RandomStringFunction()); - getMembers().put("randomValue", new RandomPatternFunction()); + getMembers().put("randomPattern", new RandomPatternFunction()); getMembers().put("concat", new ConcatFunction()); getMembers().put("currentDate", new CurrentDateFunction()); getMembers().put("substring", new SubstringFunction()); diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/AdvancedRandomNumberFunction.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/AdvancedRandomNumberFunction.java index bc7252e057..6cde8506c4 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/core/AdvancedRandomNumberFunction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/AdvancedRandomNumberFunction.java @@ -21,8 +21,9 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; -import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.InvalidFunctionUsageException; import org.citrusframework.functions.Function; @@ -37,71 +38,72 @@ *
  • Min value: The minimum value for the generated random number (optional, default: Double.MIN_VALUE).
  • *
  • Max value: The maximum value for the generated random number (optional, default: Double.MAX_VALUE).
  • *
  • Exclude min: Whether to exclude the minimum value (optional, default: false).
  • - *
  • Exclude man: Whether to exclude the maximum value (optional, default: false).
  • + *
  • Exclude max: Whether to exclude the maximum value (optional, default: false).
  • + *
  • Multiple of: The generated number will be a multiple of this value (optional).
  • * *

    - * This function differs from the {@link RandomNumberFunction} in several key ways: - *

      + * This function differs from the {@link RandomNumberFunction} in several key ways: + *
        *
      • It allows to specify several aspects of a number (see above).
      • *
      • The length of the number is restricted to the range and precision of a double, whereas RandomNumberFunction can create arbitrarily long integer values.
      • - *
      + *
    */ public class AdvancedRandomNumberFunction implements Function { - /** - * Basic seed generating random number - */ - private static final Random generator = new Random(System.currentTimeMillis()); + public static final BigDecimal DEFAULT_MAX_VALUE = new BigDecimal(1000000); + public static final BigDecimal DEFAULT_MIN_VALUE = DEFAULT_MAX_VALUE.negate(); public String execute(List parameterList, TestContext context) { - int decimalPlaces = 0; - double minValue = -1000000; - double maxValue = 1000000; - boolean excludeMin = false; - boolean excludeMax = false; - if (parameterList == null) { throw new InvalidFunctionUsageException("Function parameters must not be null."); } - if (!parameterList.isEmpty()) { - decimalPlaces = parseParameter(1, parameterList.get(0), Integer.class, - Integer::parseInt); - if (decimalPlaces < 0) { - throw new InvalidFunctionUsageException( - "Invalid parameter definition. Decimal places must be a non-negative integer value."); - } + int decimalPlaces = getParameter(parameterList, 0, Integer.class, Integer::parseInt, 2); + if (decimalPlaces < 0) { + throw new InvalidFunctionUsageException( + "Decimal places must be a non-negative integer value."); } - if (parameterList.size() > 1) { - minValue = parseParameter(2, parameterList.get(1), Double.class, Double::parseDouble); + BigDecimal minValue = getParameter(parameterList, 1, BigDecimal.class, BigDecimal::new, + DEFAULT_MIN_VALUE); + BigDecimal maxValue = getParameter(parameterList, 2, BigDecimal.class, BigDecimal::new, + DEFAULT_MAX_VALUE); + if (minValue.compareTo(maxValue) > 0) { + throw new InvalidFunctionUsageException("Min value must be less than max value."); } - if (parameterList.size() > 2) { - maxValue = parseParameter(3, parameterList.get(2), Double.class, Double::parseDouble); - if (minValue > maxValue) { - throw new InvalidFunctionUsageException( - "Invalid parameter definition. Min value must be less than max value."); - } - } + boolean excludeMin = getParameter(parameterList, 3, Boolean.class, Boolean::parseBoolean, + false); + boolean excludeMax = getParameter(parameterList, 4, Boolean.class, Boolean::parseBoolean, + false); + BigDecimal multiple = getParameter(parameterList, 5, BigDecimal.class, BigDecimal::new, + null); - if (parameterList.size() > 3) { - excludeMin = parseParameter(4, parameterList.get(3), Boolean.class, - Boolean::parseBoolean); - } + return getRandomNumber(decimalPlaces, minValue, maxValue, excludeMin, excludeMax, multiple); + } - if (parameterList.size() > 4) { - excludeMax = parseParameter(5, parameterList.get(4), Boolean.class, - Boolean::parseBoolean); + private T getParameter(List params, int index, Class type, + java.util.function.Function parser, T defaultValue) { + if (index < params.size()) { + String param = params.get(index); + return "null".equals(param) ? defaultValue + : parseParameter(index + 1, param, type, parser); } - - return getRandomNumber(decimalPlaces, minValue, maxValue, excludeMin, excludeMax); + return defaultValue; } private T parseParameter(int index, String text, Class type, java.util.function.Function parseFunction) { + T value; try { - return parseFunction.apply(text); + + value = parseFunction.apply(text); + if (value == null) { + throw new CitrusRuntimeException( + "Text '%s' could not be parsed to '%s'. Resulting value is null".formatted(text, + type.getSimpleName())); + } + return value; } catch (Exception e) { throw new InvalidFunctionUsageException( format("Invalid parameter at index %d. %s must be parsable to %s.", index, text, @@ -112,33 +114,114 @@ private T parseParameter(int index, String text, Class type, /** * Static number generator method. */ - private String getRandomNumber(int decimalPlaces, double minValue, double maxValue, - boolean excludeMin, boolean excludeMax) { - double adjustment = Math.pow(10, -decimalPlaces); + private String getRandomNumber(int decimalPlaces, BigDecimal minValue, BigDecimal maxValue, + boolean excludeMin, boolean excludeMax, BigDecimal multiple) { - if (excludeMin) { - minValue += adjustment; - } + minValue = excludeMin ? incrementToExclude(minValue) : minValue; + maxValue = excludeMax ? decrementToExclude(maxValue) : maxValue; - if (excludeMax) { - maxValue -= adjustment; - } + BigDecimal range = maxValue.subtract(minValue); - BigDecimal range = BigDecimal.valueOf(maxValue).subtract(BigDecimal.valueOf(minValue)); + BigDecimal randomValue; + if (multiple != null) { + randomValue = createMultipleOf(minValue, maxValue, multiple); + } else { + randomValue = createRandomValue(minValue, range, + ThreadLocalRandom.current().nextDouble()); + randomValue = randomValue.setScale(decimalPlaces, RoundingMode.HALF_UP); + } - double randomValue = getRandomValue(minValue, range, generator.nextDouble()); - BigDecimal bd = new BigDecimal(Double.toString(randomValue)); - bd = bd.setScale(2, RoundingMode.HALF_UP); + if (randomValue == null) { + // May only happen if multiple is out of range of min/max + return format("%s", Double.POSITIVE_INFINITY); + } return decimalPlaces == 0 ? - format("%s", bd.longValue()) : - format(format("%%.%sf", decimalPlaces), bd.doubleValue()); + format("%s", randomValue.longValue()) : + format(format("%%.%sf", decimalPlaces), randomValue.doubleValue()); } - double getRandomValue(double minValue, BigDecimal range, double random) { + // Pass in random for testing + BigDecimal createRandomValue(BigDecimal minValue, BigDecimal range, double random) { BigDecimal offset = range.multiply(BigDecimal.valueOf(random)); - BigDecimal value = BigDecimal.valueOf(minValue).add(offset); - return value.compareTo(BigDecimal.valueOf(Double.MAX_VALUE)) > 0 ? Double.MAX_VALUE : value.doubleValue(); + BigDecimal value = minValue.add(offset); + return value.compareTo(BigDecimal.valueOf(Double.MAX_VALUE)) > 0 ? BigDecimal.valueOf( + Double.MAX_VALUE) : value; + } + + private BigDecimal largestMultipleOf(BigDecimal highest, BigDecimal multipleOf) { + RoundingMode roundingMode = + highest.compareTo(BigDecimal.ZERO) < 0 ? RoundingMode.UP : RoundingMode.DOWN; + BigDecimal factor = highest.divide(multipleOf, 0, roundingMode); + return multipleOf.multiply(factor); + } + + private BigDecimal lowestMultipleOf(BigDecimal lowest, BigDecimal multipleOf) { + RoundingMode roundingMode = + lowest.compareTo(java.math.BigDecimal.ZERO) < 0 ? RoundingMode.DOWN : RoundingMode.UP; + BigDecimal factor = lowest.divide(multipleOf, 0, roundingMode); + return multipleOf.multiply(factor); + } + + private BigDecimal incrementToExclude(BigDecimal val) { + return val.add(determineIncrement(val)) + .setScale(findLeastSignificantDecimalPlace(val), RoundingMode.HALF_DOWN); + } + + private BigDecimal decrementToExclude(BigDecimal val) { + return val.subtract(determineIncrement(val)) + .setScale(findLeastSignificantDecimalPlace(val), RoundingMode.HALF_DOWN); + } + + private BigDecimal determineIncrement(BigDecimal number) { + return java.math.BigDecimal.valueOf( + 1.0d / (Math.pow(10d, findLeastSignificantDecimalPlace(number)))); + } + + private int findLeastSignificantDecimalPlace(BigDecimal number) { + number = number.stripTrailingZeros(); + + String[] parts = number.toPlainString().split("\\."); + + if (parts.length == 1) { + return 0; + } + + return parts[1].length(); + } + + private BigDecimal createMultipleOf( + BigDecimal minimum, + BigDecimal maximum, + BigDecimal multipleOf + ) { + + BigDecimal lowestMultiple = lowestMultipleOf(minimum, multipleOf); + BigDecimal largestMultiple = largestMultipleOf(maximum, multipleOf); + + // Check if there are no valid multiples in the range + if (lowestMultiple.compareTo(largestMultiple) > 0) { + return null; + } + + BigDecimal range = largestMultiple.subtract(lowestMultiple) + .divide(multipleOf, RoundingMode.DOWN); + + // Don't go for incredible large numbers + if (range.compareTo(BigDecimal.valueOf(11)) > 0) { + range = BigDecimal.valueOf(10); + } + + long factor = 0; + if (range.compareTo(BigDecimal.ZERO) != 0) { + factor = ThreadLocalRandom.current().nextLong(1, range.longValue() + 1); + } + BigDecimal randomMultiple = lowestMultiple.add( + multipleOf.multiply(BigDecimal.valueOf(factor))); + randomMultiple = randomMultiple.setScale(findLeastSignificantDecimalPlace(multipleOf), + RoundingMode.HALF_UP); + + return randomMultiple; } } \ No newline at end of file diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomPatternFunction.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomPatternFunction.java index 4720921c7e..377b2d5fe7 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomPatternFunction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomPatternFunction.java @@ -25,9 +25,8 @@ import org.citrusframework.functions.Function; /** - * The RandomPatternFunction class implements the Function interface. This function generates a - * random string based on a provided regular expression pattern. It uses the Generex library to - * generate the random string. + * The RandomPatternFunction generates a random string based on a provided regular expression pattern. + * It uses the Generex library to generate the random string. *

    * Note: The Generex library has limitations in its ability to generate all possible expressions * from a given regular expression. It may not support certain complex regex features or produce all diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomStringFunction.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomStringFunction.java index 099c13df55..326471a2b5 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomStringFunction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomStringFunction.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.InvalidFunctionUsageException; import org.citrusframework.functions.Function; @@ -32,7 +33,7 @@ * */ public class RandomStringFunction implements Function { - private static Random generator = new Random(System.currentTimeMillis()); + private static final Random generator = new Random(System.currentTimeMillis()); private static final char[] ALPHABET_UPPER = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', @@ -67,12 +68,13 @@ public String execute(List parameterList, TestContext context) { int numberOfLetters; String notationMethod = MIXED; boolean includeNumbers = false; + int minNumberOfLetters = -1; if (parameterList == null || parameterList.isEmpty()) { throw new InvalidFunctionUsageException("Function parameters must not be empty"); } - if (parameterList.size() > 3) { + if (parameterList.size() > 4) { throw new InvalidFunctionUsageException("Too many parameters for function"); } @@ -89,12 +91,16 @@ public String execute(List parameterList, TestContext context) { includeNumbers = parseBoolean(parameterList.get(2)); } + if (parameterList.size() > 3) { + minNumberOfLetters = parseInt(parameterList.get(3)); + } + if (notationMethod.equals(UPPERCASE)) { - return getRandomString(numberOfLetters, ALPHABET_UPPER, includeNumbers); + return getRandomString(numberOfLetters, ALPHABET_UPPER, includeNumbers, minNumberOfLetters); } else if (notationMethod.equals(LOWERCASE)) { - return getRandomString(numberOfLetters, ALPHABET_LOWER, includeNumbers); + return getRandomString(numberOfLetters, ALPHABET_LOWER, includeNumbers, minNumberOfLetters); } else { - return getRandomString(numberOfLetters, ALPHABET_MIXED, includeNumbers); + return getRandomString(numberOfLetters, ALPHABET_MIXED, includeNumbers, minNumberOfLetters); } } @@ -105,7 +111,7 @@ public String execute(List parameterList, TestContext context) { * @param includeNumbers * @return */ - public static String getRandomString(int numberOfLetters, char[] alphabet, boolean includeNumbers) { + public static String getRandomString(int numberOfLetters, char[] alphabet, boolean includeNumbers, int minNumberOfLetters) { StringBuilder builder = new StringBuilder(); int upperRange = alphabet.length - 1; @@ -117,6 +123,11 @@ public static String getRandomString(int numberOfLetters, char[] alphabet, boole upperRange += NUMBERS.length; } + if (minNumberOfLetters > -1) { + numberOfLetters = ThreadLocalRandom.current() + .nextInt(minNumberOfLetters, numberOfLetters + 1); + } + for (int i = 1; i < numberOfLetters; i++) { int letterIndex = generator.nextInt(upperRange); diff --git a/core/citrus-base/src/main/java/org/citrusframework/util/StringUtils.java b/core/citrus-base/src/main/java/org/citrusframework/util/StringUtils.java index 3d6be1f9b3..21cf207378 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/util/StringUtils.java +++ b/core/citrus-base/src/main/java/org/citrusframework/util/StringUtils.java @@ -63,7 +63,7 @@ public static String appendSegmentToUrlPath(String path, String segment) { } public static String quote(String text, boolean quote) { - return quote ? String.format("\"%s\"", text) : text; + return quote ? "\"" + text + "\"" : text; } /** diff --git a/core/citrus-base/src/test/java/org/citrusframework/functions/core/AdvancedRandomNumberFunctionTest.java b/core/citrus-base/src/test/java/org/citrusframework/functions/core/AdvancedRandomNumberFunctionTest.java new file mode 100644 index 0000000000..82cf51cfa7 --- /dev/null +++ b/core/citrus-base/src/test/java/org/citrusframework/functions/core/AdvancedRandomNumberFunctionTest.java @@ -0,0 +1,416 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.functions.core; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.math.BigDecimal; +import java.util.List; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.InvalidFunctionUsageException; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class AdvancedRandomNumberFunctionTest { + + private AdvancedRandomNumberFunction function; + private TestContext context; + + @BeforeMethod + public void setUp() { + function = new AdvancedRandomNumberFunction(); + context = new TestContext(); + } + + @Test + public void testRandomNumberWithNullParameter() { + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(null, context)); + assertEquals(exception.getMessage(), + "Function parameters must not be null."); + } + + @Test + public void testRandomNumberWithDefaultValues() { + List params = List.of(); + String result = function.execute(params, context); + assertNotNull(result); + assertTrue(result.matches("-?\\d*\\.\\d{2}")); + } + + @Test + public void testRandomNumberWithDecimalPlaces() { + List params = List.of("2"); + String result = function.execute(params, context); + assertNotNull(result); + assertTrue(result.matches("-?\\d*\\.\\d{2}"), "result does not match pattern: " + result); + } + + @Test + public void testRandomNumberWithinRange() { + List params = List.of("2", "10.5", "20.5"); + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= 10.5 && randomValue <= 20.5); + } + + @Test + public void testRandomNumberIncludesMin() { + List params = List.of("1", "10.5", "20.5"); + function = new AdvancedRandomNumberFunction() { + @Override + BigDecimal createRandomValue(BigDecimal minValue, BigDecimal range, double random) { + random = 0.0; + return super.createRandomValue(minValue, range, random); + } + }; + String result = function.execute(params, context); + assertEquals(result, "10.5"); + } + + @Test + public void testRandomNumberIncludesMax() { + List params = List.of("1", "10.5", "20.5"); + function = new AdvancedRandomNumberFunction() { + @Override + BigDecimal createRandomValue(BigDecimal minValue, BigDecimal range, double random) { + random = 1.0; + return super.createRandomValue(minValue, range, random); + } + }; + String result = function.execute(params, context); + assertEquals(result, "20.5"); + } + + @Test + public void testRandomNumberExcludeMin() { + List params = List.of("1", "10.5", "20.5", "true", "false"); + function = new AdvancedRandomNumberFunction() { + @Override + BigDecimal createRandomValue(BigDecimal minValue, BigDecimal range, double random) { + random = 0.0; + return super.createRandomValue(minValue, range, random); + } + }; + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue > 10.5 && randomValue <= 20.5); + } + + @Test + public void testRandomNumberExcludeMax() { + List params = List.of("2", "10.5", "20.5", "false", "true"); + function = new AdvancedRandomNumberFunction() { + @Override + BigDecimal createRandomValue(BigDecimal minValue, BigDecimal range, double random) { + random = 1.0; + return super.createRandomValue(minValue, range, random); + } + }; + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= 10.5 && randomValue < 20.5); + } + + @Test + public void testRandomInteger32EdgeCase() { + List params = List.of("0", "-2147483648", "2147483647", "false", "false"); + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= -Integer.MAX_VALUE && randomValue < Integer.MAX_VALUE); + } + + @Test + public void testRandomInteger32MinEqualsMaxEdgeCase() { + List params = List.of("0", "3", "3", "false", "false"); + for (int i = 0; i < 100; i++) { + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertEquals(randomValue, 3); + } + } + + // randomDouble('0','3','3','true','true') + // randomDouble('0','3','3','true','true') + + @Test + public void testRandomDouble32MinEqualsMaxEdgeCase() { + List params = List.of("2", "3.0", "3.0", "false", "false"); + for (int i = 0; i < 100; i++) { + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertEquals(randomValue, 3); + } + } + + @Test + public void testRandomInteger64EdgeCase() { + List params = List.of("0", "-9223372036854775808", "9223372036854775807", "false", + "false"); + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= -Long.MAX_VALUE && randomValue < Long.MAX_VALUE); + } + + @Test + public void testRandomNumberFloatEdgeCase() { + List params = List.of("0", "-3.4028235E38", "3.4028235E38", "false", "false"); + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= -Float.MAX_VALUE && randomValue < Float.MAX_VALUE); + } + + @Test + public void testRandomNumberDoubleEdgeCase() { + List params = List.of("0", "-1.7976931348623157E308", "1.7976931348623157E308", + "false", "false"); + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= -Double.MAX_VALUE && randomValue < Double.MAX_VALUE); + } + + @Test + public void testInvalidDecimalPlaces() { + List params = List.of("-1"); // invalid decimalPlaces + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(params, context)); + assertEquals(exception.getMessage(), + "Decimal places must be a non-negative integer value."); + } + + @Test + public void testInvalidRange() { + List params = List.of("2", "20.5", "10.5"); // invalid range + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(params, context)); + assertEquals(exception.getMessage(), + "Min value must be less than max value."); + } + + @Test + public void testInvalidDecimalPlacesFormat() { + List params = List.of("xxx"); // invalid decimalPlaces + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(params, context)); + assertEquals(exception.getMessage(), + "Invalid parameter at index 1. xxx must be parsable to Integer."); + } + + @Test + public void testInvalidMinValueFormat() { + List params = List.of("1", "xxx"); // invalid min value + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(params, context)); + assertEquals(exception.getMessage(), + "Invalid parameter at index 2. xxx must be parsable to BigDecimal."); + } + + @Test + public void testInvalidMaxValueFormat() { + List params = List.of("1", "1.1", "xxx"); // invalid max value + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(params, context)); + assertEquals(exception.getMessage(), + "Invalid parameter at index 3. xxx must be parsable to BigDecimal."); + } + + @DataProvider(name = "testRandomNumber") + public static Object[][] testRandomNumber() { + return new Object[][]{ + + {0, 12, null, null, false, false}, + {0, null, 0, 2, true, true}, + {0, null, null, null, false, false}, + {0, null, 0, 100, false, false}, + {0, null, 0, 2, false, false}, + {0, null, -100, 0, false, false}, + {0, null, -2, 0, false, false}, + {0, null, 0, 100, true, true}, + {0, null, -100, 0, true, true}, + {0, null, -2, 0, true, true}, + {0, null, 0, null, false, false}, + {0, null, 0, 0, false, false}, + {0, 11, 0, 12, true, true}, + + {0, 13, 0, 100, false, false}, + {0, 14, 0, 14, false, false}, + {0, 15, -100, 0, false, false}, + {0, 16, -16, 0, false, false}, + {0, 17, 0, 100, true, true}, + {0, 18, -100, 0, true, true}, + {0, 19, -20, 0, true, true}, + {0, 20, 0, null, false, false}, + {0, 21, 21, 21, false, false}, + + {0, null, 0, 2, true, true}, + {0, null, null, null, false, false}, + {0, null, 0, 100, false, false}, + {0, null, 0, 2, false, false}, + {0, null, -100, 0, false, false}, + {0, null, -2, 0, false, false}, + {0, null, 0, 100, true, true}, + {0, null, -100, 0, true, true}, + {0, null, -2, 0, true, true}, + {0, null, 0, null, false, false}, + {0, null, 0, 0, false, false}, + {0, 11, 0, 12, true, true}, + {0, 12, null, null, false, false}, + {0, 13, 0, 100, false, false}, + {0, 14, 0, 14, false, false}, + {0, 15, -100, 0, false, false}, + {0, 16, -16, 0, false, false}, + {0, 17, 0, 100, true, true}, + {0, 18, -100, 0, true, true}, + {0, 19, -20, 0, true, true}, + {0, 20, 0, null, false, false}, + {0, 21, 21, 21, false, false}, + + {3, null, 0, 2, true, true}, + {3, null, null, null, false, false}, + {3, null, 0, 100, false, false}, + {3, null, 0, 2, false, false}, + {3, null, -100, 0, false, false}, + {3, null, -2, 0, false, false}, + {3, null, 0, 100, true, true}, + {3, null, -100, 0, true, true}, + {3, null, -2, 0, true, true}, + {3, null, 0, null, false, false}, + {3, null, 0, 0, false, false}, + {3, 11.123f, 0, 13, true, true}, + {3, 12.123f, null, null, false, false}, + {3, 13.123f, 0, 100, false, false}, + {3, 14.123f, 0, 14, false, false}, + {3, 15.123f, -100, 0, false, false}, + {3, 16.123f, -16, 0, false, false}, + {3, 17.123f, 0, 100, true, true}, + {3, 18.123f, -100, 0, true, true}, + {3, 19.123f, -21, 0, true, true}, + {3, 20.123f, 0, null, false, false}, + {3, 21.123f, 21.122f, 21.124f, false, false}, + + {5, null, 0, 2, true, true}, + {5, null, null, null, false, false}, + {5, null, 0, 100, false, false}, + {5, null, 0, 2, false, false}, + {5, null, -100, 0, false, false}, + {5, null, -2, 0, false, false}, + {5, null, 0, 100, true, true}, + {5, null, -100, 0, true, true}, + {5, null, -2, 0, true, true}, + {5, null, 0, null, false, false}, + {5, null, 0, 0, false, false}, + {5, 11.123d, 0, 13, true, true}, + {5, 12.123d, null, null, false, false}, + {5, 13.123d, 0, 100, false, false}, + {5, 14.123d, 0, 14, false, false}, + {5, 15.123d, -100, 0, false, false}, + {5, 16.123d, -16, 0, false, false}, + {5, 17.123d, 0, 100, true, true}, + {5, 18.123d, -100, 0, true, true}, + {5, 19.123d, -21, 0, true, true}, + {5, 20.123d, 0, null, false, false}, + {5, 21.123d, 21.122d, 21.124d, false, false}, + + }; + } + + @Test(dataProvider = "testRandomNumber") + void testRandomNumber(Number decimalPlaces, Number multipleOf, Number minimum, Number maximum, + boolean exclusiveMinimum, boolean exclusiveMaximum) { + + TestContext testContext = new TestContext(); + AdvancedRandomNumberFunction advancedRandomNumberFunction = new AdvancedRandomNumberFunction(); + try { + for (int i = 0; i < 1000; i++) { + + BigDecimal value = new BigDecimal(advancedRandomNumberFunction.execute( + List.of(toString(decimalPlaces), toString(minimum), toString(maximum), + toString(exclusiveMinimum), toString(exclusiveMaximum), toString(multipleOf)), testContext)); + + if (multipleOf != null) { + BigDecimal remainder = value.remainder(new BigDecimal(multipleOf.toString())); + + assertEquals( + remainder.compareTo(BigDecimal.ZERO), 0, + "Expected %s to be a multiple of %s! Remainder is %s".formatted( + value, multipleOf, + remainder)); + } + + if (maximum != null) { + if (exclusiveMaximum) { + assertTrue(value.doubleValue() < maximum.doubleValue(), + "Expected %s to be lower than %s!".formatted( + value, maximum)); + } else { + assertTrue(value.doubleValue() <= maximum.doubleValue(), + "Expected %s to be lower or equal than %s!".formatted( + value, maximum)); + } + } + + if (minimum != null) { + if (exclusiveMinimum) { + assertTrue(value.doubleValue() > minimum.doubleValue(), + "Expected %s to be larger than %s!".formatted( + value, minimum)); + } else { + assertTrue(value.doubleValue() >= minimum.doubleValue(), + "Expected %s to be larger or equal than %s!".formatted( + value, minimum)); + } + } + } + } catch (Exception e) { + Assert.fail("Creation of multiple float threw an exception: " + e.getMessage(), e); + } + } + + private String toString(Object obj) { + if (obj == null) { + return "null"; + } + return obj.toString(); + + } + + private T expectThrows(Class exceptionClass, Runnable runnable) { + try { + runnable.run(); + } catch (Throwable throwable) { + if (exceptionClass.isInstance(throwable)) { + return exceptionClass.cast(throwable); + } else { + throw new AssertionError("Unexpected exception type", throwable); + } + } + throw new AssertionError("Expected exception not thrown"); + } +} \ No newline at end of file diff --git a/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomDoubleFunctionTest.java b/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomDoubleFunctionTest.java deleted file mode 100644 index 1452fb881b..0000000000 --- a/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomDoubleFunctionTest.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.citrusframework.functions.core; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertTrue; - -import java.math.BigDecimal; -import java.util.List; -import org.citrusframework.context.TestContext; -import org.citrusframework.exceptions.InvalidFunctionUsageException; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -public class RandomDoubleFunctionTest { - - private AdvancedRandomNumberFunction function; - private TestContext context; - - @BeforeMethod - public void setUp() { - function = new AdvancedRandomNumberFunction(); - context = new TestContext(); - } - - @Test - public void testRandomNumberWithNullParameter() { - InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, - () -> function.execute(null, context)); - assertEquals(exception.getMessage(), - "Function parameters must not be null."); - } - - @Test - public void testRandomNumberWithDefaultValues() { - List params = List.of(); - String result = function.execute(params, context); - assertNotNull(result); - assertTrue(result.matches("-?\\d*")); - } - - @Test - public void testRandomNumberWithDecimalPlaces() { - List params = List.of("2"); - String result = function.execute(params, context); - assertNotNull(result); - assertTrue(result.matches("-?\\d*\\.\\d{2}"), "result does not match pattern: "+result); - } - - @Test - public void testRandomNumberWithinRange() { - List params = List.of("2", "10.5", "20.5"); - String result = function.execute(params, context); - assertNotNull(result); - double randomValue = Double.parseDouble(result); - assertTrue(randomValue >= 10.5 && randomValue <= 20.5); - } - - @Test - public void testRandomNumberIncludesMin() { - List params = List.of("1", "10.5", "20.5"); - function = new AdvancedRandomNumberFunction() { - @Override - double getRandomValue(double minValue, BigDecimal range, double random) { - random = 0.0; - return super.getRandomValue(minValue, range, random); - } - }; - String result = function.execute(params, context); - assertEquals(result, "10.5"); - } - - @Test - public void testRandomNumberIncludesMax() { - List params = List.of("1", "10.5", "20.5"); - function = new AdvancedRandomNumberFunction() { - @Override - double getRandomValue(double minValue, BigDecimal range, double random) { - random = 1.0; - return super.getRandomValue(minValue, range, random); - } - }; - String result = function.execute(params, context); - assertEquals(result, "20.5"); - } - - @Test - public void testRandomNumberExcludeMin() { - List params = List.of("1", "10.5", "20.5", "true", "false"); - function = new AdvancedRandomNumberFunction() { - @Override - double getRandomValue(double minValue, BigDecimal range, double random) { - random = 0.0; - return super.getRandomValue(minValue, range, random); - } - }; - String result = function.execute(params, context); - assertNotNull(result); - double randomValue = Double.parseDouble(result); - assertTrue(randomValue > 10.5 && randomValue <= 20.5); - } - - @Test - public void testRandomNumberExcludeMax() { - List params = List.of("2", "10.5", "20.5", "false", "true"); - function = new AdvancedRandomNumberFunction() { - @Override - double getRandomValue(double minValue, BigDecimal range, double random) { - random = 1.0; - return super.getRandomValue(minValue, range, random); - } - }; - String result = function.execute(params, context); - assertNotNull(result); - double randomValue = Double.parseDouble(result); - assertTrue(randomValue >= 10.5 && randomValue < 20.5); - } - - @Test - public void testRandomInteger32EdgeCase() { - List params = List.of("0", "-2147483648", "2147483647", "false", "false"); - String result = function.execute(params, context); - assertNotNull(result); - double randomValue = Double.parseDouble(result); - assertTrue(randomValue >= -Integer.MAX_VALUE && randomValue < Integer.MAX_VALUE); - } - - @Test - public void testRandomInteger32MinEqualsMaxEdgeCase() { - List params = List.of("0", "3", "3", "false", "false"); - for (int i =0;i<100;i++) { - String result = function.execute(params, context); - assertNotNull(result); - double randomValue = Double.parseDouble(result); - assertEquals(randomValue, 3); - } - } - - - - // randomDouble('0','3','3','true','true') - // randomDouble('0','3','3','true','true') - - @Test - public void testRandomDouble32MinEqualsMaxEdgeCase() { - List params = List.of("2", "3.0", "3.0", "false", "false"); - for (int i =0;i<100;i++) { - String result = function.execute(params, context); - assertNotNull(result); - double randomValue = Double.parseDouble(result); - assertEquals(randomValue, 3); - } - } - - @Test - public void testRandomInteger64EdgeCase() { - List params = List.of("0", "-9223372036854775808", "9223372036854775807", "false", "false"); - String result = function.execute(params, context); - assertNotNull(result); - double randomValue = Double.parseDouble(result); - assertTrue(randomValue >=-Long.MAX_VALUE && randomValue < Long.MAX_VALUE); - } - - @Test - public void testRandomNumberFloatEdgeCase() { - List params = List.of("0", "-3.4028235E38", "3.4028235E38", "false", "false"); - String result = function.execute(params, context); - assertNotNull(result); - double randomValue = Double.parseDouble(result); - assertTrue(randomValue >= -Float.MAX_VALUE && randomValue < Float.MAX_VALUE); - } - - @Test - public void testRandomNumberDoubleEdgeCase() { - List params = List.of("0", "-1.7976931348623157E308", "1.7976931348623157E308", "false", "false"); - String result = function.execute(params, context); - assertNotNull(result); - double randomValue = Double.parseDouble(result); - assertTrue(randomValue >= -Double.MAX_VALUE && randomValue < Double.MAX_VALUE); - } - - @Test - public void testInvalidDecimalPlaces() { - List params = List.of("-1"); // invalid decimalPlaces - InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, () -> function.execute(params, context)); - assertEquals(exception.getMessage(), "Invalid parameter definition. Decimal places must be a non-negative integer value."); - } - - @Test - public void testInvalidRange() { - List params = List.of("2", "20.5", "10.5"); // invalid range - InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, () -> function.execute(params, context)); - assertEquals(exception.getMessage(), "Invalid parameter definition. Min value must be less than max value."); - } - - @Test - public void testInvalidDecimalPlacesFormat() { - List params = List.of("xxx"); // invalid decimalPlaces - InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, () -> function.execute(params, context)); - assertEquals(exception.getMessage(), "Invalid parameter at index 1. xxx must be parsable to Integer."); - } - - @Test - public void testInvalidMinValueFormat() { - List params = List.of("1","xxx"); // invalid min value - InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, () -> function.execute(params, context)); - assertEquals(exception.getMessage(), "Invalid parameter at index 2. xxx must be parsable to Double."); - } - - @Test - public void testInvalidMaxValueFormat() { - List params = List.of("1", "1.1", "xxx"); // invalid max value - InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, - () -> function.execute(params, context)); - assertEquals(exception.getMessage(), - "Invalid parameter at index 3. xxx must be parsable to Double."); - } - - private T expectThrows(Class exceptionClass, Runnable runnable) { - try { - runnable.run(); - } catch (Throwable throwable) { - if (exceptionClass.isInstance(throwable)) { - return exceptionClass.cast(throwable); - } else { - throw new AssertionError("Unexpected exception type", throwable); - } - } - throw new AssertionError("Expected exception not thrown"); - } -} \ No newline at end of file diff --git a/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomStringFunctionTest.java b/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomStringFunctionTest.java index 34b982a486..381adc2825 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomStringFunctionTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomStringFunctionTest.java @@ -17,8 +17,10 @@ package org.citrusframework.functions.core; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.citrusframework.UnitTestSupport; import org.citrusframework.exceptions.InvalidFunctionUsageException; import org.testng.Assert; @@ -28,7 +30,8 @@ import static java.util.Collections.singletonList; public class RandomStringFunctionTest extends UnitTestSupport { - private RandomStringFunction function = new RandomStringFunction(); + + private final RandomStringFunction function = new RandomStringFunction(); @Test public void testFunction() { @@ -110,8 +113,31 @@ public void testTooManyParameters() { params.add("3"); params.add("UPPERCASE"); params.add("true"); - params.add("too much"); + params.add("0"); + params.add("too many"); function.execute(params, context); } -} + + @Test + public void testRandomSize() { + List params; + params = new ArrayList<>(); + params.add("10"); + params.add("UPPERCASE"); + params.add("true"); + params.add("8"); + + Set sizes = new HashSet<>(); + + for (int i = 0; i < 1000; i++) { + String text = function.execute(params, context); + sizes.add(text.length()); + } + + Assert.assertTrue(sizes.contains(8)); + Assert.assertTrue(sizes.contains(9)); + Assert.assertTrue(sizes.contains(10)); + + } +} \ No newline at end of file