diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiConstants.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiConstants.java
new file mode 100644
index 0000000000..d6802ed996
--- /dev/null
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiConstants.java
@@ -0,0 +1,41 @@
+/*
+ * 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.openapi;
+
+public abstract class OpenApiConstants {
+
+ public static final String TYPE_ARRAY = "array";
+ public static final String TYPE_BOOLEAN = "boolean";
+ public static final String TYPE_INTEGER = "integer";
+ public static final String TYPE_NUMBER = "number";
+ public static final String TYPE_OBJECT = "object";
+ public static final String TYPE_STRING = "string";
+
+ public static final String FORMAT_INT32 = "int32";
+ public static final String FORMAT_INT64 = "int64";
+ public static final String FORMAT_FLOAT = "float";
+ public static final String FORMAT_DOUBLE = "double";
+ public static final String FORMAT_DATE = "date";
+ public static final String FORMAT_DATE_TIME = "date-time";
+ public static final String FORMAT_UUID = "uuid";
+
+ /**
+ * Prevent instantiation.
+ */
+ private OpenApiConstants() {
+ }
+}
diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiRepository.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiRepository.java
index 083da342ca..20f845d604 100644
--- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiRepository.java
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiRepository.java
@@ -94,8 +94,8 @@ public void setResponseValidationEnabled(boolean responseValidationEnabled) {
public void addRepository(Resource openApiResource) {
OpenApiSpecification openApiSpecification = OpenApiSpecification.from(openApiResource);
determineResourceAlias(openApiResource).ifPresent(openApiSpecification::addAlias);
- openApiSpecification.setRequestValidationEnabled(requestValidationEnabled);
- openApiSpecification.setResponseValidationEnabled(responseValidationEnabled);
+ openApiSpecification.setApiRequestValidationEnabled(requestValidationEnabled);
+ openApiSpecification.setApiResponseValidationEnabled(responseValidationEnabled);
openApiSpecification.setRootContextPath(rootContextPath);
this.openApiSpecifications.add(openApiSpecification);
diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSettings.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSettings.java
index ea99928985..02441f1115 100644
--- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSettings.java
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSettings.java
@@ -1,3 +1,19 @@
+/*
+ * 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.openapi;
import static java.lang.Boolean.parseBoolean;
@@ -35,7 +51,7 @@ public static boolean isValidateOptionalFieldsGlobally() {
System.getenv(VALIDATE_OPTIONAL_FIELDS_ENV) : "true"));
}
- public static boolean isRequestValidationEnabledlobally() {
+ public static boolean isRequestValidationEnabledGlobally() {
return parseBoolean(System.getProperty(
REQUEST_VALIDATION_ENABLED_PROPERTY, System.getenv(REQUEST_VALIDATION_ENABLED_ENV) != null ?
System.getenv(REQUEST_VALIDATION_ENABLED_ENV) : "true"));
diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecification.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecification.java
index cd70694f74..f27f1bed48 100644
--- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecification.java
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecification.java
@@ -16,24 +16,14 @@
package org.citrusframework.openapi;
-import com.atlassian.oai.validator.OpenApiInteractionValidator;
-import com.atlassian.oai.validator.OpenApiInteractionValidator.Builder;
+import static org.citrusframework.openapi.OpenApiSettings.isGenerateOptionalFieldsGlobally;
+import static org.citrusframework.openapi.OpenApiSettings.isRequestValidationEnabledGlobally;
+import static org.citrusframework.openapi.OpenApiSettings.isResponseValidationEnabledGlobally;
+import static org.citrusframework.openapi.OpenApiSettings.isValidateOptionalFieldsGlobally;
+
import io.apicurio.datamodels.core.models.common.Info;
import io.apicurio.datamodels.openapi.models.OasDocument;
import io.apicurio.datamodels.openapi.models.OasOperation;
-import org.citrusframework.context.TestContext;
-import org.citrusframework.exceptions.CitrusRuntimeException;
-import org.citrusframework.http.client.HttpClient;
-import org.citrusframework.openapi.model.OasModelHelper;
-import org.citrusframework.openapi.model.OperationPathAdapter;
-import org.citrusframework.openapi.validation.OpenApiRequestValidator;
-import org.citrusframework.openapi.validation.OpenApiResponseValidator;
-import org.citrusframework.spi.Resource;
-import org.citrusframework.spi.Resources;
-import org.citrusframework.util.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
@@ -45,11 +35,19 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
-
-import static org.citrusframework.openapi.OpenApiSettings.isGenerateOptionalFieldsGlobally;
-import static org.citrusframework.openapi.OpenApiSettings.isRequestValidationEnabledlobally;
-import static org.citrusframework.openapi.OpenApiSettings.isResponseValidationEnabledGlobally;
-import static org.citrusframework.openapi.OpenApiSettings.isValidateOptionalFieldsGlobally;
+import org.citrusframework.context.TestContext;
+import org.citrusframework.exceptions.CitrusRuntimeException;
+import org.citrusframework.http.client.HttpClient;
+import org.citrusframework.openapi.model.OasModelHelper;
+import org.citrusframework.openapi.model.OperationPathAdapter;
+import org.citrusframework.openapi.util.OpenApiUtils;
+import org.citrusframework.openapi.validation.SwaggerOpenApiValidationContext;
+import org.citrusframework.openapi.validation.SwaggerOpenApiValidationContextLoader;
+import org.citrusframework.spi.Resource;
+import org.citrusframework.spi.Resources;
+import org.citrusframework.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* OpenApi specification resolves URL or local file resources to a specification document.
@@ -97,33 +95,41 @@ public class OpenApiSpecification {
private OasDocument openApiDoc;
+ private SwaggerOpenApiValidationContext swaggerOpenApiValidationContext;
+
private boolean generateOptionalFields = isGenerateOptionalFieldsGlobally();
private boolean validateOptionalFields = isValidateOptionalFieldsGlobally();
- private boolean requestValidationEnabled = isRequestValidationEnabledlobally();
+ /**
+ * Flag to indicate, whether request validation is enabled on api level. Api level overrules global
+ * level and may be overruled by request level.
+ */
+ private boolean apiRequestValidationEnabled = isRequestValidationEnabledGlobally();
- private boolean responseValidationEnabled = isResponseValidationEnabledGlobally();
+ /**
+ * Flag to indicate, whether response validation is enabled on api level. Api level overrules global
+ * level and may be overruled by request level.
+ */
+ private boolean apiResponseValidationEnabled = isResponseValidationEnabledGlobally();
private final Set aliases = Collections.synchronizedSet(new HashSet<>());
/**
- * Maps the identifier (id) of an operation to OperationPathAdapters. Two different keys may be used for each operation.
- * Refer to {@link org.citrusframework.openapi.OpenApiSpecification#storeOperationPathAdapter} for more details.
+ * Maps the identifier (id) of an operation to OperationPathAdapters. Two different keys may be
+ * used for each operation. Refer to
+ * {@link org.citrusframework.openapi.OpenApiSpecification#storeOperationPathAdapter} for more
+ * details.
*/
private final Map operationIdToOperationPathAdapter = new ConcurrentHashMap<>();
/**
- * Stores the unique identifier (uniqueId) of an operation, derived from its HTTP method and path.
- * This identifier can always be determined and is therefore safe to use, even for operations without
- * an optional operationId defined.
+ * Stores the unique identifier (uniqueId) of an operation, derived from its HTTP method and
+ * path. This identifier can always be determined and is therefore safe to use, even for
+ * operations without an optional operationId defined.
*/
private final Map operationToUniqueId = new ConcurrentHashMap<>();
- private OpenApiRequestValidator openApiRequestValidator;
-
- private OpenApiResponseValidator openApiResponseValidator;
-
public static OpenApiSpecification from(String specUrl) {
OpenApiSpecification specification = new OpenApiSpecification();
specification.setSpecUrl(specUrl);
@@ -134,21 +140,19 @@ public static OpenApiSpecification from(String specUrl) {
public static OpenApiSpecification from(URL specUrl) {
OpenApiSpecification specification = new OpenApiSpecification();
OasDocument openApiDoc;
- OpenApiInteractionValidator validator;
+ SwaggerOpenApiValidationContext swaggerOpenApiValidationContext;
if (specUrl.getProtocol().startsWith(HTTPS)) {
openApiDoc = OpenApiResourceLoader.fromSecuredWebResource(specUrl);
- validator = new OpenApiInteractionValidator.Builder().withInlineApiSpecification(
- OpenApiResourceLoader.rawFromSecuredWebResource(specUrl)).build();
+ swaggerOpenApiValidationContext = SwaggerOpenApiValidationContextLoader.fromSecuredWebResource(specUrl);
} else {
openApiDoc = OpenApiResourceLoader.fromWebResource(specUrl);
- validator = new OpenApiInteractionValidator.Builder().withInlineApiSpecification(
- OpenApiResourceLoader.rawFromWebResource(specUrl)).build();
+ swaggerOpenApiValidationContext = SwaggerOpenApiValidationContextLoader.fromWebResource(specUrl);
}
specification.setSpecUrl(specUrl.toString());
specification.initPathLookups();
specification.setOpenApiDoc(openApiDoc);
- specification.setValidator(validator);
+ specification.setSwaggerOpenApiValidationContext(swaggerOpenApiValidationContext);
specification.setRequestUrl(
String.format("%s://%s%s%s", specUrl.getProtocol(), specUrl.getHost(),
specUrl.getPort() > 0 ? ":" + specUrl.getPort() : "",
@@ -160,11 +164,9 @@ public static OpenApiSpecification from(URL specUrl) {
public static OpenApiSpecification from(Resource resource) {
OpenApiSpecification specification = new OpenApiSpecification();
OasDocument openApiDoc = OpenApiResourceLoader.fromFile(resource);
- OpenApiInteractionValidator validator = new Builder().withInlineApiSpecification(
- OpenApiResourceLoader.rawFromFile(resource)).build();
specification.setOpenApiDoc(openApiDoc);
- specification.setValidator(validator);
+ specification.setSwaggerOpenApiValidationContext(SwaggerOpenApiValidationContextLoader.fromFile(resource));
String schemeToUse = Optional.ofNullable(OasModelHelper.getSchemes(openApiDoc))
.orElse(Collections.singletonList(HTTP))
@@ -215,12 +217,11 @@ public synchronized OasDocument getOpenApiDoc(TestContext context) {
if (resolvedSpecUrl.startsWith(HTTPS)) {
initApiDoc(
() -> OpenApiResourceLoader.fromSecuredWebResource(specWebResource));
- setValidator(new OpenApiInteractionValidator.Builder().withInlineApiSpecification(
- OpenApiResourceLoader.rawFromSecuredWebResource(specWebResource)).build());
+
+ setSwaggerOpenApiValidationContext(SwaggerOpenApiValidationContextLoader.fromSecuredWebResource(specWebResource));
} else {
initApiDoc(() -> OpenApiResourceLoader.fromWebResource(specWebResource));
- setValidator(new OpenApiInteractionValidator.Builder().withInlineApiSpecification(
- OpenApiResourceLoader.rawFromWebResource(specWebResource)).build());
+ setSwaggerOpenApiValidationContext(SwaggerOpenApiValidationContextLoader.fromWebResource(specWebResource));
}
if (requestUrl == null) {
@@ -234,8 +235,7 @@ public synchronized OasDocument getOpenApiDoc(TestContext context) {
Resource resource = Resources.create(resolvedSpecUrl);
initApiDoc(
() -> OpenApiResourceLoader.fromFile(resource));
- setValidator(new OpenApiInteractionValidator.Builder().withInlineApiSpecification(
- OpenApiResourceLoader.rawFromFile(resource)).build());
+ setSwaggerOpenApiValidationContext(SwaggerOpenApiValidationContextLoader.fromFile(resource));
if (requestUrl == null) {
String schemeToUse = Optional.ofNullable(OasModelHelper.getSchemes(openApiDoc))
@@ -255,6 +255,11 @@ public synchronized OasDocument getOpenApiDoc(TestContext context) {
return openApiDoc;
}
+ public SwaggerOpenApiValidationContext getSwaggerOpenApiValidationContext() {
+ return swaggerOpenApiValidationContext;
+ }
+
+
// provided for testing
URL toSpecUrl(String resolvedSpecUrl) {
try {
@@ -269,12 +274,10 @@ void setOpenApiDoc(OasDocument openApiDoc) {
initApiDoc(() -> openApiDoc);
}
- private void setValidator(OpenApiInteractionValidator openApiInteractionValidator) {
- openApiRequestValidator = new OpenApiRequestValidator(openApiInteractionValidator);
- openApiRequestValidator.setEnabled(requestValidationEnabled);
-
- openApiResponseValidator = new OpenApiResponseValidator(openApiInteractionValidator);
- openApiRequestValidator.setEnabled(responseValidationEnabled);
+ private void setSwaggerOpenApiValidationContext(SwaggerOpenApiValidationContext swaggerOpenApiValidationContext) {
+ this.swaggerOpenApiValidationContext = swaggerOpenApiValidationContext;
+ this.swaggerOpenApiValidationContext.setResponseValidationEnabled(apiResponseValidationEnabled);
+ this.swaggerOpenApiValidationContext.setRequestValidationEnabled(apiRequestValidationEnabled);
}
private void initApiDoc(Supplier openApiDocSupplier) {
@@ -306,12 +309,14 @@ private void initPathLookups() {
}
/**
- * Stores an {@link OperationPathAdapter} in {@link org.citrusframework.openapi.OpenApiSpecification#operationIdToOperationPathAdapter}.
- * The adapter is stored using two keys: the operationId (optional) and the full path of the operation, including the method.
- * The full path is always determinable and thus can always be safely used.
+ * Stores an {@link OperationPathAdapter} in
+ * {@link org.citrusframework.openapi.OpenApiSpecification#operationIdToOperationPathAdapter}.
+ * The adapter is stored using two keys: the operationId (optional) and the full path of the
+ * operation, including the method. The full path is always determinable and thus can always be
+ * safely used.
*
* @param operation The {@link OperationPathAdapter} to store.
- * @param path The full path of the operation, including the method.
+ * @param path The full path of the operation, including the method.
*/
private void storeOperationPathAdapter(OasOperation operation, String path) {
@@ -319,9 +324,10 @@ private void storeOperationPathAdapter(OasOperation operation, String path) {
String fullOperationPath = StringUtils.appendSegmentToUrlPath(basePath, path);
OperationPathAdapter operationPathAdapter = new OperationPathAdapter(path, rootContextPath,
- StringUtils.appendSegmentToUrlPath(rootContextPath, path), operation);
+ StringUtils.appendSegmentToUrlPath(rootContextPath, path), operation);
- String uniqueOperationId = OpenApiUtils.createFullPathOperationIdentifier(fullOperationPath, operation);
+ String uniqueOperationId = OpenApiUtils.createFullPathOperationIdentifier(fullOperationPath,
+ operation);
operationToUniqueId.put(operation, uniqueOperationId);
operationIdToOperationPathAdapter.put(uniqueOperationId, operationPathAdapter);
@@ -358,25 +364,25 @@ public void setRequestUrl(String requestUrl) {
this.requestUrl = requestUrl;
}
- public boolean isRequestValidationEnabled() {
- return requestValidationEnabled;
+ public boolean isApiRequestValidationEnabled() {
+ return apiRequestValidationEnabled;
}
- public void setRequestValidationEnabled(boolean enabled) {
- this.requestValidationEnabled = enabled;
- if (this.openApiRequestValidator != null) {
- this.openApiRequestValidator.setEnabled(enabled);
+ public void setApiRequestValidationEnabled(boolean enabled) {
+ this.apiRequestValidationEnabled = enabled;
+ if (this.swaggerOpenApiValidationContext != null) {
+ this.swaggerOpenApiValidationContext.setRequestValidationEnabled(enabled);
}
}
- public boolean isResponseValidationEnabled() {
- return responseValidationEnabled;
+ public boolean isApiResponseValidationEnabled() {
+ return apiResponseValidationEnabled;
}
- public void setResponseValidationEnabled(boolean enabled) {
- this.responseValidationEnabled = enabled;
- if (this.openApiResponseValidator != null) {
- this.openApiResponseValidator.setEnabled(enabled);
+ public void setApiResponseValidationEnabled(boolean enabled) {
+ this.apiResponseValidationEnabled = enabled;
+ if (this.swaggerOpenApiValidationContext != null) {
+ this.swaggerOpenApiValidationContext.setResponseValidationEnabled(enabled);
}
}
@@ -449,14 +455,6 @@ public Optional getOperation(String operationId, TestConte
return Optional.ofNullable(operationIdToOperationPathAdapter.get(operationId));
}
- public Optional getRequestValidator() {
- return Optional.ofNullable(openApiRequestValidator);
- }
-
- public Optional getResponseValidator() {
- return Optional.ofNullable(openApiResponseValidator);
- }
-
public OpenApiSpecification withRootContext(String rootContextPath) {
setRootContextPath(rootContextPath);
return this;
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 e2d3d1feeb..74deef213f 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,72 +17,125 @@
package org.citrusframework.openapi;
import io.apicurio.datamodels.openapi.models.OasSchema;
-import jakarta.annotation.Nullable;
+import io.apicurio.datamodels.openapi.v3.models.Oas30Schema;
import org.citrusframework.CitrusSettings;
import org.citrusframework.context.TestContext;
-import org.citrusframework.exceptions.CitrusRuntimeException;
import org.citrusframework.openapi.model.OasModelHelper;
-import org.springframework.util.CollectionUtils;
-import org.springframework.util.StringUtils;
-
+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;
+
/**
- * Generates proper payloads and validation expressions based on Open API specification rules. Creates outbound payloads
- * with generated random test data according to specification and creates inbound payloads with proper validation expressions to
- * enforce the specification rules.
- *
+ * Generates proper payloads and validation expressions based on Open API specification rules.
*/
-public class OpenApiTestDataGenerator {
+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) {
+ 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));
- return createOutboundPayload(resolved, definitions, specification);
+ createOutboundPayloadAsMap(randomModelBuilder, resolved, definitions, specification,
+ visitedRefSchemas);
+ return;
}
- StringBuilder payload = new StringBuilder();
- if (OasModelHelper.isObjectType(schema)) {
- payload.append("{");
-
- if (schema.properties != null) {
- for (Map.Entry entry : schema.properties.entrySet()) {
- if (specification.isGenerateOptionalFields() || isRequired(schema, entry.getKey())) {
- payload.append("\"")
- .append(entry.getKey())
- .append("\": ")
- .append(createRandomValueExpression(entry.getValue(), definitions, true, specification))
- .append(",");
- }
- }
- }
-
- if (payload.toString().endsWith(",")) {
- payload.replace(payload.length() - 1, payload.length(), "");
- }
-
- payload.append("}");
- } else if (OasModelHelper.isArrayType(schema)) {
- payload.append("[");
- payload.append(createRandomValueExpression((OasSchema) schema.items, definitions, true, specification));
- payload.append("]");
- } else {
- payload.append(createRandomValueExpression(schema, definitions, true, specification));
+ if (OasModelHelper.isCompositeSchema(schema)) {
+ createComposedSchema(randomModelBuilder, schema, true, specification,
+ visitedRefSchemas);
+ return;
}
- return payload.toString();
+ 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("\"\"");
+ }
}
/**
* 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,
+ Map definitions,
+ boolean quotes, OpenApiSpecification specification, TestContext context) {
if (context.getVariables().containsKey(name)) {
return CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX;
}
@@ -90,20 +143,12 @@ public static String createRandomValueExpression(String name, OasSchema schema,
return createRandomValueExpression(schema, definitions, quotes, specification);
}
- public static T createRawRandomValueExpression(String name, OasSchema schema, Map definitions,
- boolean quotes, OpenApiSpecification specification, TestContext context) {
- if (context.getVariables().containsKey(name)) {
- return (T)context.getVariables().get(CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX);
- }
-
- return createRawRandomValueExpression(schema, definitions, quotes, specification, context);
- }
-
/**
* Create payload from schema with random values.
*/
- public static String createRandomValueExpression(OasSchema schema, Map definitions, boolean quotes,
- OpenApiSpecification specification) {
+ 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);
@@ -112,31 +157,44 @@ public static String createRandomValueExpression(OasSchema schema, Map "'" + value + "'").collect(Collectors.joining(","))).append(")");
- } else if (schema.format != null && schema.format.equals("uuid")) {
+ } 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 {
- payload.append("citrus:randomString(").append(schema.maxLength != null && schema.maxLength.intValue() > 0 ? schema.maxLength : (schema.minLength != null && schema.minLength.intValue() > 0 ? schema.minLength : 10)).append(")");
+ 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 ("integer".equals(schema.type) || "number".equals(schema.type)) {
+ } else if (OpenApiUtils.isAnyNumberScheme(schema)) {
payload.append("citrus:randomNumber(8)");
- } else if ("boolean".equals(schema.type)) {
+ } else if (OpenApiConstants.TYPE_BOOLEAN.equals(schema.type)) {
payload.append("citrus:randomEnumValue('true', 'false')");
} else if (quotes) {
payload.append("\"\"");
@@ -145,69 +203,33 @@ public static String createRandomValueExpression(OasSchema schema, Map T createRawRandomValueExpression(OasSchema schema, Map definitions, boolean quotes,
+ 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);
+ return createRawRandomValueExpression(resolved, definitions, quotes, specification,
+ context);
}
StringBuilder payload = new StringBuilder();
- if ("string".equals(schema.type) || OasModelHelper.isObjectType(schema) || OasModelHelper.isArrayType(schema)) {
- return (T)createRandomValueExpression(schema, definitions, quotes, specification);
- } else if ("number".equals(schema.type)) {
- return (T)Double.valueOf(context.replaceDynamicContentInString("citrus:randomNumber(8,2)"));
+ 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)"));
+ 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')"));
+ return (T) Boolean.valueOf(
+ context.replaceDynamicContentInString("citrus:randomEnumValue('true', 'false')"));
} else if (quotes) {
payload.append("\"\"");
}
- return (T)payload.toString();
- }
-
- /**
- * Creates control payload from schema for validation.
- */
- public static String createInboundPayload(OasSchema schema, Map definitions,
- OpenApiSpecification specification) {
- if (OasModelHelper.isReferenceType(schema)) {
- OasSchema resolved = definitions.get(OasModelHelper.getReferenceName(schema.$ref));
- return createInboundPayload(resolved, definitions, specification);
- }
-
- StringBuilder payload = new StringBuilder();
- if (OasModelHelper.isObjectType(schema)) {
- payload.append("{");
-
- if (schema.properties != null) {
- for (Map.Entry entry : schema.properties.entrySet()) {
- if (specification.isValidateOptionalFields() || isRequired(schema, entry.getKey())) {
- payload.append("\"")
- .append(entry.getKey())
- .append("\": ")
- .append(createValidationExpression(entry.getValue(), definitions, true, specification))
- .append(",");
- }
- }
- }
-
- if (payload.toString().endsWith(",")) {
- payload.replace(payload.length() - 1, payload.length(), "");
- }
-
- payload.append("}");
- } else if (OasModelHelper.isArrayType(schema)) {
- payload.append("[");
- payload.append(createValidationExpression((OasSchema) schema.items, definitions, true, specification));
- payload.append("]");
- } else {
- payload.append(createValidationExpression(schema, definitions, false, specification));
- }
-
- return payload.toString();
+ return (T) payload.toString();
}
/**
@@ -222,182 +244,403 @@ private static boolean isRequired(OasSchema schema, String field) {
}
/**
- * Use test variable with given name if present or create validation expression using functions according to schema type and format.
+ * Use test variable with given name (if present) or create random value expression using
+ * functions according to schema type and format.
*/
- public static String createValidationExpression(String name, OasSchema schema, Map definitions,
- boolean quotes, OpenApiSpecification specification,
- TestContext context) {
+ public static String createRandomValueExpression(String name, OasSchema schema,
+ TestContext context) {
if (context.getVariables().containsKey(name)) {
return CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX;
}
- return createValidationExpression(schema, definitions, quotes, specification);
+ 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 validation expression using functions according to schema type and format.
+ * Create random value expression using functions according to schema type and format.
*/
- public static String createValidationExpression(OasSchema schema, Map definitions, boolean quotes,
- OpenApiSpecification specification) {
- if (OasModelHelper.isReferenceType(schema)) {
- OasSchema resolved = definitions.get(OasModelHelper.getReferenceName(schema.$ref));
- return createValidationExpression(resolved, definitions, quotes, specification);
- }
+ private static void createRandomValueExpressionMap(RandomModelBuilder randomModelBuilder,
+ OasSchema schema, boolean quotes) {
- StringBuilder payload = new StringBuilder();
- if (OasModelHelper.isObjectType(schema)) {
- payload.append("{");
-
- if (schema.properties != null) {
- for (Map.Entry entry : schema.properties.entrySet()) {
- if (specification.isValidateOptionalFields() || isRequired(schema, entry.getKey())) {
- payload.append("\"")
- .append(entry.getKey())
- .append("\": ")
- .append(createValidationExpression(entry.getValue(), definitions, quotes, specification))
- .append(",");
+ 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("");
+ }
+ }
- if (payload.toString().endsWith(",")) {
- payload.replace(payload.length() - 1, payload.length(), "");
- }
+ private static String createRandomNumber(OasSchema schema) {
+ Number multipleOf = schema.multipleOf;
- payload.append("}");
- } else {
- if (quotes) {
- payload.append("\"");
- }
+ boolean exclusiveMaximum = TRUE.equals(schema.exclusiveMaximum);
+ boolean exclusiveMinimum = TRUE.equals(schema.exclusiveMinimum);
- payload.append(createValidationExpression(schema));
+ BigDecimal[] bounds = determineBounds(schema);
- if (quotes) {
- payload.append("\"");
+ 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
+ );
+ }
+
+ /**
+ * 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)));
+ }
+ }
+
+ /**
+ * 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 payload.toString();
+ return new BigDecimal[]{bdMinimum, bdMaximum};
}
/**
- * Create validation expression using functions according to schema type and format.
+ * 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 String createValidationExpression(OasSchema schema) {
+ 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)) {
- /*
- * Currently these schemas are not supported by validation expressions. They are supported
- * by {@link org.citrusframework.openapi.validation.OpenApiValidator} though.
- */
- return "@ignore@";
+ createComposedSchema(randomModelBuilder, schema, quotes, specification,
+ visitedRefSchemas);
+ return;
}
switch (schema.type) {
- case "string":
- if (schema.format != null && schema.format.equals("date")) {
- return "@matchesDatePattern('yyyy-MM-dd')@";
- } else if (schema.format != null && schema.format.equals("date-time")) {
- return "@matchesDatePattern('yyyy-MM-dd'T'hh:mm:ssZ')@";
- } else if (StringUtils.hasText(schema.pattern)) {
- return String.format("@matches(%s)@", schema.pattern);
- } else if (!CollectionUtils.isEmpty(schema.enum_)) {
- return String.format("@matches(%s)@", String.join("|", schema.enum_));
+ 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 {
- return "@notEmpty()@";
+ randomModelBuilder.appendSimple("");
}
- case "number":
- case "integer":
- return "@isNumber()@";
- case "boolean":
- return "@matches(true|false)@";
- default:
- return "@ignore@";
+ }
}
}
- /**
- * Use test variable with given name (if present) or create random value expression using functions according to
- * schema type and format.
- */
- public static String createRandomValueExpression(String name, OasSchema schema, TestContext context) {
- if (context.getVariables().containsKey(name)) {
- return CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX;
+ 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);
}
+ }
- return createRandomValueExpression(schema);
+ 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));
}
- /**
- * Create random value expression using functions according to schema type and format.
- */
- public static String createRandomValueExpression(OasSchema schema) {
- switch (schema.type) {
- case "string":
- if (schema.format != null && schema.format.equals("date")) {
- return "\"citrus:currentDate('yyyy-MM-dd')\"";
- } else if (schema.format != null && schema.format.equals("date-time")) {
- return "\"citrus:currentDate('yyyy-MM-dd'T'hh:mm:ssZ')\"";
- } else if (StringUtils.hasText(schema.pattern)) {
- return "\"citrus:randomValue(" + schema.pattern + ")\"";
- } else if (!CollectionUtils.isEmpty(schema.enum_)) {
- return "\"citrus:randomEnumValue(" + (String.join(",", schema.enum_)) + ")\"";
- } else if (schema.format != null && schema.format.equals("uuid")){
- return "citrus:randomUUID()";
- } else {
- return "citrus:randomString(10)";
+ 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;
}
- case "number":
- case "integer":
- return "citrus:randomNumber(8)";
- case "boolean":
- return "citrus:randomEnumValue('true', 'false')";
- default:
- return "";
+ }
+
+ // 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 validation expression using regex according to schema type and format.
+ * Create a random array value.
+ *
+ * @param schema the type to create
+ * @param visitedRefSchemas the schemas already created during descent, used to avoid recursion
*/
- public static String createValidationRegex(String name, @Nullable OasSchema oasSchema) {
-
- if (oasSchema != null && (OasModelHelper.isReferenceType(oasSchema) || OasModelHelper.isObjectType(oasSchema))) {
- throw new CitrusRuntimeException(String.format("Unable to create a validation regex for an reference of object schema '%s'!", name));
+ @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!");
}
+ }
- return createValidationRegex(oasSchema);
+ 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);
+ }
+ });
}
- public static String createValidationRegex(@Nullable OasSchema schema) {
+ 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);
+ }
- if (schema == null) {
- return "";
- }
+ 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);
+ }
- switch (schema.type) {
- case "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")) {
- return "\\d{4}-\\d{2}-\\d{2}T[01]\\d:[0-5]\\d:[0-5]\\dZ";
- } else if (StringUtils.hasText(schema.pattern)) {
- return schema.pattern;
- } else if (!CollectionUtils.isEmpty(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 {
- return ".*";
- }
- case "number":
- return "[0-9]+\\.?[0-9]*";
- case "integer":
- return "[0-9]+";
- case "boolean":
- return "(true|false)";
- default:
- return "";
+ 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
new file mode 100644
index 0000000000..3f9e123679
--- /dev/null
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestValidationDataGenerator.java
@@ -0,0 +1,246 @@
+/*
+ * 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.openapi;
+
+import static org.citrusframework.util.StringUtils.hasText;
+import static org.springframework.util.CollectionUtils.isEmpty;
+
+import io.apicurio.datamodels.openapi.models.OasSchema;
+import jakarta.annotation.Nullable;
+import java.util.Map;
+import org.citrusframework.CitrusSettings;
+import org.citrusframework.context.TestContext;
+import org.citrusframework.exceptions.CitrusRuntimeException;
+import org.citrusframework.openapi.model.OasModelHelper;
+
+/**
+ * Generates proper payloads and validation expressions based on Open API specification rules.
+ * Creates outbound payloads with generated random test data according to specification and creates
+ * inbound payloads with proper validation expressions to enforce the specification rules.
+ *
+ * @author Christoph Deppisch
+ */
+public abstract class OpenApiTestValidationDataGenerator {
+
+ private OpenApiTestValidationDataGenerator() {
+ // Static access only
+ }
+
+ /**
+ * Creates control payload from schema for validation.
+ */
+ public static String createInboundPayload(OasSchema schema, Map definitions,
+ OpenApiSpecification specification) {
+ if (OasModelHelper.isReferenceType(schema)) {
+ OasSchema resolved = definitions.get(OasModelHelper.getReferenceName(schema.$ref));
+ return createInboundPayload(resolved, definitions, specification);
+ }
+
+ StringBuilder payload = new StringBuilder();
+ if (OasModelHelper.isObjectType(schema)) {
+ payload.append("{");
+
+ if (schema.properties != null) {
+ for (Map.Entry entry : schema.properties.entrySet()) {
+ if (specification.isValidateOptionalFields() || isRequired(schema,
+ entry.getKey())) {
+ payload.append("\"")
+ .append(entry.getKey())
+ .append("\": ")
+ .append(createValidationExpression(entry.getValue(), definitions, true,
+ specification))
+ .append(",");
+ }
+ }
+ }
+
+ if (payload.toString().endsWith(",")) {
+ payload.replace(payload.length() - 1, payload.length(), "");
+ }
+
+ payload.append("}");
+ } else if (OasModelHelper.isArrayType(schema)) {
+ payload.append("[");
+ payload.append(createValidationExpression((OasSchema) schema.items, definitions, true,
+ specification));
+ payload.append("]");
+ } else {
+ payload.append(createValidationExpression(schema, definitions, false, specification));
+ }
+
+ return 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;
+ }
+
+ return schema.required.contains(field);
+ }
+
+ /**
+ * Use test variable with given name if present or create validation expression using functions
+ * according to schema type and format.
+ */
+ public static String createValidationExpression(String name, OasSchema schema,
+ Map definitions,
+ boolean quotes, OpenApiSpecification specification,
+ TestContext context) {
+ if (context.getVariables().containsKey(name)) {
+ return CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX;
+ }
+
+ return createValidationExpression(schema, definitions, quotes, specification);
+ }
+
+ /**
+ * Create validation expression using functions according to schema type and format.
+ */
+ public static String createValidationExpression(OasSchema schema,
+ Map definitions, boolean quotes,
+ OpenApiSpecification specification) {
+ if (OasModelHelper.isReferenceType(schema)) {
+ OasSchema resolved = definitions.get(OasModelHelper.getReferenceName(schema.$ref));
+ return createValidationExpression(resolved, definitions, quotes, specification);
+ }
+
+ StringBuilder payload = new StringBuilder();
+ if (OasModelHelper.isObjectType(schema)) {
+ payload.append("{");
+
+ if (schema.properties != null) {
+ for (Map.Entry entry : schema.properties.entrySet()) {
+ if (specification.isValidateOptionalFields() || isRequired(schema,
+ entry.getKey())) {
+ payload.append("\"")
+ .append(entry.getKey())
+ .append("\": ")
+ .append(
+ createValidationExpression(entry.getValue(), definitions, quotes,
+ specification))
+ .append(",");
+ }
+ }
+ }
+
+ if (payload.toString().endsWith(",")) {
+ payload.replace(payload.length() - 1, payload.length(), "");
+ }
+
+ payload.append("}");
+ } else {
+ if (quotes) {
+ payload.append("\"");
+ }
+
+ payload.append(createValidationExpression(schema));
+
+ if (quotes) {
+ payload.append("\"");
+ }
+ }
+
+ return payload.toString();
+ }
+
+ /**
+ * Create validation expression using functions according to schema type and format.
+ */
+ private static String createValidationExpression(OasSchema schema) {
+
+ if (OasModelHelper.isCompositeSchema(schema)) {
+ /*
+ * Currently these schemas are not supported by validation expressions. They are supported
+ * by {@link org.citrusframework.openapi.validation.OpenApiValidator} though.
+ */
+ return "@ignore@";
+ }
+
+ switch (schema.type) {
+ case "string" :
+ if (schema.format != null && schema.format.equals("date")) {
+ return "@matchesDatePattern('yyyy-MM-dd')@";
+ } else if (schema.format != null && schema.format.equals("date-time")) {
+ return "@matchesDatePattern('yyyy-MM-dd'T'hh:mm:ssZ')@";
+ } else if (hasText(schema.pattern)) {
+ return String.format("@matches(%s)@", schema.pattern);
+ } else if (!isEmpty(schema.enum_)) {
+ return String.format("@matches(%s)@",
+ String.join("|", schema.enum_));
+ } else {
+ return "@notEmpty()@";
+ }
+ case OpenApiConstants.TYPE_NUMBER, OpenApiConstants.TYPE_INTEGER:
+ return "@isNumber()@";
+ case "boolean" :
+ return "@matches(true|false)@";
+ default:
+ return "@ignore@";
+ }
+ }
+
+ /**
+ * Create validation expression using regex according to schema type and format.
+ */
+ public static String createValidationRegex(String name, @Nullable OasSchema oasSchema) {
+
+ if (oasSchema != null && (OasModelHelper.isReferenceType(oasSchema)
+ || OasModelHelper.isObjectType(oasSchema))) {
+ throw new CitrusRuntimeException(String.format(
+ "Unable to create a validation regex for an reference of object schema '%s'!",
+ name));
+ }
+
+ return createValidationRegex(oasSchema);
+ }
+
+ public static String createValidationRegex(@Nullable OasSchema schema) {
+
+ if (schema == null) {
+ return "";
+ }
+
+ switch (schema.type) {
+ case "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")) {
+ return "\\d{4}-\\d{2}-\\d{2}T[01]\\d:[0-5]\\d:[0-5]\\dZ";
+ } else if (hasText(schema.pattern)) {
+ return schema.pattern;
+ } else if (!isEmpty(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 {
+ return ".*";
+ }
+ case OpenApiConstants.TYPE_NUMBER:
+ return "[0-9]+\\.?[0-9]*";
+ case "integer" :
+ return "[0-9]+";
+ case "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 07ab6bd1c6..616e5b119b 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
@@ -19,7 +19,12 @@
import io.apicurio.datamodels.openapi.models.OasOperation;
import io.apicurio.datamodels.openapi.models.OasParameter;
import io.apicurio.datamodels.openapi.models.OasSchema;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.regex.Pattern;
import org.citrusframework.CitrusSettings;
+import org.citrusframework.actions.SendMessageAction;
import org.citrusframework.context.TestContext;
import org.citrusframework.exceptions.CitrusRuntimeException;
import org.citrusframework.http.actions.HttpClientRequestActionBuilder;
@@ -34,17 +39,18 @@
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
-import java.util.List;
-import java.util.Locale;
-import java.util.Optional;
-import java.util.regex.Pattern;
-
/**
* @since 4.1
*/
public class OpenApiClientRequestActionBuilder extends HttpClientRequestActionBuilder {
- private final OpenApiRequestValidationProcessor openApiRequestValidationProcessor;
+ private final OpenApiSpecification openApiSpec;
+
+ private final String operationId;
+
+ private boolean oasValidationEnabled = true;
+
+ private OpenApiRequestValidationProcessor openApiRequestValidationProcessor;
/**
* Default constructor initializes http request message builder.
@@ -57,14 +63,23 @@ public OpenApiClientRequestActionBuilder(HttpMessage httpMessage, OpenApiSpecifi
String operationId) {
super(new OpenApiClientRequestMessageBuilder(httpMessage, openApiSpec, operationId), httpMessage);
- openApiRequestValidationProcessor = new OpenApiRequestValidationProcessor(openApiSpec, operationId);
- process(openApiRequestValidationProcessor);
- }
+ this.openApiSpec = openApiSpec;
+ this.operationId = operationId;
+ }
+
+ @Override
+ public SendMessageAction doBuild() {
- public OpenApiClientRequestActionBuilder disableOasValidation(boolean b) {
- if (openApiRequestValidationProcessor != null) {
- openApiRequestValidationProcessor.setEnabled(!b);
+ if (oasValidationEnabled && !messageProcessors.contains(openApiRequestValidationProcessor)) {
+ openApiRequestValidationProcessor = new OpenApiRequestValidationProcessor(openApiSpec, operationId);
+ process(openApiRequestValidationProcessor);
}
+
+ return super.doBuild();
+ }
+
+ public OpenApiClientRequestActionBuilder disableOasValidation(boolean disabled) {
+ oasValidationEnabled = !disabled;
return this;
}
@@ -117,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);
+ parameterValue = OpenApiTestDataGenerator.createRandomValueExpression((OasSchema) parameter.schema, false);
}
randomizedPath = Pattern.compile("\\{" + parameter.getName() + "}")
.matcher(randomizedPath)
@@ -171,4 +186,5 @@ private void setSpecifiedHeaders(TestContext context, OasOperation operation) {
});
}
}
+
}
diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientResponseActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientResponseActionBuilder.java
index de3998e10e..3bc4b9d1c4 100644
--- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientResponseActionBuilder.java
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientResponseActionBuilder.java
@@ -20,7 +20,12 @@
import io.apicurio.datamodels.openapi.models.OasResponse;
import io.apicurio.datamodels.openapi.models.OasSchema;
import jakarta.annotation.Nullable;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Pattern;
import org.citrusframework.CitrusSettings;
+import org.citrusframework.actions.ReceiveMessageAction;
import org.citrusframework.context.TestContext;
import org.citrusframework.exceptions.CitrusRuntimeException;
import org.citrusframework.http.actions.HttpClientResponseActionBuilder;
@@ -29,7 +34,7 @@
import org.citrusframework.message.Message;
import org.citrusframework.message.MessageType;
import org.citrusframework.openapi.OpenApiSpecification;
-import org.citrusframework.openapi.OpenApiTestDataGenerator;
+import org.citrusframework.openapi.OpenApiTestValidationDataGenerator;
import org.citrusframework.openapi.model.OasModelHelper;
import org.citrusframework.openapi.model.OperationPathAdapter;
import org.citrusframework.openapi.validation.OpenApiResponseValidationProcessor;
@@ -37,17 +42,18 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
-import java.util.Collection;
-import java.util.Map;
-import java.util.Optional;
-import java.util.regex.Pattern;
-
/**
* @since 4.1
*/
public class OpenApiClientResponseActionBuilder extends HttpClientResponseActionBuilder {
- private final OpenApiResponseValidationProcessor openApiResponseValidationProcessor;
+ private OpenApiResponseValidationProcessor openApiResponseValidationProcessor;
+
+ private final OpenApiSpecification openApiSpec;
+
+ private final String operationId;
+
+ private boolean oasValidationEnabled = true;
/**
* Default constructor initializes http response message builder.
@@ -62,15 +68,24 @@ public OpenApiClientResponseActionBuilder(HttpMessage httpMessage,
String operationId, String statusCode) {
super(new OpenApiClientResponseMessageBuilder(httpMessage, openApiSpec, operationId,
statusCode), httpMessage);
-
- openApiResponseValidationProcessor = new OpenApiResponseValidationProcessor(openApiSpec, operationId);
- validate(openApiResponseValidationProcessor);
+ this.openApiSpec = openApiSpec;
+ this.operationId = operationId;
}
- public OpenApiClientResponseActionBuilder disableOasValidation(boolean b) {
- if (openApiResponseValidationProcessor != null) {
- openApiResponseValidationProcessor.setEnabled(!b);
+ @Override
+ public ReceiveMessageAction doBuild() {
+
+ if (oasValidationEnabled && !messageProcessors.contains(openApiResponseValidationProcessor)) {
+ openApiResponseValidationProcessor = new OpenApiResponseValidationProcessor(openApiSpec, operationId);
+ validate(openApiResponseValidationProcessor);
}
+
+ return super.doBuild();
+ }
+
+ public OpenApiClientResponseActionBuilder disableOasValidation(boolean disable) {
+ oasValidationEnabled = !disable;
+ ((OpenApiClientResponseMessageBuilder)getMessageBuilderSupport().getMessageBuilder()).setOasValidationEnabled(oasValidationEnabled);
return this;
}
@@ -88,12 +103,10 @@ public static void fillMessageFromResponse(OpenApiSpecification openApiSpecifica
Optional responseSchema = OasModelHelper.getSchema(response);
responseSchema.ifPresent(oasSchema -> {
httpMessage.setPayload(
- OpenApiTestDataGenerator.createInboundPayload(oasSchema,
+ OpenApiTestValidationDataGenerator.createInboundPayload(oasSchema,
OasModelHelper.getSchemaDefinitions(
openApiSpecification.getOpenApiDoc(context)), openApiSpecification));
- // Best guess for the content type. Currently, we can only determine the content type
- // for sure for json. Other content types will be neglected.
OasSchema resolvedSchema = OasModelHelper.resolveSchema(
openApiSpecification.getOpenApiDoc(null), oasSchema);
if (OasModelHelper.isObjectType(resolvedSchema) || OasModelHelper.isObjectArrayType(
@@ -108,7 +121,6 @@ public static void fillMessageFromResponse(OpenApiSpecification openApiSpecifica
}
}
);
-
}
private static void fillRequiredHeaders(
@@ -118,7 +130,7 @@ private static void fillRequiredHeaders(
Map requiredHeaders = OasModelHelper.getRequiredHeaders(response);
for (Map.Entry header : requiredHeaders.entrySet()) {
httpMessage.setHeader(header.getKey(),
- OpenApiTestDataGenerator.createValidationExpression(header.getKey(),
+ OpenApiTestValidationDataGenerator.createValidationExpression(header.getKey(),
header.getValue(),
OasModelHelper.getSchemaDefinitions(
openApiSpecification.getOpenApiDoc(context)), false,
@@ -145,6 +157,8 @@ private static class OpenApiClientResponseMessageBuilder extends HttpMessageBuil
private final HttpMessage httpMessage;
+ private boolean oasValidationEnabled = true;
+
public OpenApiClientResponseMessageBuilder(HttpMessage httpMessage,
OpenApiSpecification openApiSpec,
String operationId, String statusCode) {
@@ -169,7 +183,7 @@ public Message build(TestContext context, String messageType) {
private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter, TestContext context) {
OasOperation operation = operationPathAdapter.operation();
- if (operation.responses != null) {
+ if (oasValidationEnabled && operation.responses != null) {
Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration(
openApiSpec.getOpenApiDoc(context), operation, statusCode, null);
@@ -184,5 +198,9 @@ private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter
httpMessage.status(HttpStatus.OK);
}
}
+
+ public void setOasValidationEnabled(boolean oasValidationEnabled) {
+ this.oasValidationEnabled = oasValidationEnabled;
+ }
}
}
diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerRequestActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerRequestActionBuilder.java
index 9d93fd2326..bfb8c4ea5c 100644
--- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerRequestActionBuilder.java
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerRequestActionBuilder.java
@@ -16,10 +16,23 @@
package org.citrusframework.openapi.actions;
+import static java.lang.String.format;
+import static org.citrusframework.message.MessageType.JSON;
+import static org.citrusframework.message.MessageType.PLAINTEXT;
+import static org.citrusframework.message.MessageType.XML;
+import static org.citrusframework.openapi.model.OasModelHelper.getRequestContentType;
+import static org.citrusframework.util.StringUtils.appendSegmentToUrlPath;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+import static org.springframework.http.MediaType.APPLICATION_XML_VALUE;
+
import io.apicurio.datamodels.openapi.models.OasOperation;
import io.apicurio.datamodels.openapi.models.OasParameter;
import io.apicurio.datamodels.openapi.models.OasSchema;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
import org.citrusframework.CitrusSettings;
+import org.citrusframework.actions.ReceiveMessageAction;
import org.citrusframework.context.TestContext;
import org.citrusframework.exceptions.CitrusRuntimeException;
import org.citrusframework.http.actions.HttpServerRequestActionBuilder;
@@ -27,32 +40,25 @@
import org.citrusframework.http.message.HttpMessageBuilder;
import org.citrusframework.message.Message;
import org.citrusframework.openapi.OpenApiSpecification;
-import org.citrusframework.openapi.OpenApiTestDataGenerator;
+import org.citrusframework.openapi.OpenApiTestValidationDataGenerator;
import org.citrusframework.openapi.model.OasModelHelper;
import org.citrusframework.openapi.model.OperationPathAdapter;
import org.citrusframework.openapi.validation.OpenApiRequestValidationProcessor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
-import java.util.List;
-import java.util.Optional;
-import java.util.regex.Pattern;
-
-import static java.lang.String.format;
-import static org.citrusframework.message.MessageType.JSON;
-import static org.citrusframework.message.MessageType.PLAINTEXT;
-import static org.citrusframework.message.MessageType.XML;
-import static org.citrusframework.openapi.model.OasModelHelper.getRequestContentType;
-import static org.citrusframework.util.StringUtils.appendSegmentToUrlPath;
-import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
-import static org.springframework.http.MediaType.APPLICATION_XML_VALUE;
-
/**
* @since 4.1
*/
public class OpenApiServerRequestActionBuilder extends HttpServerRequestActionBuilder {
- private final OpenApiRequestValidationProcessor openApiRequestValidationProcessor;
+ private OpenApiRequestValidationProcessor openApiRequestValidationProcessor;
+
+ private final OpenApiSpecification openApiSpec;
+
+ private final String operationId;
+
+ private boolean oasValidationEnabled = true;
/**
* Default constructor initializes http request message builder.
@@ -66,15 +72,23 @@ public OpenApiServerRequestActionBuilder(HttpMessage httpMessage,
String operationId) {
super(new OpenApiServerRequestMessageBuilder(httpMessage, openApiSpec, operationId),
httpMessage);
-
- openApiRequestValidationProcessor = new OpenApiRequestValidationProcessor(openApiSpec, operationId);
- validate(openApiRequestValidationProcessor);
+ this.openApiSpec = openApiSpec;
+ this.operationId = operationId;
}
- public OpenApiServerRequestActionBuilder disableOasValidation(boolean b) {
- if (openApiRequestValidationProcessor != null) {
- openApiRequestValidationProcessor.setEnabled(!b);
+ @Override
+ public ReceiveMessageAction doBuild() {
+
+ if (oasValidationEnabled && !messageProcessors.contains(openApiRequestValidationProcessor)) {
+ openApiRequestValidationProcessor = new OpenApiRequestValidationProcessor(openApiSpec, operationId);
+ validate(openApiRequestValidationProcessor);
}
+
+ return super.doBuild();
+ }
+
+ public OpenApiServerRequestActionBuilder disableOasValidation(boolean disable) {
+ oasValidationEnabled = !disable;
return this;
}
@@ -142,7 +156,7 @@ private void setSpecifiedBody(TestContext context, OperationPathAdapter operatio
Optional body = OasModelHelper.getRequestBodySchema(
openApiSpec.getOpenApiDoc(context), operationPathAdapter.operation());
body.ifPresent(oasSchema -> httpMessage.setPayload(
- OpenApiTestDataGenerator.createInboundPayload(oasSchema,
+ OpenApiTestValidationDataGenerator.createInboundPayload(oasSchema,
OasModelHelper.getSchemaDefinitions(
openApiSpec.getOpenApiDoc(context)), openApiSpec)));
}
@@ -161,7 +175,7 @@ private String determinePath(TestContext context, OasOperation operation,
.matcher(randomizedPath)
.replaceAll(parameterValue);
} else {
- parameterValue = OpenApiTestDataGenerator.createValidationRegex(
+ parameterValue = OpenApiTestValidationDataGenerator.createValidationRegex(
parameter.getName(),
OasModelHelper.getParameterSchema(parameter).orElse(null));
@@ -188,7 +202,7 @@ private void setSpecifiedQueryParameters(TestContext context,
param -> (param.required != null && param.required) || context.getVariables()
.containsKey(param.getName()))
.forEach(param -> httpMessage.queryParam(param.getName(),
- OpenApiTestDataGenerator.createValidationExpression(param.getName(),
+ OpenApiTestValidationDataGenerator.createValidationExpression(param.getName(),
OasModelHelper.getParameterSchema(param).orElse(null),
OasModelHelper.getSchemaDefinitions(openApiSpec.getOpenApiDoc(context)), false,
openApiSpec,
@@ -209,7 +223,7 @@ private void setSpecifiedHeaders(TestContext context,
param -> (param.required != null && param.required) || context.getVariables()
.containsKey(param.getName()))
.forEach(param -> httpMessage.setHeader(param.getName(),
- OpenApiTestDataGenerator.createValidationExpression(param.getName(),
+ OpenApiTestValidationDataGenerator.createValidationExpression(param.getName(),
OasModelHelper.getParameterSchema(param).orElse(null),
OasModelHelper.getSchemaDefinitions(openApiSpec.getOpenApiDoc(context)), false,
openApiSpec,
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 2c673b711a..d5914d336e 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
@@ -16,11 +16,29 @@
package org.citrusframework.openapi.actions;
+import static java.lang.Integer.parseInt;
+import static java.util.Collections.singletonMap;
+import static org.citrusframework.openapi.OpenApiTestDataGenerator.createOutboundPayload;
+import static org.citrusframework.openapi.OpenApiTestDataGenerator.createRandomValueExpression;
+import static org.springframework.http.HttpStatus.OK;
+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;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
import org.citrusframework.CitrusSettings;
+import org.citrusframework.actions.SendMessageAction;
import org.citrusframework.context.TestContext;
import org.citrusframework.exceptions.CitrusRuntimeException;
import org.citrusframework.http.actions.HttpServerResponseActionBuilder;
@@ -37,30 +55,18 @@
import org.citrusframework.openapi.validation.OpenApiResponseValidationProcessor;
import org.springframework.http.HttpStatus;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Predicate;
-import java.util.regex.Pattern;
-
-import static java.lang.Integer.parseInt;
-import static java.util.Collections.singletonMap;
-import static org.citrusframework.openapi.OpenApiTestDataGenerator.createOutboundPayload;
-import static org.citrusframework.openapi.OpenApiTestDataGenerator.createRandomValueExpression;
-import static org.springframework.http.HttpStatus.OK;
-import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
-import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;
-
/**
* @since 4.1
*/
public class OpenApiServerResponseActionBuilder extends HttpServerResponseActionBuilder {
- private final OpenApiResponseValidationProcessor openApiResponseValidationProcessor;
+ private OpenApiResponseValidationProcessor openApiResponseValidationProcessor;
+
+ private final OpenApiSpecification openApiSpec;
+
+ private final String operationId;
+
+ private boolean oasValidationEnabled = true;
/**
* Default constructor initializes http response message builder.
@@ -75,16 +81,23 @@ public OpenApiServerResponseActionBuilder(HttpMessage httpMessage,
String operationId, String statusCode, String accept) {
super(new OpenApiServerResponseMessageBuilder(httpMessage, openApiSpec, operationId,
statusCode, accept), httpMessage);
-
- openApiResponseValidationProcessor = new OpenApiResponseValidationProcessor(openApiSpec,
- operationId);
- process(openApiResponseValidationProcessor);
+ this.openApiSpec = openApiSpec;
+ this.operationId = operationId;
}
- public OpenApiServerResponseActionBuilder disableOasValidation(boolean b) {
- if (openApiResponseValidationProcessor != null) {
- openApiResponseValidationProcessor.setEnabled(!b);
+ @Override
+ public SendMessageAction doBuild() {
+
+ if (oasValidationEnabled && !messageProcessors.contains(openApiResponseValidationProcessor)) {
+ openApiResponseValidationProcessor = new OpenApiResponseValidationProcessor(openApiSpec, operationId);
+ process(openApiResponseValidationProcessor);
}
+
+ return super.doBuild();
+ }
+
+ public OpenApiServerResponseActionBuilder disableOasValidation(boolean disable) {
+ oasValidationEnabled = !disable;
return this;
}
diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java
index 854f8f743d..cfbffdf139 100644
--- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java
@@ -16,6 +16,10 @@
package org.citrusframework.openapi.model;
+import static java.util.Collections.singletonList;
+import static org.citrusframework.openapi.OpenApiConstants.TYPE_ARRAY;
+import static org.citrusframework.openapi.OpenApiConstants.TYPE_OBJECT;
+
import io.apicurio.datamodels.combined.visitors.CombinedVisitorAdapter;
import io.apicurio.datamodels.openapi.models.OasDocument;
import io.apicurio.datamodels.openapi.models.OasOperation;
@@ -36,11 +40,6 @@
import io.apicurio.datamodels.openapi.v3.models.Oas30Response;
import io.apicurio.datamodels.openapi.v3.models.Oas30Schema;
import jakarta.annotation.Nullable;
-import org.citrusframework.openapi.model.v2.Oas20ModelHelper;
-import org.citrusframework.openapi.model.v3.Oas30ModelHelper;
-import org.citrusframework.util.StringUtils;
-import org.springframework.http.MediaType;
-
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -53,12 +52,14 @@
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
-
-import static java.util.Collections.singletonList;
+import org.citrusframework.openapi.model.v2.Oas20ModelHelper;
+import org.citrusframework.openapi.model.v3.Oas30ModelHelper;
+import org.citrusframework.util.StringUtils;
+import org.springframework.http.MediaType;
public final class OasModelHelper {
- public static final String DEFAULT_ = "default_";
+ public static final String DEFAULT = "default_";
/**
* List of preferred media types in the order of priority,
@@ -76,7 +77,7 @@ private OasModelHelper() {
* @return true if given schema is an object.
*/
public static boolean isObjectType(@Nullable OasSchema schema) {
- return schema != null && "object".equals(schema.type);
+ return schema != null && TYPE_OBJECT.equals(schema.type);
}
/**
@@ -85,7 +86,7 @@ public static boolean isObjectType(@Nullable OasSchema schema) {
* @return true if given schema is an array.
*/
public static boolean isArrayType(@Nullable OasSchema schema) {
- return schema != null && "array".equals(schema.type);
+ return schema != null && TYPE_ARRAY.equals(schema.type);
}
/**
@@ -95,7 +96,7 @@ public static boolean isArrayType(@Nullable OasSchema schema) {
*/
public static boolean isObjectArrayType(@Nullable OasSchema schema) {
- if (schema == null || !"array".equals(schema.type)) {
+ if (schema == null || !TYPE_ARRAY.equals(schema.type)) {
return false;
}
@@ -290,7 +291,7 @@ public static Optional getResponseForRandomGeneration(OasDocument o
Predicate acceptedSchemas = resp -> getSchema(operation, resp, accept != null ? singletonList(accept) : DEFAULT_ACCEPTED_MEDIA_TYPES).isPresent();
// Fallback 1: Pick the default if it exists
- Optional response = Optional.ofNullable(responseMap.get(DEFAULT_));
+ Optional response = Optional.ofNullable(responseMap.get(DEFAULT));
if (response.isEmpty()) {
// Fallback 2: Pick the response object related to the first 2xx, providing an accepted schema
@@ -463,7 +464,7 @@ private static boolean isOas20(OasDocument openApiDoc) {
* This method iterates over the responses contained in the {@link OasResponses} object. If a response has a reference
* (indicated by a non-null {@code $ref} field), it resolves the reference and adds the resolved response to the result list.
* Non-referenced responses are added to the result list as-is. The resulting map includes the default response under
- * the key {@link OasModelHelper#DEFAULT_}, if it exists.
+ * the key {@link OasModelHelper#DEFAULT}, if it exists.
*
*
* @param responses the {@link OasResponses} instance containing the responses to be resolved.
@@ -491,10 +492,10 @@ private static Map resolveResponses(OasDocument openApiDoc,
if (responses.default_.$ref != null) {
OasResponse resolved = responseResolver.apply(responses.default_.$ref);
if (resolved != null) {
- responseMap.put(DEFAULT_, resolved);
+ responseMap.put(DEFAULT, resolved);
}
} else {
- responseMap.put(DEFAULT_, responses.default_);
+ responseMap.put(DEFAULT, responses.default_);
}
}
@@ -504,8 +505,8 @@ private static Map resolveResponses(OasDocument openApiDoc,
private static Function getResponseResolver(
OasDocument openApiDoc) {
return delegate(openApiDoc,
- (Function>) doc -> (responseRef -> doc.responses.getResponse(OasModelHelper.getReferenceName(responseRef))),
- (Function>) doc -> (responseRef -> doc.components.responses.get(OasModelHelper.getReferenceName(responseRef))));
+ doc -> (responseRef -> doc.responses.getResponse(OasModelHelper.getReferenceName(responseRef))),
+ doc -> (responseRef -> doc.components.responses.get(OasModelHelper.getReferenceName(responseRef))));
}
/**
diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OperationPathAdapter.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OperationPathAdapter.java
index 987ed05c90..c1a1999ac9 100644
--- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OperationPathAdapter.java
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OperationPathAdapter.java
@@ -17,7 +17,7 @@
package org.citrusframework.openapi.model;
import io.apicurio.datamodels.openapi.models.OasOperation;
-import org.citrusframework.openapi.OpenApiUtils;
+import org.citrusframework.openapi.util.OpenApiUtils;
import static java.lang.String.format;
diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiUtils.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java
similarity index 79%
rename from connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiUtils.java
rename to connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java
index 21fe3d32cc..dd6e48deb7 100644
--- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiUtils.java
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java
@@ -14,16 +14,18 @@
* limitations under the License.
*/
-package org.citrusframework.openapi;
+package org.citrusframework.openapi.util;
+
+import static java.lang.String.format;
import io.apicurio.datamodels.openapi.models.OasOperation;
+import io.apicurio.datamodels.openapi.models.OasSchema;
import jakarta.annotation.Nonnull;
import org.citrusframework.http.message.HttpMessage;
import org.citrusframework.http.message.HttpMessageHeaders;
+import org.citrusframework.openapi.OpenApiConstants;
import org.citrusframework.util.StringUtils;
-import static java.lang.String.format;
-
public class OpenApiUtils {
private OpenApiUtils() {
@@ -35,7 +37,7 @@ public static String getMethodPath(@Nonnull HttpMessage httpMessage) {
Object path = httpMessage.getHeader(HttpMessageHeaders.HTTP_REQUEST_URI);
return getMethodPath(methodHeader != null ? methodHeader.toString().toLowerCase() : "null",
- path != null? path.toString() : "null");
+ path != null ? path.toString() : "null");
}
public static String getMethodPath(@Nonnull String method, @Nonnull String path) {
@@ -52,4 +54,12 @@ public static String createFullPathOperationIdentifier(String path, OasOperation
return format("%s_%s", oasOperation.getMethod().toUpperCase(), path);
}
+ public static boolean isAnyNumberScheme(OasSchema schema) {
+ return (
+ schema != null &&
+ (OpenApiConstants.TYPE_INTEGER.equalsIgnoreCase(schema.type) ||
+ OpenApiConstants.TYPE_NUMBER.equalsIgnoreCase(schema.type))
+ );
+ }
+
}
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/util/RandomElement.java
new file mode 100644
index 0000000000..4a31733459
--- /dev/null
+++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/RandomElement.java
@@ -0,0 +1,116 @@
+/*
+ * 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.openapi.util;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+
+/**
+ * Interface representing a random element in a JSON structure. This interface provides default
+ * methods to push values into the element, which can be overridden by implementing classes.
+ */
+public interface RandomElement {
+
+ default void push(Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ default void push(String key, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * A random element representing an array. Array elements can be of type String (native
+ * attribute) or {@link RandomElement}.
+ */
+ class RandomList extends ArrayList