Skip to content

Commit

Permalink
feat(#1175): added random generator framework
Browse files Browse the repository at this point in the history
Framework was added in favour of OpenApiTestDataGenerator implementation
  • Loading branch information
Thorsten Schlathoelter committed Jul 7, 2024
1 parent 8ef6181 commit 8f4bcb5
Show file tree
Hide file tree
Showing 37 changed files with 2,627 additions and 1,046 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,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)
Expand All @@ -152,8 +152,7 @@ private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter
private void setSpecifiedBody(TestContext context, OasOperation operation) {
Optional<OasSchema> 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) {
Expand All @@ -180,9 +179,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));
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -161,26 +160,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<OasResponse> 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<String> filteredHeaders = new HashSet<>(getMessage().getHeaders().keySet());
Predicate<Entry<String, OasSchema>> filteredHeadersPredicate = entry -> !filteredHeaders.contains(
entry.getKey());
Expand All @@ -192,7 +189,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))))
);
Expand All @@ -210,8 +206,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<OasAdapter<OasSchema, String>> schemaForMediaTypeOptional;
if (statusCode.startsWith("2")) {
Expand All @@ -228,7 +223,7 @@ private void buildRandomPayload(OasOperation operation, OasDocument oasDocument,
OasAdapter<OasSchema, String> 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.
Expand All @@ -239,21 +234,19 @@ private void buildRandomPayload(OasOperation operation, OasDocument oasDocument,
}
}

private void createRandomPayload(HttpMessage message, OasDocument oasDocument, OasAdapter<OasSchema, String> schemaForMediaType) {
private void createRandomPayload(HttpMessage message, OasAdapter<OasSchema, String> schemaForMediaType) {

if (schemaForMediaType.node() == null) {
// No schema means no payload, no type
message.setPayload(null);
} 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.</p>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 : 9;

int nItems = ThreadLocalRandom.current()
.nextInt(minItems.intValue(), maxItems.intValue() + 1);

randomContext.getRandomModelBuilder().array(() -> {
for (int i = 0; i < nItems; i++) {
randomContext.generate(itemsSchema);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>The generator supports composite schemas, which include `allOf`, `anyOf`, and `oneOf` constructs.</p>
*/
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<OasSchema> 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<String, Object> createAllOff(RandomContext randomContext, OasSchema schema) {
Map<String, Object> allOf = new HashMap<>();

randomContext.getRandomModelBuilder().object(() -> {
for (OasSchema oneSchema : schema.allOf) {
randomContext.generate(oneSchema);
}
});

return allOf;
}
}
Original file line number Diff line number Diff line change
@@ -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<RandomGenerator> randomGenerators;

public static final RandomConfiguration RANDOM_CONFIGURATION = new RandomConfiguration();

private RandomConfiguration() {
List<RandomGenerator> 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);
}
}
Loading

0 comments on commit 8f4bcb5

Please sign in to comment.