From 69349998f2e0cc652c15f50858077ccd847c5f26 Mon Sep 17 00:00:00 2001 From: Thorsten Schlathoelter Date: Sun, 21 Jul 2024 07:33:39 +0200 Subject: [PATCH] feat(#1175): adds open api validation by standard citrus schema validaton mechanism --- connectors/citrus-openapi/pom.xml | 2 +- .../openapi/OpenApiMessageHeaders.java | 24 +++ .../openapi/OpenApiMessageType.java | 22 +++ .../openapi/OpenApiRepository.java | 21 ++- .../openapi/OpenApiSpecification.java | 6 +- .../openapi/actions/OpenApiActionBuilder.java | 45 +++-- .../actions/OpenApiClientActionBuilder.java | 20 ++- .../OpenApiClientRequestActionBuilder.java | 127 +++++++++----- .../OpenApiClientResponseActionBuilder.java | 46 +++-- .../actions/OpenApiServerActionBuilder.java | 16 +- .../OpenApiServerRequestActionBuilder.java | 74 ++++---- .../OpenApiServerResponseActionBuilder.java | 77 ++++---- .../actions/OpenApiSpecificationSource.java | 61 +++++++ .../openapi/model/OperationPathAdapter.java | 2 +- .../openapi/util/OpenApiUtils.java | 17 ++ .../validation/OpenApiMessageProcessor.java | 56 ++++++ .../OpenApiMessageValidationContext.java | 117 ++++++++++++ .../OpenApiRequestValidationProcessor.java | 3 + .../validation/OpenApiRequestValidator.java | 11 ++ .../OpenApiResponseValidationProcessor.java | 57 ------ .../validation/OpenApiResponseValidator.java | 18 +- .../validation/OpenApiSchemaValidation.java | 166 ++++++++++++++++++ .../citrus/message/schemaValidator/openApi | 2 + .../META-INF/citrus/message/validator/openApi | 2 + .../openapi/OpenApiMessageTypeTest.java | 18 ++ .../openapi/OpenApiRepositoryTest.java | 4 +- .../openapi/OpenApiUtilsTest.java | 60 +++++++ .../openapi/groovy/OpenApiClientTest.java | 6 +- .../openapi/integration/OpenApiClientIT.java | 19 +- .../openapi/integration/OpenApiServerIT.java | 50 +++--- .../model/OperationPathAdapterTest.java | 3 +- .../OpenApiMessageProcessorTest.java | 59 +++++++ ...penApiResponseValidationProcessorTest.java | 123 ------------- .../openapi/xml/OpenApiClientTest.java | 6 +- .../openapi/yaml/OpenApiClientTest.java | 6 +- .../validation/MessageValidatorRegistry.java | 13 +- .../validation/SchemaValidator.java | 12 ++ .../actions/SendMessageAction.java | 105 ++++------- .../HttpClientRequestActionBuilder.java | 7 +- .../HttpClientResponseActionBuilder.java | 9 +- .../HttpServerRequestActionBuilder.java | 9 +- .../HttpServerResponseActionBuilder.java | 6 +- pom.xml | 2 +- test-api-generator/pom.xml | 1 + .../json/schema/JsonSchemaValidation.java | 38 +++- .../json/SendMessageActionTest.java | 71 +++++--- .../xml/schema/XmlSchemaValidation.java | 54 ++++-- .../validation/xml/SendMessageActionTest.java | 11 +- 48 files changed, 1173 insertions(+), 511 deletions(-) create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageHeaders.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageType.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiSpecificationSource.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiMessageProcessor.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiMessageValidationContext.java delete mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessor.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiSchemaValidation.java create mode 100644 connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/schemaValidator/openApi create mode 100644 connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/validator/openApi create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiMessageTypeTest.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiMessageProcessorTest.java delete mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessorTest.java diff --git a/connectors/citrus-openapi/pom.xml b/connectors/citrus-openapi/pom.xml index 556d6f70d9..a0c362f21c 100644 --- a/connectors/citrus-openapi/pom.xml +++ b/connectors/citrus-openapi/pom.xml @@ -48,7 +48,7 @@ com.atlassian.oai swagger-request-validator-core - 2.40.0 + ${swagger-request-validator.version} com.fasterxml.jackson.datatype diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageHeaders.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageHeaders.java new file mode 100644 index 0000000000..5629c91c36 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageHeaders.java @@ -0,0 +1,24 @@ +package org.citrusframework.openapi; + +import org.citrusframework.message.MessageHeaders; + +public class OpenApiMessageHeaders { + + public static final String OAS_PREFIX = MessageHeaders.PREFIX + "oas_"; + + public static final String OAS_OPERATION = OAS_PREFIX + "operation"; + + public static final String OAS_MEDIA_TYPE = OAS_PREFIX + "media_type"; + + public static final String OAS_UNIQUE_OPERATION_ID = OAS_PREFIX + "unique_operation_id"; + + public static final String OAS_MESSAGE_TYPE = OAS_PREFIX + "message_type"; + + public static final String RESPONSE_TYPE = OAS_PREFIX + "response"; + + public static final String REQUEST_TYPE = OAS_PREFIX + "request"; + + private OpenApiMessageHeaders() { + // Static access only + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageType.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageType.java new file mode 100644 index 0000000000..e6b747d5ae --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageType.java @@ -0,0 +1,22 @@ +package org.citrusframework.openapi; + +/** + * The {@code OpenApiMessageType} enum defines the types of OpenAPI messages, + * specifically REQUEST and RESPONSE. Each type is associated with a specific + * header name, which is used to identify the type of message in the OpenAPI + * message headers. + */ +public enum OpenApiMessageType { + + REQUEST(OpenApiMessageHeaders.REQUEST_TYPE), RESPONSE(OpenApiMessageHeaders.RESPONSE_TYPE); + + private final String headerName; + + OpenApiMessageType(String headerName) { + this.headerName = headerName; + } + + public String toHeaderName() { + return headerName; + } +} 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 20f845d604..7479040c33 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 @@ -115,7 +115,12 @@ static Optional determineResourceAlias(Resource openApiResource) { try { File file = openApiResource.getFile(); if (file != null) { - return Optional.of(file.getName()); + resourceAlias = file.getName(); + int index = resourceAlias.lastIndexOf("."); + if (index != -1 && index != resourceAlias.length()-1) { + resourceAlias = resourceAlias.substring(0, index); + } + return Optional.of(resourceAlias); } } catch (Exception e) { // Ignore and try with url @@ -130,6 +135,11 @@ static Optional determineResourceAlias(Resource openApiResource) { if (index != -1 && index != urlString.length()-1) { resourceAlias = resourceAlias.substring(index+1); } + index = resourceAlias.lastIndexOf("."); + if (index != -1 && index != resourceAlias.length()-1) { + resourceAlias = resourceAlias.substring(0, index); + } + } } catch (MalformedURLException e) { logger.error("Unable to determine resource alias from resource!", e); @@ -141,4 +151,13 @@ static Optional determineResourceAlias(Resource openApiResource) { public List getOpenApiSpecifications() { return openApiSpecifications; } + + public OpenApiRepository locations(List locations) { + setLocations(locations); + return this; + } + + public OpenApiSpecification openApi(String alias) { + return getOpenApiSpecifications().stream().filter(spec -> spec.getAliases().contains(alias)).findFirst().orElse(null); + } } 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 f27f1bed48..b344e7d9cd 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 @@ -323,13 +323,13 @@ private void storeOperationPathAdapter(OasOperation operation, String path) { String basePath = OasModelHelper.getBasePath(openApiDoc); String fullOperationPath = StringUtils.appendSegmentToUrlPath(basePath, path); - OperationPathAdapter operationPathAdapter = new OperationPathAdapter(path, rootContextPath, - StringUtils.appendSegmentToUrlPath(rootContextPath, path), operation); - String uniqueOperationId = OpenApiUtils.createFullPathOperationIdentifier(fullOperationPath, operation); operationToUniqueId.put(operation, uniqueOperationId); + OperationPathAdapter operationPathAdapter = new OperationPathAdapter(path, rootContextPath, + StringUtils.appendSegmentToUrlPath(rootContextPath, path), operation, uniqueOperationId); + operationIdToOperationPathAdapter.put(uniqueOperationId, operationPathAdapter); if (StringUtils.hasText(operation.operationId)) { operationIdToOperationPathAdapter.put(operation.operationId, operationPathAdapter); diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiActionBuilder.java index cee2f0c207..73ece7f474 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiActionBuilder.java @@ -35,13 +35,17 @@ */ public class OpenApiActionBuilder extends AbstractReferenceResolverAwareTestActionBuilder { - private OpenApiSpecification specification; + private OpenApiSpecificationSource openApiSpecificationSource; public OpenApiActionBuilder() { } public OpenApiActionBuilder(OpenApiSpecification specification) { - this.specification = specification; + this.openApiSpecificationSource = new OpenApiSpecificationSource(specification); + } + + public OpenApiActionBuilder(String openApiAlias) { + this.openApiSpecificationSource = new OpenApiSpecificationSource(openApiAlias); } /** @@ -55,8 +59,17 @@ public static OpenApiActionBuilder openapi(OpenApiSpecification specification) { return new OpenApiActionBuilder(specification); } + public static OpenApiActionBuilder openapi(String openApiAlias) { + return new OpenApiActionBuilder(openApiAlias); + } + public OpenApiActionBuilder specification(OpenApiSpecification specification) { - this.specification = specification; + this.openApiSpecificationSource = new OpenApiSpecificationSource(specification); + return this; + } + + public OpenApiActionBuilder specificationByAlias(String openApiAlias) { + this.openApiSpecificationSource = new OpenApiSpecificationSource(openApiAlias); return this; } @@ -70,7 +83,11 @@ public OpenApiActionBuilder specification(String specUrl) { public OpenApiClientActionBuilder client() { assertSpecification(); - return client(specification.getRequestUrl()); + OpenApiClientActionBuilder clientActionBuilder = new OpenApiClientActionBuilder( + openApiSpecificationSource) + .withReferenceResolver(referenceResolver); + this.delegate = clientActionBuilder; + return clientActionBuilder; } /** @@ -80,10 +97,11 @@ public OpenApiClientActionBuilder client(HttpClient httpClient) { assertSpecification(); if (httpClient.getEndpointConfiguration().getRequestUrl() != null) { - specification.setRequestUrl(httpClient.getEndpointConfiguration().getRequestUrl()); + openApiSpecificationSource.setHttpClient(httpClient.getEndpointConfiguration().getRequestUrl()); } - OpenApiClientActionBuilder clientActionBuilder = new OpenApiClientActionBuilder(httpClient, specification) + OpenApiClientActionBuilder clientActionBuilder = new OpenApiClientActionBuilder(httpClient, + openApiSpecificationSource) .withReferenceResolver(referenceResolver); this.delegate = clientActionBuilder; return clientActionBuilder; @@ -95,9 +113,10 @@ public OpenApiClientActionBuilder client(HttpClient httpClient) { public OpenApiClientActionBuilder client(String httpClient) { assertSpecification(); - specification.setHttpClient(httpClient); + openApiSpecificationSource.setHttpClient(httpClient); - OpenApiClientActionBuilder clientActionBuilder = new OpenApiClientActionBuilder(httpClient, specification) + OpenApiClientActionBuilder clientActionBuilder = new OpenApiClientActionBuilder(httpClient, + openApiSpecificationSource) .withReferenceResolver(referenceResolver); this.delegate = clientActionBuilder; return clientActionBuilder; @@ -109,15 +128,16 @@ public OpenApiClientActionBuilder client(String httpClient) { public OpenApiServerActionBuilder server(Endpoint endpoint) { assertSpecification(); - OpenApiServerActionBuilder serverActionBuilder = new OpenApiServerActionBuilder(endpoint, specification) + OpenApiServerActionBuilder serverActionBuilder = new OpenApiServerActionBuilder(endpoint, + openApiSpecificationSource) .withReferenceResolver(referenceResolver); this.delegate = serverActionBuilder; return serverActionBuilder; } private void assertSpecification() { - if (specification == null) { - throw new CitrusRuntimeException("Invalid OpenApi specification - please set specification first"); + if (openApiSpecificationSource == null) { + throw new CitrusRuntimeException("Invalid OpenApiSpecificationSource - please set specification first"); } } @@ -127,7 +147,8 @@ private void assertSpecification() { public OpenApiServerActionBuilder server(String httpServer) { assertSpecification(); - OpenApiServerActionBuilder serverActionBuilder = new OpenApiServerActionBuilder(httpServer, specification) + OpenApiServerActionBuilder serverActionBuilder = new OpenApiServerActionBuilder(httpServer, + openApiSpecificationSource) .withReferenceResolver(referenceResolver); this.delegate = serverActionBuilder; return serverActionBuilder; diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilder.java index a80cd953ba..ed1b099e6b 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilder.java @@ -31,7 +31,7 @@ */ public class OpenApiClientActionBuilder extends AbstractReferenceResolverAwareTestActionBuilder { - private final OpenApiSpecification specification; + private final OpenApiSpecificationSource openApiSpecificationSource; /** Target http client instance */ private Endpoint httpClient; @@ -40,24 +40,29 @@ public class OpenApiClientActionBuilder extends AbstractReferenceResolverAwareTe /** * Default constructor. */ - public OpenApiClientActionBuilder(Endpoint httpClient, OpenApiSpecification specification) { + public OpenApiClientActionBuilder(Endpoint httpClient, OpenApiSpecificationSource openApiSpecificationSource) { this.httpClient = httpClient; - this.specification = specification; + this.openApiSpecificationSource = openApiSpecificationSource; } /** * Default constructor. */ - public OpenApiClientActionBuilder(String httpClientUri, OpenApiSpecification specification) { + public OpenApiClientActionBuilder(String httpClientUri, OpenApiSpecificationSource openApiSpecificationSource) { this.httpClientUri = httpClientUri; - this.specification = specification; + this.openApiSpecificationSource = openApiSpecificationSource; + } + + public OpenApiClientActionBuilder(OpenApiSpecificationSource openApiSpecificationSource) { + this.openApiSpecificationSource = openApiSpecificationSource; } /** * Sends Http requests as client. */ public OpenApiClientRequestActionBuilder send(String operationId) { - OpenApiClientRequestActionBuilder builder = new OpenApiClientRequestActionBuilder(specification, operationId); + OpenApiClientRequestActionBuilder builder = new OpenApiClientRequestActionBuilder( + openApiSpecificationSource, operationId); if (httpClient != null) { builder.endpoint(httpClient); } else { @@ -90,7 +95,8 @@ public OpenApiClientResponseActionBuilder receive(String operationId, HttpStatus * Receives Http response messages as client. */ public OpenApiClientResponseActionBuilder receive(String operationId, String statusCode) { - OpenApiClientResponseActionBuilder builder = new OpenApiClientResponseActionBuilder(specification, operationId, statusCode); + OpenApiClientResponseActionBuilder builder = new OpenApiClientResponseActionBuilder( + openApiSpecificationSource, operationId, statusCode); if (httpClient != null) { builder.endpoint(httpClient); } else { 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 a7722559f1..ee009fe5aa 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 @@ -31,11 +31,12 @@ import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; import org.citrusframework.message.Message; +import org.citrusframework.openapi.OpenApiMessageType; import org.citrusframework.openapi.OpenApiSpecification; import org.citrusframework.openapi.OpenApiTestDataGenerator; import org.citrusframework.openapi.model.OasModelHelper; import org.citrusframework.openapi.model.OperationPathAdapter; -import org.citrusframework.openapi.validation.OpenApiRequestValidationProcessor; +import org.citrusframework.openapi.validation.OpenApiMessageProcessor; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -44,40 +45,56 @@ */ public class OpenApiClientRequestActionBuilder extends HttpClientRequestActionBuilder { - private final OpenApiSpecification openApiSpec; + private OpenApiMessageProcessor openApiMessageProcessor; + + private final OpenApiSpecificationSource openApiSpecificationSource; private final String operationId; private boolean oasValidationEnabled = true; - private OpenApiRequestValidationProcessor openApiRequestValidationProcessor; - /** * Default constructor initializes http request message builder. */ - public OpenApiClientRequestActionBuilder(OpenApiSpecification openApiSpec, String operationId) { + public OpenApiClientRequestActionBuilder(OpenApiSpecificationSource openApiSpec, + String operationId) { this(new HttpMessage(), openApiSpec, operationId); } - public OpenApiClientRequestActionBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, - String operationId) { - super(new OpenApiClientRequestMessageBuilder(httpMessage, openApiSpec, operationId), httpMessage); + public OpenApiClientRequestActionBuilder(HttpMessage httpMessage, + OpenApiSpecificationSource openApiSpec, + String operationId) { + super(new OpenApiClientRequestMessageBuilder(httpMessage, openApiSpec, operationId), + httpMessage); - this.openApiSpec = openApiSpec; + this.openApiSpecificationSource = openApiSpec; this.operationId = operationId; - } + } @Override public SendMessageAction doBuild() { - - if (oasValidationEnabled && !messageProcessors.contains(openApiRequestValidationProcessor)) { - openApiRequestValidationProcessor = new OpenApiRequestValidationProcessor(openApiSpec, operationId); - process(openApiRequestValidationProcessor); + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve( + referenceResolver); + if (oasValidationEnabled && !messageProcessors.contains( + openApiMessageProcessor)) { + openApiMessageProcessor = new OpenApiMessageProcessor(openApiSpecification, operationId, + OpenApiMessageType.REQUEST); + process(openApiMessageProcessor); } return super.doBuild(); } + /** + * By default, enable schema validation as the OpenAPI is always available. + */ + @Override + protected HttpMessageBuilderSupport createHttpMessageBuilderSupport() { + HttpMessageBuilderSupport httpMessageBuilderSupport = super.createHttpMessageBuilderSupport(); + httpMessageBuilderSupport.schemaValidation(true); + return httpMessageBuilderSupport; + } + public OpenApiClientRequestActionBuilder disableOasValidation(boolean disabled) { oasValidationEnabled = !disabled; return this; @@ -85,41 +102,52 @@ public OpenApiClientRequestActionBuilder disableOasValidation(boolean disabled) private static class OpenApiClientRequestMessageBuilder extends HttpMessageBuilder { - private final OpenApiSpecification openApiSpec; + private final OpenApiSpecificationSource openApiSpecificationSource; + private final String operationId; private final HttpMessage httpMessage; - public OpenApiClientRequestMessageBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, - String operationId) { + public OpenApiClientRequestMessageBuilder(HttpMessage httpMessage, + OpenApiSpecificationSource openApiSpec, + String operationId) { super(httpMessage); - this.openApiSpec = openApiSpec; + this.openApiSpecificationSource = openApiSpec; this.operationId = operationId; this.httpMessage = httpMessage; } @Override public Message build(TestContext context, String messageType) { - openApiSpec.getOperation(operationId, context).ifPresentOrElse(operationPathAdapter -> - buildMessageFromOperation(operationPathAdapter, context), () -> { - throw new CitrusRuntimeException("Unable to locate operation with id '%s' in OpenAPI specification %s".formatted(operationId, openApiSpec.getSpecUrl())); - }); + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve( + context.getReferenceResolver()); + openApiSpecification.getOperation(operationId, context) + .ifPresentOrElse(operationPathAdapter -> + buildMessageFromOperation(openApiSpecification, operationPathAdapter, context), + () -> { + throw new CitrusRuntimeException( + "Unable to locate operation with id '%s' in OpenAPI specification %s".formatted( + operationId, openApiSpecification.getSpecUrl())); + }); return super.build(context, messageType); } - private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter, TestContext context) { - OasOperation operation = operationPathAdapter.operation(); - String path = operationPathAdapter.apiPath(); - HttpMethod method = HttpMethod.valueOf(operationPathAdapter.operation().getMethod().toUpperCase(Locale.US)); + private void buildMessageFromOperation(OpenApiSpecification openApiSpecification, + OperationPathAdapter operationPathAdapter, TestContext context) { + OasOperation operation = operationPathAdapter.operation(); + String path = operationPathAdapter.apiPath(); + HttpMethod method = HttpMethod.valueOf( + operationPathAdapter.operation().getMethod().toUpperCase(Locale.US)); if (operation.parameters != null) { - setSpecifiedHeaders(context, operation); + setSpecifiedHeaders(openApiSpecification, context, operation); setSpecifiedQueryParameters(context, operation); } - if(httpMessage.getPayload() == null || (httpMessage.getPayload() instanceof String p && p.isEmpty())) { - setSpecifiedBody(context, operation); + if (httpMessage.getPayload() == null || (httpMessage.getPayload() instanceof String p + && p.isEmpty())) { + setSpecifiedBody(openApiSpecification, context, operation); } String randomizedPath = path; @@ -130,9 +158,11 @@ private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter for (OasParameter parameter : pathParams) { String parameterValue; if (context.getVariables().containsKey(parameter.getName())) { - parameterValue = "\\" + CitrusSettings.VARIABLE_PREFIX + parameter.getName() + CitrusSettings.VARIABLE_SUFFIX; + parameterValue = "\\" + CitrusSettings.VARIABLE_PREFIX + parameter.getName() + + CitrusSettings.VARIABLE_SUFFIX; } else { - parameterValue = OpenApiTestDataGenerator.createRandomValueExpression((OasSchema) parameter.schema); + parameterValue = OpenApiTestDataGenerator.createRandomValueExpression( + (OasSchema) parameter.schema); } randomizedPath = Pattern.compile("\\{" + parameter.getName() + "}") .matcher(randomizedPath) @@ -141,44 +171,55 @@ private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter } OasModelHelper.getRequestContentType(operation) - .ifPresent(contentType -> httpMessage.setHeader(HttpHeaders.CONTENT_TYPE, contentType)); + .ifPresent( + contentType -> httpMessage.setHeader(HttpHeaders.CONTENT_TYPE, contentType)); httpMessage.path(randomizedPath); httpMessage.method(method); - } - private void setSpecifiedBody(TestContext context, OasOperation operation) { + private void setSpecifiedBody(OpenApiSpecification openApiSpecification, + TestContext context, OasOperation operation) { Optional body = OasModelHelper.getRequestBodySchema( - openApiSpec.getOpenApiDoc(context), operation); - body.ifPresent(oasSchema -> httpMessage.setPayload(OpenApiTestDataGenerator.createOutboundPayload(oasSchema, openApiSpec))); + openApiSpecification.getOpenApiDoc(context), operation); + body.ifPresent(oasSchema -> httpMessage.setPayload( + OpenApiTestDataGenerator.createOutboundPayload(oasSchema, + openApiSpecification))); } private void setSpecifiedQueryParameters(TestContext context, OasOperation operation) { operation.parameters.stream() .filter(param -> "query".equals(param.in)) - .filter(param -> (param.required != null && param.required) || context.getVariables().containsKey(param.getName())) + .filter( + param -> (param.required != null && param.required) || context.getVariables() + .containsKey(param.getName())) .forEach(param -> { - if(!httpMessage.getQueryParams().containsKey(param.getName())) { + if (!httpMessage.getQueryParams().containsKey(param.getName())) { httpMessage.queryParam(param.getName(), - OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), (OasSchema) param.schema, + OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), + (OasSchema) param.schema, context)); } }); } - private void setSpecifiedHeaders(TestContext context, OasOperation operation) { + private void setSpecifiedHeaders(OpenApiSpecification openApiSpecification, TestContext context, OasOperation operation) { List configuredHeaders = getHeaderBuilders() .stream() .flatMap(b -> b.builderHeaders(context).keySet().stream()) .toList(); operation.parameters.stream() .filter(param -> "header".equals(param.in)) - .filter(param -> (param.required != null && param.required) || context.getVariables().containsKey(param.getName())) + .filter( + param -> (param.required != null && param.required) || context.getVariables() + .containsKey(param.getName())) .forEach(param -> { - if(httpMessage.getHeader(param.getName()) == null && !configuredHeaders.contains(param.getName())) { + if (httpMessage.getHeader(param.getName()) == null + && !configuredHeaders.contains(param.getName())) { httpMessage.setHeader(param.getName(), - OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), (OasSchema) param.schema, openApiSpec, context)); + OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), + (OasSchema) param.schema, + openApiSpecification, context)); } }); } 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 3bc4b9d1c4..adf514cc0d 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 @@ -16,6 +16,9 @@ package org.citrusframework.openapi.actions; +import static org.citrusframework.openapi.OpenApiMessageType.RESPONSE; +import static org.citrusframework.openapi.validation.OpenApiMessageValidationContext.Builder.openApi; + import io.apicurio.datamodels.openapi.models.OasOperation; import io.apicurio.datamodels.openapi.models.OasResponse; import io.apicurio.datamodels.openapi.models.OasSchema; @@ -37,7 +40,7 @@ import org.citrusframework.openapi.OpenApiTestValidationDataGenerator; import org.citrusframework.openapi.model.OasModelHelper; import org.citrusframework.openapi.model.OperationPathAdapter; -import org.citrusframework.openapi.validation.OpenApiResponseValidationProcessor; +import org.citrusframework.openapi.validation.OpenApiMessageProcessor; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -47,9 +50,9 @@ */ public class OpenApiClientResponseActionBuilder extends HttpClientResponseActionBuilder { - private OpenApiResponseValidationProcessor openApiResponseValidationProcessor; + private OpenApiMessageProcessor openApiMessageProcessor; - private final OpenApiSpecification openApiSpec; + private final OpenApiSpecificationSource openApiSpecificationSource; private final String operationId; @@ -58,26 +61,30 @@ public class OpenApiClientResponseActionBuilder extends HttpClientResponseAction /** * Default constructor initializes http response message builder. */ - public OpenApiClientResponseActionBuilder(OpenApiSpecification openApiSpec, String operationId, + public OpenApiClientResponseActionBuilder(OpenApiSpecificationSource openApiSpec, String operationId, String statusCode) { this(new HttpMessage(), openApiSpec, operationId, statusCode); } public OpenApiClientResponseActionBuilder(HttpMessage httpMessage, - OpenApiSpecification openApiSpec, + OpenApiSpecificationSource openApiSpecificationSource, String operationId, String statusCode) { - super(new OpenApiClientResponseMessageBuilder(httpMessage, openApiSpec, operationId, + super(new OpenApiClientResponseMessageBuilder(httpMessage, openApiSpecificationSource, operationId, statusCode), httpMessage); - this.openApiSpec = openApiSpec; + this.openApiSpecificationSource = openApiSpecificationSource; this.operationId = operationId; } @Override public ReceiveMessageAction doBuild() { - if (oasValidationEnabled && !messageProcessors.contains(openApiResponseValidationProcessor)) { - openApiResponseValidationProcessor = new OpenApiResponseValidationProcessor(openApiSpec, operationId); - validate(openApiResponseValidationProcessor); + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve(referenceResolver); + validate(openApi(openApiSpecification)); + if (oasValidationEnabled && !messageProcessors.contains(openApiMessageProcessor)) { + openApiMessageProcessor = new OpenApiMessageProcessor( + openApiSpecification, operationId, + RESPONSE); + process(openApiMessageProcessor); } return super.doBuild(); @@ -151,7 +158,7 @@ private static void fillRequiredHeaders( private static class OpenApiClientResponseMessageBuilder extends HttpMessageBuilder { - private final OpenApiSpecification openApiSpec; + private final OpenApiSpecificationSource openApiSpecificationSource; private final String operationId; private final String statusCode; @@ -160,10 +167,10 @@ private static class OpenApiClientResponseMessageBuilder extends HttpMessageBuil private boolean oasValidationEnabled = true; public OpenApiClientResponseMessageBuilder(HttpMessage httpMessage, - OpenApiSpecification openApiSpec, + OpenApiSpecificationSource oopenApiSpecificationSourceenApiSpec, String operationId, String statusCode) { super(httpMessage); - this.openApiSpec = openApiSpec; + this.openApiSpecificationSource = oopenApiSpecificationSourceenApiSpec; this.operationId = operationId; this.statusCode = statusCode; this.httpMessage = httpMessage; @@ -172,23 +179,24 @@ public OpenApiClientResponseMessageBuilder(HttpMessage httpMessage, @Override public Message build(TestContext context, String messageType) { - openApiSpec.getOperation(operationId, context).ifPresentOrElse(operationPathAdapter -> - buildMessageFromOperation(operationPathAdapter, context), () -> { - throw new CitrusRuntimeException("Unable to locate operation with id '%s' in OpenAPI specification %s".formatted(operationId, openApiSpec.getSpecUrl())); + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve(context.getReferenceResolver()); + openApiSpecification.getOperation(operationId, context).ifPresentOrElse(operationPathAdapter -> + buildMessageFromOperation(openApiSpecification, operationPathAdapter, context), () -> { + throw new CitrusRuntimeException("Unable to locate operation with id '%s' in OpenAPI specification %s".formatted(operationId, openApiSpecification.getSpecUrl())); }); return super.build(context, messageType); } - private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter, TestContext context) { + private void buildMessageFromOperation(OpenApiSpecification openApiSpecification, OperationPathAdapter operationPathAdapter, TestContext context) { OasOperation operation = operationPathAdapter.operation(); if (oasValidationEnabled && operation.responses != null) { Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration( - openApiSpec.getOpenApiDoc(context), operation, statusCode, null); + openApiSpecification.getOpenApiDoc(context), operation, statusCode, null); responseForRandomGeneration.ifPresent( - oasResponse -> fillMessageFromResponse(openApiSpec, context, httpMessage, + oasResponse -> fillMessageFromResponse(openApiSpecification, context, httpMessage, operation, oasResponse)); } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilder.java index 304893486b..e0404854e8 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilder.java @@ -31,7 +31,7 @@ */ public class OpenApiServerActionBuilder extends AbstractReferenceResolverAwareTestActionBuilder { - private final OpenApiSpecification specification; + private final OpenApiSpecificationSource openApiSpecificationSource; /** Target http client instance */ private Endpoint httpServer; @@ -40,24 +40,25 @@ public class OpenApiServerActionBuilder extends AbstractReferenceResolverAwareTe /** * Default constructor. */ - public OpenApiServerActionBuilder(Endpoint httpServer, OpenApiSpecification specification) { + public OpenApiServerActionBuilder(Endpoint httpServer, OpenApiSpecificationSource specification) { this.httpServer = httpServer; - this.specification = specification; + this.openApiSpecificationSource = specification; } /** * Default constructor. */ - public OpenApiServerActionBuilder(String httpServerUri, OpenApiSpecification specification) { + public OpenApiServerActionBuilder(String httpServerUri, OpenApiSpecificationSource specification) { this.httpServerUri = httpServerUri; - this.specification = specification; + this.openApiSpecificationSource = specification; } /** * Receive Http requests as server. */ public OpenApiServerRequestActionBuilder receive(String operationId) { - OpenApiServerRequestActionBuilder builder = new OpenApiServerRequestActionBuilder(specification, operationId); + OpenApiServerRequestActionBuilder builder = new OpenApiServerRequestActionBuilder( + openApiSpecificationSource, operationId); if (httpServer != null) { builder.endpoint(httpServer); } else { @@ -104,7 +105,8 @@ public OpenApiServerResponseActionBuilder send(String operationId, String status * Send Http response messages as server to client. */ public OpenApiServerResponseActionBuilder send(String operationId, String statusCode, String accept) { - OpenApiServerResponseActionBuilder builder = new OpenApiServerResponseActionBuilder(specification, operationId, statusCode, accept); + OpenApiServerResponseActionBuilder builder = new OpenApiServerResponseActionBuilder( + openApiSpecificationSource, operationId, statusCode, accept); if (httpServer != null) { builder.endpoint(httpServer); } else { 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 bfb8c4ea5c..6ace067846 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 @@ -39,11 +39,12 @@ import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; import org.citrusframework.message.Message; +import org.citrusframework.openapi.OpenApiMessageType; import org.citrusframework.openapi.OpenApiSpecification; 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.citrusframework.openapi.validation.OpenApiMessageProcessor; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -52,9 +53,9 @@ */ public class OpenApiServerRequestActionBuilder extends HttpServerRequestActionBuilder { - private OpenApiRequestValidationProcessor openApiRequestValidationProcessor; + private OpenApiMessageProcessor openApiMessageProcessor; - private final OpenApiSpecification openApiSpec; + private final OpenApiSpecificationSource openApiSpecificationSource; private final String operationId; @@ -63,25 +64,27 @@ public class OpenApiServerRequestActionBuilder extends HttpServerRequestActionBu /** * Default constructor initializes http request message builder. */ - public OpenApiServerRequestActionBuilder(OpenApiSpecification openApiSpec, String operationId) { - this(new HttpMessage(), openApiSpec, operationId); + public OpenApiServerRequestActionBuilder(OpenApiSpecificationSource openApiSpecificationSource, String operationId) { + this(new HttpMessage(), openApiSpecificationSource, operationId); } public OpenApiServerRequestActionBuilder(HttpMessage httpMessage, - OpenApiSpecification openApiSpec, + OpenApiSpecificationSource openApiSpecificationSource, String operationId) { - super(new OpenApiServerRequestMessageBuilder(httpMessage, openApiSpec, operationId), + super(new OpenApiServerRequestMessageBuilder(httpMessage, openApiSpecificationSource, operationId), httpMessage); - this.openApiSpec = openApiSpec; + this.openApiSpecificationSource = openApiSpecificationSource; this.operationId = operationId; } @Override public ReceiveMessageAction doBuild() { - if (oasValidationEnabled && !messageProcessors.contains(openApiRequestValidationProcessor)) { - openApiRequestValidationProcessor = new OpenApiRequestValidationProcessor(openApiSpec, operationId); - validate(openApiRequestValidationProcessor); + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve(referenceResolver); + if (oasValidationEnabled && !messageProcessors.contains(openApiMessageProcessor)) { + openApiMessageProcessor = new OpenApiMessageProcessor( + openApiSpecification, operationId, OpenApiMessageType.REQUEST); + process(openApiMessageProcessor); } return super.doBuild(); @@ -94,16 +97,16 @@ public OpenApiServerRequestActionBuilder disableOasValidation(boolean disable) { private static class OpenApiServerRequestMessageBuilder extends HttpMessageBuilder { - private final OpenApiSpecification openApiSpec; + private final OpenApiSpecificationSource openApiSpecificationSource; private final String operationId; private final HttpMessage httpMessage; public OpenApiServerRequestMessageBuilder(HttpMessage httpMessage, - OpenApiSpecification openApiSpec, + OpenApiSpecificationSource openApiSpec, String operationId) { super(httpMessage); - this.openApiSpec = openApiSpec; + this.openApiSpecificationSource = openApiSpec; this.operationId = operationId; this.httpMessage = httpMessage; } @@ -111,21 +114,23 @@ public OpenApiServerRequestMessageBuilder(HttpMessage httpMessage, @Override public Message build(TestContext context, String messageType) { - openApiSpec.getOperation(operationId, context).ifPresentOrElse(operationPathAdapter -> - buildMessageFromOperation(operationPathAdapter, context), () -> { - throw new CitrusRuntimeException("Unable to locate operation with id '%s' in OpenAPI specification %s".formatted(operationId, openApiSpec.getSpecUrl())); + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve(context.getReferenceResolver()); + + openApiSpecification.getOperation(operationId, context).ifPresentOrElse(operationPathAdapter -> + buildMessageFromOperation(openApiSpecification, operationPathAdapter, context), () -> { + throw new CitrusRuntimeException("Unable to locate operation with id '%s' in OpenAPI specification %s".formatted(operationId, openApiSpecification.getSpecUrl())); }); return super.build(context, messageType); } - private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter, TestContext context) { + private void buildMessageFromOperation(OpenApiSpecification openApiSpecification, OperationPathAdapter operationPathAdapter, TestContext context) { setSpecifiedMessageType(operationPathAdapter); - setSpecifiedHeaders(context, operationPathAdapter); - setSpecifiedQueryParameters(context, operationPathAdapter); - setSpecifiedPath(context, operationPathAdapter); - setSpecifiedBody(context, operationPathAdapter); + setSpecifiedHeaders(context, openApiSpecification, operationPathAdapter); + setSpecifiedQueryParameters(context, openApiSpecification, operationPathAdapter); + setSpecifiedPath(context, openApiSpecification, operationPathAdapter); + setSpecifiedBody(context, openApiSpecification, operationPathAdapter); setSpecifiedRequestContentType(operationPathAdapter); setSpecifiedMethod(operationPathAdapter); @@ -137,12 +142,12 @@ private void setSpecifiedRequestContentType(OperationPathAdapter operationPathAd String.format("@startsWith(%s)@", contentType))); } - private void setSpecifiedPath(TestContext context, OperationPathAdapter operationPathAdapter) { - String randomizedPath = OasModelHelper.getBasePath(openApiSpec.getOpenApiDoc(context)) + private void setSpecifiedPath(TestContext context, OpenApiSpecification openApiSpecification, OperationPathAdapter operationPathAdapter) { + String randomizedPath = OasModelHelper.getBasePath(openApiSpecification.getOpenApiDoc(context)) + operationPathAdapter.apiPath(); randomizedPath = randomizedPath.replace("//", "/"); - randomizedPath = appendSegmentToUrlPath(openApiSpec.getRootContextPath(), randomizedPath); + randomizedPath = appendSegmentToUrlPath(openApiSpecification.getRootContextPath(), randomizedPath); if (operationPathAdapter.operation().parameters != null) { randomizedPath = determinePath(context, operationPathAdapter.operation(), @@ -152,13 +157,14 @@ private void setSpecifiedPath(TestContext context, OperationPathAdapter operatio httpMessage.path(randomizedPath); } - private void setSpecifiedBody(TestContext context, OperationPathAdapter operationPathAdapter) { + private void setSpecifiedBody(TestContext context, OpenApiSpecification openApiSpecification, OperationPathAdapter operationPathAdapter) { Optional body = OasModelHelper.getRequestBodySchema( - openApiSpec.getOpenApiDoc(context), operationPathAdapter.operation()); + openApiSpecification.getOpenApiDoc(context), operationPathAdapter.operation()); body.ifPresent(oasSchema -> httpMessage.setPayload( OpenApiTestValidationDataGenerator.createInboundPayload(oasSchema, OasModelHelper.getSchemaDefinitions( - openApiSpec.getOpenApiDoc(context)), openApiSpec))); + openApiSpecification.getOpenApiDoc(context)), + openApiSpecification))); } private String determinePath(TestContext context, OasOperation operation, @@ -189,7 +195,7 @@ private String determinePath(TestContext context, OasOperation operation, return randomizedPath; } - private void setSpecifiedQueryParameters(TestContext context, + private void setSpecifiedQueryParameters(TestContext context, OpenApiSpecification openApiSpecification, OperationPathAdapter operationPathAdapter) { if (operationPathAdapter.operation().parameters == null) { @@ -204,13 +210,13 @@ private void setSpecifiedQueryParameters(TestContext context, .forEach(param -> httpMessage.queryParam(param.getName(), OpenApiTestValidationDataGenerator.createValidationExpression(param.getName(), OasModelHelper.getParameterSchema(param).orElse(null), - OasModelHelper.getSchemaDefinitions(openApiSpec.getOpenApiDoc(context)), false, - openApiSpec, + OasModelHelper.getSchemaDefinitions(openApiSpecification.getOpenApiDoc(context)), false, + openApiSpecification, context))); } - private void setSpecifiedHeaders(TestContext context, + private void setSpecifiedHeaders(TestContext context, OpenApiSpecification openApiSpecification, OperationPathAdapter operationPathAdapter) { if (operationPathAdapter.operation().parameters == null) { @@ -225,8 +231,8 @@ private void setSpecifiedHeaders(TestContext context, .forEach(param -> httpMessage.setHeader(param.getName(), OpenApiTestValidationDataGenerator.createValidationExpression(param.getName(), OasModelHelper.getParameterSchema(param).orElse(null), - OasModelHelper.getSchemaDefinitions(openApiSpec.getOpenApiDoc(context)), false, - openApiSpec, + OasModelHelper.getSchemaDefinitions(openApiSpecification.getOpenApiDoc(context)), false, + openApiSpecification, context))); } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerResponseActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerResponseActionBuilder.java index 3b4a522f92..4518f1e6b3 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 @@ -37,6 +37,7 @@ import java.util.function.Predicate; import java.util.regex.Pattern; import org.citrusframework.CitrusSettings; +import org.citrusframework.openapi.OpenApiMessageType; import org.citrusframework.actions.SendMessageAction; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -51,7 +52,7 @@ import org.citrusframework.openapi.model.OasAdapter; import org.citrusframework.openapi.model.OasModelHelper; import org.citrusframework.openapi.model.OperationPathAdapter; -import org.citrusframework.openapi.validation.OpenApiResponseValidationProcessor; +import org.citrusframework.openapi.validation.OpenApiMessageProcessor; import org.springframework.http.HttpStatus; /** @@ -59,9 +60,9 @@ */ public class OpenApiServerResponseActionBuilder extends HttpServerResponseActionBuilder { - private OpenApiResponseValidationProcessor openApiResponseValidationProcessor; + private OpenApiMessageProcessor openApiMessageProcessor; - private final OpenApiSpecification openApiSpec; + private final OpenApiSpecificationSource openApiSpecificationSource; private final String operationId; @@ -70,26 +71,27 @@ public class OpenApiServerResponseActionBuilder extends HttpServerResponseAction /** * Default constructor initializes http response message builder. */ - public OpenApiServerResponseActionBuilder(OpenApiSpecification openApiSpec, String operationId, + public OpenApiServerResponseActionBuilder(OpenApiSpecificationSource openApiSpecificationSource, String operationId, String statusCode, String accept) { - this(new HttpMessage(), openApiSpec, operationId, statusCode, accept); + this(new HttpMessage(), openApiSpecificationSource, operationId, statusCode, accept); } public OpenApiServerResponseActionBuilder(HttpMessage httpMessage, - OpenApiSpecification openApiSpec, + OpenApiSpecificationSource openApiSpecificationSource, String operationId, String statusCode, String accept) { - super(new OpenApiServerResponseMessageBuilder(httpMessage, openApiSpec, operationId, + super(new OpenApiServerResponseMessageBuilder(httpMessage, openApiSpecificationSource, operationId, statusCode, accept), httpMessage); - this.openApiSpec = openApiSpec; + this.openApiSpecificationSource = openApiSpecificationSource; this.operationId = operationId; } @Override public SendMessageAction doBuild() { - - if (oasValidationEnabled && !messageProcessors.contains(openApiResponseValidationProcessor)) { - openApiResponseValidationProcessor = new OpenApiResponseValidationProcessor(openApiSpec, operationId); - process(openApiResponseValidationProcessor); + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve(referenceResolver); + if (oasValidationEnabled && !messageProcessors.contains(openApiMessageProcessor)) { + openApiMessageProcessor = new OpenApiMessageProcessor( + openApiSpecification, operationId, OpenApiMessageType.RESPONSE); + process(openApiMessageProcessor); } return super.doBuild(); @@ -100,6 +102,16 @@ public OpenApiServerResponseActionBuilder disableOasValidation(boolean disable) return this; } + /** + * By default, enable schema validation as the OpenAPI is always available. + */ + @Override + protected HttpMessageBuilderSupport createMessageBuilderSupport() { + HttpMessageBuilderSupport messageBuilderSupport = super.createMessageBuilderSupport(); + messageBuilderSupport.schemaValidation(true); + return messageBuilderSupport; + } + public OpenApiServerResponseActionBuilder enableRandomGeneration(boolean enable) { ((OpenApiServerResponseMessageBuilder)getMessageBuilderSupport().getMessageBuilder()).enableRandomGeneration(enable); return this; @@ -109,17 +121,17 @@ private static class OpenApiServerResponseMessageBuilder extends HttpMessageBuil private static final Pattern STATUS_CODE_PATTERN = Pattern.compile("\\d+"); - private final OpenApiSpecification openApiSpec; + private final OpenApiSpecificationSource openApiSpecificationSource; private final String operationId; private final String statusCode; private final String accept; private boolean randomGenerationEnabled = true; public OpenApiServerResponseMessageBuilder(HttpMessage httpMessage, - OpenApiSpecification openApiSpec, + OpenApiSpecificationSource openApiSpecificationSource, String operationId, String statusCode, String accept) { super(httpMessage); - this.openApiSpec = openApiSpec; + this.openApiSpecificationSource = openApiSpecificationSource; this.operationId = operationId; this.statusCode = statusCode; this.accept = accept; @@ -133,6 +145,7 @@ public OpenApiServerResponseMessageBuilder enableRandomGeneration(boolean enable @Override public Message build(TestContext context, String messageType) { + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve(context.getReferenceResolver()); if (STATUS_CODE_PATTERN.matcher(statusCode).matches()) { getMessage().status(HttpStatus.valueOf(parseInt(statusCode))); } else { @@ -143,12 +156,12 @@ public Message build(TestContext context, String messageType) { getHeaderBuilders().clear(); if (randomGenerationEnabled) { - openApiSpec.getOperation(operationId, context) + openApiSpecification.getOperation(operationId, context) .ifPresentOrElse(operationPathAdapter -> - fillRandomData(operationPathAdapter, context), () -> { + fillRandomData(openApiSpecification, operationPathAdapter, context), () -> { throw new CitrusRuntimeException( "Unable to locate operation with id '%s' in OpenAPI specification %s".formatted( - operationId, openApiSpec.getSpecUrl())); + operationId, openApiSpecification.getSpecUrl())); }); } @@ -158,25 +171,25 @@ public Message build(TestContext context, String messageType) { return super.build(context, messageType); } - private void fillRandomData(OperationPathAdapter operationPathAdapter, TestContext context) { + private void fillRandomData(OpenApiSpecification openApiSpecification, OperationPathAdapter operationPathAdapter, TestContext context) { if (operationPathAdapter.operation().responses != null) { - buildResponse(context, operationPathAdapter.operation()); + buildResponse(context, openApiSpecification, operationPathAdapter.operation()); } } - private void buildResponse(TestContext context, OasOperation operation) { + private void buildResponse(TestContext context, OpenApiSpecification openApiSpecification, OasOperation operation) { Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration( - openApiSpec.getOpenApiDoc(context), operation, statusCode, null); + openApiSpecification.getOpenApiDoc(context), operation, statusCode, null); if (responseForRandomGeneration.isPresent()) { - buildRandomHeaders(context, responseForRandomGeneration.get()); - buildRandomPayload(operation, responseForRandomGeneration.get()); + buildRandomHeaders(context, openApiSpecification, responseForRandomGeneration.get()); + buildRandomPayload(openApiSpecification, operation, responseForRandomGeneration.get()); } } - private void buildRandomHeaders(TestContext context, OasResponse response) { + private void buildRandomHeaders(TestContext context, OpenApiSpecification openApiSpecification, OasResponse response) { Set filteredHeaders = new HashSet<>(getMessage().getHeaders().keySet()); Predicate> filteredHeadersPredicate = entry -> !filteredHeaders.contains( entry.getKey()); @@ -188,7 +201,7 @@ private void buildRandomHeaders(TestContext context, OasResponse response) { .forEach(entry -> addHeaderBuilder(new DefaultHeaderBuilder( singletonMap(entry.getKey(), createRandomValueExpression(entry.getKey(), entry.getValue(), - openApiSpec, + openApiSpecification, context)))) ); @@ -205,7 +218,7 @@ private void buildRandomHeaders(TestContext context, OasResponse response) { + CitrusSettings.VARIABLE_SUFFIX))))); } - private void buildRandomPayload(OasOperation operation, OasResponse response) { + private void buildRandomPayload(OpenApiSpecification openApiSpecification, OasOperation operation, OasResponse response) { Optional> schemaForMediaTypeOptional; if (statusCode.startsWith("2")) { @@ -222,7 +235,7 @@ private void buildRandomPayload(OasOperation operation, OasResponse response) { OasAdapter schemaForMediaType = schemaForMediaTypeOptional.get(); if (getMessage().getPayload() == null || ( getMessage().getPayload() instanceof String string && string.isEmpty())) { - createRandomPayload(getMessage(), schemaForMediaType); + createRandomPayload(getMessage(), openApiSpecification, schemaForMediaType); } // If we have a schema and a media type and the content type has not yet been set, do it. @@ -233,7 +246,7 @@ private void buildRandomPayload(OasOperation operation, OasResponse response) { } } - private void createRandomPayload(HttpMessage message, OasAdapter schemaForMediaType) { + private void createRandomPayload(HttpMessage message, OpenApiSpecification openApiSpecification, OasAdapter schemaForMediaType) { if (schemaForMediaType.node() == null) { // No schema means no payload, no type @@ -241,11 +254,13 @@ private void createRandomPayload(HttpMessage message, OasAdapter openApiRepository.openApi(openApiAlias)).filter( + Objects::nonNull).findFirst().orElseThrow(() -> + new CitrusRuntimeException( + "Unable to resolve OpenApiSpecification from alias '%s'. Known aliases for open api specs are '%s'".formatted( + openApiAlias, OpenApiUtils.getKnownOpenApiAliases(resolver))) + ); + } else { + throw new CitrusRuntimeException( + "Unable to resolve OpenApiSpecification. Neither OpenAPI spec, nor OpenAPI alias are specified."); + } + } + + if (httpClient != null) { + openApiSpecification.setHttpClient(httpClient); + } + + return openApiSpecification; + } + + public void setHttpClient(String httpClient) { + this.httpClient = httpClient; + } +} 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 c1a1999ac9..e5aebaea7e 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 @@ -30,7 +30,7 @@ * @param fullPath The full path combining context path and API path. * @param operation The {@link OasOperation} object representing the operation details. */ -public record OperationPathAdapter(String apiPath, String contextPath, String fullPath, OasOperation operation) { +public record OperationPathAdapter(String apiPath, String contextPath, String fullPath, OasOperation operation, String uniqueOperationId) { @Override public String toString() { diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java index ae4008111a..3e9ea0951e 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java @@ -21,9 +21,12 @@ import io.apicurio.datamodels.openapi.models.OasOperation; import io.apicurio.datamodels.openapi.models.OasSchema; import jakarta.annotation.Nonnull; +import java.util.stream.Collectors; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageHeaders; import org.citrusframework.openapi.OpenApiConstants; +import org.citrusframework.openapi.OpenApiRepository; +import org.citrusframework.spi.ReferenceResolver; import org.citrusframework.util.StringUtils; public class OpenApiUtils { @@ -73,4 +76,18 @@ public static boolean isRequired(OasSchema schema, String field) { return schema.required.contains(field); } + /** + * Retrieves all known OpenAPI aliases from {@link org.citrusframework.openapi.OpenApiSpecification}s + * registered in {@link OpenApiRepository}s. + * + * @param resolver the {@code ReferenceResolver} to use for resolving {@code OpenApiRepository} instances. + * @return a comma-separated string of all known OpenAPI aliases. + */ + public static String getKnownOpenApiAliases(ReferenceResolver resolver) { + return resolver.resolveAll(OpenApiRepository.class).values() + .stream().flatMap( + openApiRepository -> openApiRepository.getOpenApiSpecifications() + .stream()).flatMap(spec -> spec.getAliases().stream()).collect( + Collectors.joining(", ")); + } } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiMessageProcessor.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiMessageProcessor.java new file mode 100644 index 0000000000..d20c899fc2 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiMessageProcessor.java @@ -0,0 +1,56 @@ +/* + * 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.validation; + +import org.citrusframework.openapi.OpenApiMessageType; +import org.citrusframework.context.TestContext; +import org.citrusframework.message.Message; +import org.citrusframework.message.MessageProcessor; +import org.citrusframework.openapi.OpenApiMessageHeaders; +import org.citrusframework.openapi.OpenApiSpecification; + +/** + * {@code MessageProcessor} that prepares the message for OpenAPI validation by setting respective + * message headers. + */ +public class OpenApiMessageProcessor implements MessageProcessor { + + private final OpenApiSpecification openApiSpecification; + + private final String operationId; + + private final OpenApiMessageType type; + + public OpenApiMessageProcessor(OpenApiSpecification openApiSpecification, + String operationId, OpenApiMessageType type) { + this.operationId = operationId; + this.openApiSpecification = openApiSpecification; + this.type = type; + } + + @Override + public void process(Message message, TestContext context) { + + openApiSpecification + .getOperation(operationId, context) + .ifPresent(operationPathAdapter -> { + // Store the uniqueId of the operation, rather than the operationId, to avoid clashes. + message.setHeader(OpenApiMessageHeaders.OAS_UNIQUE_OPERATION_ID, operationPathAdapter.uniqueOperationId()); + message.setHeader(OpenApiMessageHeaders.OAS_MESSAGE_TYPE, type.toHeaderName()); + }); + } +} \ No newline at end of file diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiMessageValidationContext.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiMessageValidationContext.java new file mode 100644 index 0000000000..cc79a4a5a4 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiMessageValidationContext.java @@ -0,0 +1,117 @@ +package org.citrusframework.openapi.validation; + +import org.citrusframework.openapi.OpenApiSettings; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.validation.context.DefaultValidationContext; +import org.citrusframework.validation.context.SchemaValidationContext; +import org.citrusframework.validation.context.ValidationContext; + +/** + * Validation context holding OpenAPI specific validation information. + * + * @since 4.3 + */ +public class OpenApiMessageValidationContext extends DefaultValidationContext implements + SchemaValidationContext { + + /** + * Should message be validated with its schema definition. This is enabled with respect to + * global settings, which are true by default. as only messages processed by open api actions + * will be processed and validation information will be derived from open api spec. + * + *

Note that the default registered validation context is used for received messages. This is + * why the schema validation is initialized with response validation enabled globally. + */ + private boolean schemaValidation = OpenApiSettings.isResponseValidationEnabledGlobally(); + + + public OpenApiMessageValidationContext(Builder builder) { + super(); + + // If not explicitly specified, goe for the default. + this.schemaValidation = builder.schemaValidation != null ? builder.schemaValidation + : builder.openApiSpecification.isApiRequestValidationEnabled() + || builder.openApiSpecification.isApiResponseValidationEnabled(); + + } + + @Override + public boolean isSchemaValidationEnabled() { + return schemaValidation; + } + + @Override + public String getSchemaRepository() { + return null; + } + + @Override + public String getSchema() { + return null; + } + + /** + * Fluent builder + */ + public static final class Builder implements + ValidationContext.Builder, + SchemaValidationContext.Builder { + + private OpenApiSpecification openApiSpecification; + + /** + * Mapped as object to be able to indicate "not explicitly set" in which case the default is + * chosen. + * + *

Note that a message validation context is explicitly created only for send messages, + * whereas default request validation enabled is chosen as default value. + */ + private Boolean schemaValidation = OpenApiSettings.isRequestValidationEnabledGlobally(); + + public static OpenApiMessageValidationContext.Builder openApi( + OpenApiSpecification openApiSpecification) { + Builder builder = new Builder(); + builder.openApiSpecification = openApiSpecification; + return builder; + } + + public OpenApiMessageValidationContext.Builder expressions() { + return new OpenApiMessageValidationContext.Builder(); + } + + public OpenApiMessageValidationContext.Builder expression(String path, + Object expectedValue) { + return new OpenApiMessageValidationContext.Builder().expression(path, expectedValue); + } + + /** + * Sets schema validation enabled/disabled for this message. + */ + public OpenApiMessageValidationContext.Builder schemaValidation(final boolean enabled) { + this.schemaValidation = enabled; + return this; + } + + /** + * Not used for open api validation. Schema is automatically be derived from associated openApiSpecification. + */ + @Override + public OpenApiMessageValidationContext.Builder schema(final String schemaName) { + return this; + } + + /** + * Not used for open api validation. Schema is automatically be derived from associated openApiSpecification. + */ + @Override + public OpenApiMessageValidationContext.Builder schemaRepository( + final String schemaRepository) { + return this; + } + + @Override + public OpenApiMessageValidationContext build() { + return new OpenApiMessageValidationContext(this); + } + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiRequestValidationProcessor.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiRequestValidationProcessor.java index cb14d44c89..e8c49cb2b6 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiRequestValidationProcessor.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiRequestValidationProcessor.java @@ -19,6 +19,7 @@ import org.citrusframework.context.TestContext; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.message.Message; +import org.citrusframework.openapi.OpenApiMessageHeaders; import org.citrusframework.openapi.OpenApiSpecification; import org.citrusframework.validation.ValidationProcessor; @@ -49,6 +50,8 @@ public void validate(Message message, TestContext context) { return; } + message.setHeader(OpenApiMessageHeaders.OAS_UNIQUE_OPERATION_ID, operationId); + openApiSpecification.getOperation( operationId, context).ifPresent(operationPathAdapter -> openApiRequestValidator.validateRequest(operationPathAdapter, httpMessage)); diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiRequestValidator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiRequestValidator.java index 94aa08ae9a..b37e53f660 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiRequestValidator.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiRequestValidator.java @@ -57,6 +57,17 @@ public void validateRequest(OperationPathAdapter operationPathAdapter, } } + public ValidationReport validateRequestToReport(OperationPathAdapter operationPathAdapter, + HttpMessage requestMessage) { + + if (enabled && openApiInteractionValidator != null) { + return openApiInteractionValidator.validateRequest( + createRequestFromMessage(operationPathAdapter, requestMessage)); + } + + return ValidationReport.empty(); + } + Request createRequestFromMessage(OperationPathAdapter operationPathAdapter, HttpMessage httpMessage) { var payload = httpMessage.getPayload(); diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessor.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessor.java deleted file mode 100644 index c098fda6a0..0000000000 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessor.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.citrusframework.openapi.validation; - -import org.citrusframework.context.TestContext; -import org.citrusframework.http.message.HttpMessage; -import org.citrusframework.message.Message; -import org.citrusframework.openapi.OpenApiSpecification; -import org.citrusframework.validation.ValidationProcessor; - -/** - * {@code ValidationProcessor} that delegates validation of OpenApi responses to instances of - * {@link OpenApiResponseValidator}. - */ -public class OpenApiResponseValidationProcessor implements - ValidationProcessor { - - private final OpenApiSpecification openApiSpecification; - - private final String operationId; - - private final OpenApiResponseValidator openApiResponseValidator; - - public OpenApiResponseValidationProcessor(OpenApiSpecification openApiSpecification, - String operationId) { - this.operationId = operationId; - this.openApiSpecification = openApiSpecification; - this.openApiResponseValidator = new OpenApiResponseValidator(openApiSpecification); - } - - @Override - public void validate(Message message, TestContext context) { - - if (!(message instanceof HttpMessage httpMessage)) { - return; - } - - openApiSpecification.getOperation( - operationId, context).ifPresent(operationPathAdapter -> - openApiResponseValidator.validateResponse(operationPathAdapter, httpMessage)); - } - -} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiResponseValidator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiResponseValidator.java index faefe24a9b..8a08242617 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiResponseValidator.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiResponseValidator.java @@ -59,7 +59,23 @@ public void validateResponse(OperationPathAdapter operationPathAdapter, HttpMess } } - Response createResponseFromMessage(HttpMessage message, Integer statusCode) { + public ValidationReport validateResponseToReport(OperationPathAdapter operationPathAdapter, HttpMessage httpMessage) { + + if (enabled && openApiInteractionValidator != null) { + HttpStatusCode statusCode = httpMessage.getStatusCode(); + Response response = createResponseFromMessage(httpMessage, + statusCode != null ? statusCode.value() : null); + + return openApiInteractionValidator.validateResponse( + operationPathAdapter.apiPath(), + Method.valueOf(operationPathAdapter.operation().getMethod().toUpperCase()), + response); + + } + return ValidationReport.empty(); + } + + Response createResponseFromMessage(HttpMessage message, Integer statusCode) { var payload = message.getPayload(); SimpleResponse.Builder responseBuilder = new SimpleResponse.Builder(statusCode); diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiSchemaValidation.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiSchemaValidation.java new file mode 100644 index 0000000000..74b5a55046 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiSchemaValidation.java @@ -0,0 +1,166 @@ +package org.citrusframework.openapi.validation; + +import com.atlassian.oai.validator.report.ValidationReport; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.exceptions.ValidationException; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.message.Message; +import org.citrusframework.openapi.OpenApiMessageHeaders; +import org.citrusframework.openapi.OpenApiRepository; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.citrusframework.openapi.util.OpenApiUtils; +import org.citrusframework.openapi.validation.OpenApiMessageValidationContext.Builder; +import org.citrusframework.validation.MessageValidator; +import org.citrusframework.validation.SchemaValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class OpenApiSchemaValidation implements MessageValidator, + SchemaValidator { + + private Logger logger = LoggerFactory.getLogger(OpenApiSchemaValidation.class); + + @Override + public void validateMessage(Message receivedMessage, Message controlMessage, + TestContext context, List list) throws ValidationException { + validate(receivedMessage, context, new Builder().schemaValidation(true).build()); + } + + @Override + public void validate( + Message message, TestContext context, OpenApiMessageValidationContext validationContext) { + logger.debug("Starting OpenApi schema validation ..."); + + if (!(message instanceof HttpMessage httpMessage)) { + return; + } + + ValidationReportData validationReportData = validate(context, httpMessage, + findSchemaRepositories(context), + validationContext); + if (validationReportData != null && validationReportData.report.hasErrors()) { + if (logger.isErrorEnabled()) { + logger.error("Failed to validate Json schema for message:\n{}", + message.getPayload(String.class)); + } + throw new ValidationException(constructErrorMessage(validationReportData)); + } + + logger.debug("Json schema validation successful: All values OK"); + } + + @Override + public boolean supportsMessageType(String messageType, Message message) { + return message.getHeader(OpenApiMessageHeaders.OAS_UNIQUE_OPERATION_ID) != null; + } + + private String constructErrorMessage(ValidationReportData validationReportData) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("OpenApi "); + stringBuilder.append(validationReportData.type); + stringBuilder.append(" validation failed for operation: "); + stringBuilder.append(validationReportData.operationPathAdapter); + validationReportData.report.getMessages() + .forEach(message -> stringBuilder.append("\n\t").append(message)); + return stringBuilder.toString(); + } + + /** + * Find json schema repositories in test context. + */ + private List findSchemaRepositories(TestContext context) { + return new ArrayList<>( + context.getReferenceResolver().resolveAll(OpenApiRepository.class).values()); + } + + @Nullable + private ValidationReportData validate(TestContext context, HttpMessage message, + List schemaRepositories, + OpenApiMessageValidationContext validationContext) { + + if (!validationContext.isSchemaValidationEnabled()) { + return null; + } + if (schemaRepositories.isEmpty()) { + return null; + } else { + + // Is it request or response? + String uniqueOperationId = (String) message.getHeader( + OpenApiMessageHeaders.OAS_UNIQUE_OPERATION_ID); + + OpenApiSpecification openApiSpecification = schemaRepositories + .stream() + .flatMap(repository -> repository.getOpenApiSpecifications().stream()) + .filter(spec -> spec.getOperation(uniqueOperationId, + context).isPresent()).findFirst().orElse(null); + + if (openApiSpecification == null) { + throw new CitrusRuntimeException(""" + Unable to derive OpenAPI spec for operation '%s' for validation of message from available " + schema repositories. Known repository aliases are: %s""".formatted( + uniqueOperationId, OpenApiUtils.getKnownOpenApiAliases( + context.getReferenceResolver()))); + } + + OperationPathAdapter operationPathAdapter = openApiSpecification.getOperation( + uniqueOperationId, context).orElseThrow(() -> new CitrusRuntimeException( + "Unexpectedly could not resolve operation path adapter for operationId: " + + uniqueOperationId)); + ValidationReportData validationReportData = null; + if (isRequestMessage(message)) { + ValidationReport validationReport = new OpenApiRequestValidator( + openApiSpecification).validateRequestToReport(operationPathAdapter, message); + validationReportData = new ValidationReportData(operationPathAdapter, "request", + validationReport); + } else if (isResponseMessage(message)) { + ValidationReport validationReport = new OpenApiResponseValidator( + openApiSpecification).validateResponseToReport(operationPathAdapter, message); + validationReportData = new ValidationReportData(operationPathAdapter, "response", + validationReport); + } + return validationReportData; + } + } + + private boolean isResponseMessage(HttpMessage message) { + return OpenApiMessageHeaders.RESPONSE_TYPE.equals( + message.getHeader(OpenApiMessageHeaders.OAS_MESSAGE_TYPE)); + } + + private boolean isRequestMessage(HttpMessage message) { + return OpenApiMessageHeaders.REQUEST_TYPE.equals( + message.getHeader(OpenApiMessageHeaders.OAS_MESSAGE_TYPE)); + } + + private record ValidationReportData(OperationPathAdapter operationPathAdapter, String type, + ValidationReport report) { + + } + + @Override + public boolean canValidate(Message message, boolean schemaValidationEnabled) { + return schemaValidationEnabled && + message instanceof HttpMessage httpMessage && (isRequestMessage(httpMessage) + || isResponseMessage(httpMessage)); + } + + @Override + public void validate(Message message, TestContext context, String schemaRepository, + String schema) { + + if (!(message instanceof HttpMessage)) { + return; + } + + validate(message, context, + new Builder().schemaValidation(true).schema(schema).schemaRepository(schemaRepository) + .build()); + + } +} diff --git a/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/schemaValidator/openApi b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/schemaValidator/openApi new file mode 100644 index 0000000000..897edb75c0 --- /dev/null +++ b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/schemaValidator/openApi @@ -0,0 +1,2 @@ +name=defaultOpenApiSchemaValidator +type=org.citrusframework.openapi.validation.OpenApiSchemaValidation diff --git a/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/validator/openApi b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/validator/openApi new file mode 100644 index 0000000000..ad5d27c497 --- /dev/null +++ b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/validator/openApi @@ -0,0 +1,2 @@ +name=defaultOpenApiMessageValidator +type=org.citrusframework.openapi.validation.OpenApiSchemaValidation diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiMessageTypeTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiMessageTypeTest.java new file mode 100644 index 0000000000..39486d14f8 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiMessageTypeTest.java @@ -0,0 +1,18 @@ +package org.citrusframework.openapi; + +import static org.testng.Assert.assertEquals; + +import org.testng.annotations.Test; + +public class OpenApiMessageTypeTest { + + @Test + public void testToHeaderNameRequest() { + assertEquals(OpenApiMessageType.REQUEST.toHeaderName(), OpenApiMessageHeaders.REQUEST_TYPE); + } + + @Test + public void testToHeaderNameResponse() { + assertEquals(OpenApiMessageHeaders.REQUEST_TYPE, OpenApiMessageHeaders.RESPONSE_TYPE); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiRepositoryTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiRepositoryTest.java index fe61791bf2..15f6fa113e 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiRepositoryTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiRepositoryTest.java @@ -71,7 +71,7 @@ public void shouldResolveResourceAliasFromFile() { Optional alias = OpenApiRepository.determineResourceAlias(resourceMock); assertTrue(alias.isPresent()); - assertEquals(alias.get(), "MyApi.json"); + assertEquals(alias.get(), "MyApi"); } @Test @@ -84,7 +84,7 @@ public void shouldResolveResourceAliasFromUrl() throws MalformedURLException { Optional alias = OpenApiRepository.determineResourceAlias(resourceMock); assertTrue(alias.isPresent()); - assertEquals(alias.get(), "MyApi.json"); + assertEquals(alias.get(), "MyApi"); } @Test diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiUtilsTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiUtilsTest.java index 9dbc709fa6..533ccb5211 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiUtilsTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiUtilsTest.java @@ -16,17 +16,25 @@ package org.citrusframework.openapi; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageHeaders; import org.citrusframework.openapi.util.OpenApiUtils; +import org.citrusframework.openapi.validation.OpenApiMessageProcessor; +import org.citrusframework.spi.ReferenceResolver; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import static org.citrusframework.openapi.util.OpenApiUtils.getKnownOpenApiAliases; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; public class OpenApiUtilsTest { @@ -94,4 +102,56 @@ public void shouldReturnFormattedMethodPathWhenMethodAndPathAreEmpty() { // Then assertEquals(methodPath, "//"); } + + @Test + public void testGetKnownOpenApiAliases() { + + ReferenceResolver resolver = mock(); + OpenApiRepository repository1 = mock(); + OpenApiRepository repository2 = mock(); + OpenApiSpecification spec1 = mock(); + OpenApiSpecification spec2 = mock(); + + when(resolver.resolveAll(OpenApiRepository.class)).thenReturn( + Map.of( + "repo1", repository1, + "repo2", repository2 + ) + ); + + when(repository1.getOpenApiSpecifications()).thenReturn(List.of(spec1)); + when(repository2.getOpenApiSpecifications()).thenReturn(List.of(spec2)); + + when(spec1.getAliases()).thenReturn(Set.of("alias1", "alias2")); + when(spec2.getAliases()).thenReturn(Set.of("alias3")); + + String result = getKnownOpenApiAliases(resolver); + + assertTrue(result.contains("alias1")); + assertTrue(result.contains("alias2")); + assertTrue(result.contains("alias3")); + } + + @Test + public void testGetKnownOpenApiAliasesNoAliases() { + ReferenceResolver resolver = mock(); + OpenApiRepository repository1 = mock(); + OpenApiRepository repository2 = mock(); + + when(resolver.resolveAll(OpenApiRepository.class)).thenReturn( + Map.of( + "repo1", repository1, + "repo2", repository2 + ) + ); + + when(repository1.getOpenApiSpecifications()).thenReturn(List.of()); + when(repository2.getOpenApiSpecifications()).thenReturn(List.of()); + + // Call the method under test + String result = getKnownOpenApiAliases(resolver); + + // Verify the result + assertEquals(result, ""); + } } diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiClientTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiClientTest.java index 607d1b3c54..33aad7e924 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiClientTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiClientTest.java @@ -178,7 +178,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { validator.validateMessage(request, controlMessage, context, new DefaultValidationContext()); ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); @@ -197,7 +197,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "application/json"); Assert.assertNull(receiveMessageAction.getEndpoint()); Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpClient"); - Assert.assertEquals(receiveMessageAction.getMessageProcessors().size(), 0); + Assert.assertEquals(receiveMessageAction.getMessageProcessors().size(), 1); Assert.assertEquals(receiveMessageAction.getControlMessageProcessors().size(), 0); sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); @@ -218,7 +218,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { Assert.assertEquals(sendMessageAction.getEndpoint(), httpClient); receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientIT.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientIT.java index 94786d08c2..c210208820 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientIT.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientIT.java @@ -23,6 +23,7 @@ import org.citrusframework.http.client.HttpClientBuilder; import org.citrusframework.http.server.HttpServer; import org.citrusframework.http.server.HttpServerBuilder; +import org.citrusframework.openapi.OpenApiRepository; import org.citrusframework.openapi.OpenApiSpecification; import org.citrusframework.openapi.actions.OpenApiActionBuilder; import org.citrusframework.openapi.actions.OpenApiClientResponseActionBuilder; @@ -35,6 +36,8 @@ import org.testng.annotations.Ignore; import org.testng.annotations.Test; +import java.util.List; + import static org.citrusframework.http.actions.HttpActionBuilder.http; import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi; import static org.testng.Assert.assertThrows; @@ -61,8 +64,12 @@ public class OpenApiClientIT extends TestNGCitrusSpringSupport { .requestUrl("http://localhost:%d".formatted(port)) .build(); + @BindToRegistry + private final OpenApiRepository openApiRepository = new OpenApiRepository() + .locations(List.of("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); + private final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( - Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); + Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); private final OpenApiSpecification pingSpec = OpenApiSpecification.from( Resources.create("classpath:org/citrusframework/openapi/ping/ping-api.yaml")); @@ -140,8 +147,7 @@ public void shouldFailOnMissingNameInRequest() { HttpMessageBuilderSupport addPetBuilder = openapi(petstoreSpec) .client(httpClient) .send("addPet") - .message().body(Resources.create(INVALID_PET_PATH)) - .fork(true); + .message().body(Resources.create(INVALID_PET_PATH)); assertThrows(TestCaseFailedException.class, () ->when(addPetBuilder)); } @@ -153,9 +159,7 @@ public void shouldFailOnWrongQueryIdType() { HttpMessageBuilderSupport addPetBuilder = openapi(petstoreSpec) .client(httpClient) .send("addPet") - .message().body(Resources.create(VALID_PET_PATH)) - .fork(true); - + .message().body(Resources.create(VALID_PET_PATH)); assertThrows(TestCaseFailedException.class, () ->when(addPetBuilder)); } @@ -167,8 +171,7 @@ public void shouldSucceedOnWrongQueryIdTypeWithOasDisabled() { .client(httpClient) .send("addPet") .disableOasValidation(true) - .message().body(Resources.create(VALID_PET_PATH)) - .fork(true); + .message().body(Resources.create(VALID_PET_PATH)); try { when(addPetBuilder); diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiServerIT.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiServerIT.java index 82d48fa427..808c84cdc2 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiServerIT.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiServerIT.java @@ -23,7 +23,7 @@ import org.citrusframework.http.client.HttpClientBuilder; import org.citrusframework.http.server.HttpServer; import org.citrusframework.http.server.HttpServerBuilder; -import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.OpenApiRepository; import org.citrusframework.openapi.actions.OpenApiActionBuilder; import org.citrusframework.openapi.actions.OpenApiServerRequestActionBuilder; import org.citrusframework.openapi.actions.OpenApiServerResponseActionBuilder; @@ -34,6 +34,8 @@ import org.springframework.http.HttpStatus; import org.testng.annotations.Test; +import java.util.List; + import static org.citrusframework.http.actions.HttpActionBuilder.http; import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi; import static org.testng.Assert.assertThrows; @@ -60,11 +62,10 @@ public class OpenApiServerIT extends TestNGCitrusSpringSupport { .requestUrl("http://localhost:%d/petstore/v3".formatted(port)) .build(); - /** - * Directly loaded open api. - */ - private final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( - Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); + @BindToRegistry + private final OpenApiRepository openApiRepository = new OpenApiRepository() + .locations(List.of("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); + @CitrusTest public void shouldExecuteGetPetById() { @@ -78,11 +79,11 @@ public void shouldExecuteGetPetById() { .accept("application/json") .fork(true)); - then(openapi(petstoreSpec) + then(openapi("petstore-v3") .server(httpServer) .receive("getPetById")); - then(openapi(petstoreSpec) + then(openapi("petstore-v3") .server(httpServer) .send("getPetById", HttpStatus.OK)); @@ -118,11 +119,11 @@ public void executeGetPetByIdShouldFailOnInvalidResponse() { .accept("application/json") .fork(true)); - then(openapi(petstoreSpec) + then(openapi("petstore-v3") .server(httpServer) .receive("getPetById")); - HttpMessageBuilderSupport getPetByIdResponseBuilder = openapi(petstoreSpec) + HttpMessageBuilderSupport getPetByIdResponseBuilder = openapi("petstore-v3") .server(httpServer) .send("getPetById", HttpStatus.OK) .message().body(""" @@ -153,11 +154,11 @@ public void executeGetPetByIdShouldSucceedOnInvalidResponseWithValidationDisable .accept("application/json") .fork(true)); - then(openapi(petstoreSpec) + then(openapi("petstore-v3") .server(httpServer) .receive("getPetById")); - HttpMessageBuilderSupport getPetByIdResponseBuilder = openapi(petstoreSpec) + HttpMessageBuilderSupport getPetByIdResponseBuilder = openapi("petstore-v3") .server(httpServer) .send("getPetById", HttpStatus.OK) .disableOasValidation(true) @@ -198,12 +199,12 @@ public void executeGetPetByIdShouldSucceedOnInvalidResponseWithValidationDisable @CitrusTest public void shouldExecuteAddPet() { - shouldExecuteAddPet(openapi(petstoreSpec), VALID_PET_PATH, true); + shouldExecuteAddPet(openapi("petstore-v3"), VALID_PET_PATH, true); } @CitrusTest public void shouldFailOnMissingNameInRequest() { - shouldExecuteAddPet(openapi(petstoreSpec), INVALID_PET_PATH, false); + shouldExecuteAddPet(openapi("petstore-v3"), INVALID_PET_PATH, false); } @CitrusTest @@ -218,11 +219,11 @@ public void shouldFailOnMissingNameInResponse() { .accept("application/json") .fork(true)); - then(openapi(petstoreSpec) + then(openapi("petstore-v3") .server(httpServer) .receive("getPetById")); - OpenApiServerResponseActionBuilder sendMessageActionBuilder = openapi(petstoreSpec) + OpenApiServerResponseActionBuilder sendMessageActionBuilder = openapi("petstore-v3") .server(httpServer) .send("getPetById", HttpStatus.OK); sendMessageActionBuilder.message().body(Resources.create(INVALID_PET_PATH)); @@ -244,7 +245,7 @@ public void shouldFailOnWrongQueryIdTypeWithOasDisabled() { .contentType("application/json") .fork(true)); - OpenApiServerRequestActionBuilder addPetBuilder = openapi(petstoreSpec) + OpenApiServerRequestActionBuilder addPetBuilder = openapi("petstore-v3") .server(httpServer) .receive("addPet"); @@ -264,7 +265,7 @@ public void shouldSucceedOnWrongQueryIdTypeWithOasDisabled() { .contentType("application/json") .fork(true)); - OpenApiServerRequestActionBuilder addPetBuilder = openapi(petstoreSpec) + OpenApiServerRequestActionBuilder addPetBuilder = openapi("petstore-v3") .server(httpServer) .receive("addPet") .disableOasValidation(false); @@ -293,18 +294,21 @@ private void shouldExecuteAddPet(OpenApiActionBuilder openapi, String requestFil .receive("addPet"); if (valid) { then(receiveActionBuilder); - } else { - assertThrows(() -> then(receiveActionBuilder)); - } - then(openapi + then(openapi .server(httpServer) .send("addPet", HttpStatus.CREATED)); - then(http() + then(http() .client(httpClient) .receive() .response(HttpStatus.CREATED)); + + } else { + assertThrows(() -> then(receiveActionBuilder)); + } + + } } diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/OperationPathAdapterTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/OperationPathAdapterTest.java index d24101fb35..68c5da1786 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/OperationPathAdapterTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/OperationPathAdapterTest.java @@ -31,7 +31,8 @@ public void shouldReturnFormattedStringWhenToStringIsCalled() { Oas30Operation oas30Operation = new Oas30Operation("get"); oas30Operation.operationId = "operationId"; - OperationPathAdapter adapter = new OperationPathAdapter("/api/path", "/context/path", "/full/path", oas30Operation); + OperationPathAdapter adapter = new OperationPathAdapter("/api/path", "/context/path", "/full/path", oas30Operation, + oas30Operation.operationId); // When String expectedString = format("%s (%s)", OpenApiUtils.getMethodPath("GET", "/api/path"), "operationId"); diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiMessageProcessorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiMessageProcessorTest.java new file mode 100644 index 0000000000..34f82542a2 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiMessageProcessorTest.java @@ -0,0 +1,59 @@ +package org.citrusframework.openapi.validation; + +import java.util.Optional; +import org.citrusframework.context.TestContext; +import org.citrusframework.message.Message; +import org.citrusframework.openapi.OpenApiMessageHeaders; +import org.citrusframework.openapi.OpenApiMessageType; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.mockito.Mockito; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import static org.mockito.Mockito.*; +import static org.testng.Assert.*; + +public class OpenApiMessageProcessorTest { + + private OpenApiSpecification openApiSpecification; + private String operationId; + private OpenApiMessageType type; + private OpenApiMessageProcessor processor; + private Message message; + private TestContext context; + + @BeforeMethod + public void setUp() { + openApiSpecification = mock(OpenApiSpecification.class); + operationId = "testOperationId"; + type = mock(OpenApiMessageType.class); + processor = new OpenApiMessageProcessor(openApiSpecification, operationId, type); + + message = mock(Message.class); + context = mock(TestContext.class); + } + + @Test + public void testProcess() { + OperationPathAdapter operationPathAdapter = mock(OperationPathAdapter.class); + when(openApiSpecification.getOperation(operationId, context)) + .thenReturn(Optional.of(operationPathAdapter)); + when(operationPathAdapter.uniqueOperationId()).thenReturn("uniqueOperationId"); + when(type.toHeaderName()).thenReturn("headerName"); + + processor.process(message, context); + + verify(message).setHeader(OpenApiMessageHeaders.OAS_UNIQUE_OPERATION_ID, "uniqueOperationId"); + verify(message).setHeader(OpenApiMessageHeaders.OAS_MESSAGE_TYPE, "headerName"); + } + + @Test + public void testProcessOperationNotPresent() { + when(openApiSpecification.getOperation(operationId, context)) + .thenReturn(Optional.empty()); + + processor.process(message, context); + + verify(message, never()).setHeader(anyString(), anyString()); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessorTest.java deleted file mode 100644 index 2058af2558..0000000000 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidationProcessorTest.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.citrusframework.openapi.validation; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import static org.testng.Assert.assertNotNull; - -import java.util.Optional; -import org.citrusframework.context.TestContext; -import org.citrusframework.http.message.HttpMessage; -import org.citrusframework.message.Message; -import org.citrusframework.openapi.OpenApiSpecification; -import org.citrusframework.openapi.model.OperationPathAdapter; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.test.util.ReflectionTestUtils; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -public class OpenApiResponseValidationProcessorTest { - - @Mock - private OpenApiSpecification openApiSpecificationMock; - - @Mock - private OperationPathAdapter operationPathAdapterMock; - - private OpenApiResponseValidationProcessor processor; - - private AutoCloseable mockCloseable; - - @BeforeMethod - public void beforeMethod() { - mockCloseable = MockitoAnnotations.openMocks(this); - processor = new OpenApiResponseValidationProcessor(openApiSpecificationMock, "operationId"); - } - - @AfterMethod - public void afterMethod() throws Exception { - mockCloseable.close(); - } - - @Test - public void shouldNotValidateNonHttpMessage() { - Message messageMock = mock(); - - processor.validate(messageMock, mock()); - - verify(openApiSpecificationMock,times(2)).getSwaggerOpenApiValidationContext(); - verifyNoMoreInteractions(openApiSpecificationMock); - } - - @Test - public void shouldCallValidateResponse() { - HttpMessage httpMessageMock = mock(); - TestContext contextMock = mock(); - - OpenApiResponseValidator openApiResponseValidatorSpy = replaceValidatorWithSpy(httpMessageMock); - - when(openApiSpecificationMock.getOperation(anyString(), any(TestContext.class))) - .thenReturn(Optional.of(operationPathAdapterMock)); - - processor.validate(httpMessageMock, contextMock); - - verify(openApiResponseValidatorSpy).validateResponse(operationPathAdapterMock, httpMessageMock); - } - - @Test - public void shouldNotValidateWhenNoOperation() { - HttpMessage httpMessageMock = mock(); - TestContext contextMock = mock(); - - OpenApiResponseValidator openApiResponseValidatorSpy = replaceValidatorWithSpy(httpMessageMock); - - when(openApiSpecificationMock.getOperation(anyString(), any(TestContext.class))) - .thenReturn(Optional.empty()); - - processor.validate(httpMessageMock, contextMock); - - verify(openApiSpecificationMock).getOperation(anyString(), - any(TestContext.class)); - verify(openApiResponseValidatorSpy, times(0)).validateResponse(operationPathAdapterMock, httpMessageMock); - } - - private OpenApiResponseValidator replaceValidatorWithSpy(HttpMessage httpMessage) { - OpenApiResponseValidator openApiResponseValidator = (OpenApiResponseValidator) ReflectionTestUtils.getField( - processor, - "openApiResponseValidator"); - - assertNotNull(openApiResponseValidator); - OpenApiResponseValidator openApiResponseValidatorSpy = spy(openApiResponseValidator); - ReflectionTestUtils.setField(processor, "openApiResponseValidator", openApiResponseValidatorSpy); - - doAnswer((invocation) -> null - // do nothing - ).when(openApiResponseValidatorSpy).validateResponse(operationPathAdapterMock, httpMessage); - - return openApiResponseValidatorSpy; - } -} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiClientTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiClientTest.java index ca2d7e8732..2fc0bb33bf 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiClientTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiClientTest.java @@ -184,7 +184,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { validator.validateMessage(request, controlMessage, context, new DefaultValidationContext()); ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); @@ -203,7 +203,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "application/json"); Assert.assertNull(receiveMessageAction.getEndpoint()); Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpClient"); - Assert.assertEquals(receiveMessageAction.getMessageProcessors().size(), 0); + Assert.assertEquals(receiveMessageAction.getMessageProcessors().size(), 1); Assert.assertEquals(receiveMessageAction.getControlMessageProcessors().size(), 0); sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); @@ -224,7 +224,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpClient"); receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof HeaderValidationContext); diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiClientTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiClientTest.java index b5b152d559..e81a89e63b 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiClientTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiClientTest.java @@ -179,7 +179,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { validator.validateMessage(request, controlMessage, context, new DefaultValidationContext()); ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); @@ -198,7 +198,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "application/json"); Assert.assertNull(receiveMessageAction.getEndpoint()); Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpClient"); - Assert.assertEquals(receiveMessageAction.getMessageProcessors().size(), 0); + Assert.assertEquals(receiveMessageAction.getMessageProcessors().size(), 1); Assert.assertEquals(receiveMessageAction.getControlMessageProcessors().size(), 0); sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); @@ -219,7 +219,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpClient"); receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof HeaderValidationContext); diff --git a/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidatorRegistry.java b/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidatorRegistry.java index 7ba3d3abd4..bbeecbcab6 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidatorRegistry.java +++ b/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidatorRegistry.java @@ -112,7 +112,11 @@ public List> findMessageValidators if (isEmptyOrDefault(matchingValidators)) { if (mustFindValidator) { - logger.warn(String.format("Unable to find proper message validator. Message type is '%s' and message payload is '%s'", messageType, message.getPayload(String.class))); + if (logger.isWarnEnabled()) { + logger.warn(String.format( + "Unable to find proper message validator. Message type is '%s' and message payload is '%s'", + messageType, message.getPayload(String.class))); + } throw new CitrusRuntimeException("Failed to find proper message validator for message"); } @@ -308,4 +312,11 @@ public Optional> findSchemaVa public void setSchemaValidators(Map> schemaValidators) { this.schemaValidators = schemaValidators; } + + /** + * Return all schema validators. + */ + public Map> getSchemaValidators() { + return schemaValidators; + } } diff --git a/core/citrus-api/src/main/java/org/citrusframework/validation/SchemaValidator.java b/core/citrus-api/src/main/java/org/citrusframework/validation/SchemaValidator.java index ecc2b006df..3d6a8d11d0 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/validation/SchemaValidator.java +++ b/core/citrus-api/src/main/java/org/citrusframework/validation/SchemaValidator.java @@ -88,4 +88,16 @@ static Optional> lookup(Strin * @return true if the message/message type can be validated by this validator */ boolean supportsMessageType(String messageType, Message message); + + /** + * @param message the message which is subject of validation + * @param schemaValidationEnabled flag to indicate whether schema validation is explicitly enabled + * @return true, if the validator can validate the given message + */ + boolean canValidate(Message message, boolean schemaValidationEnabled); + + /** + * Validate the message against the given schemaRepository and schema. + */ + void validate(Message message, TestContext context, String schemaRepository, String schema); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/SendMessageAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/SendMessageAction.java index 20fa11ccff..d91ae3d002 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/SendMessageAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/SendMessageAction.java @@ -16,7 +16,6 @@ package org.citrusframework.actions; -import org.citrusframework.CitrusSettings; import org.citrusframework.Completable; import org.citrusframework.context.TestContext; import org.citrusframework.endpoint.Endpoint; @@ -25,16 +24,9 @@ import org.citrusframework.message.MessageBuilder; import org.citrusframework.message.MessageDirection; import org.citrusframework.message.MessageProcessor; -import org.citrusframework.message.MessageType; import org.citrusframework.message.builder.MessageBuilderSupport; import org.citrusframework.message.builder.SendMessageBuilderSupport; -import org.citrusframework.util.IsJsonPredicate; -import org.citrusframework.util.IsXmlPredicate; import org.citrusframework.util.StringUtils; -import org.citrusframework.validation.SchemaValidator; -import org.citrusframework.validation.context.SchemaValidationContext; -import org.citrusframework.validation.json.JsonMessageValidationContext; -import org.citrusframework.validation.xml.XmlMessageValidationContext; import org.citrusframework.variable.VariableExtractor; import org.citrusframework.variable.dictionary.DataDictionary; import org.slf4j.Logger; @@ -128,7 +120,9 @@ public void doExecute(final TestContext context) { finished.whenComplete((ctx, ex) -> { if (ex != null) { - logger.warn("Failure in forked send action: " + ex.getMessage()); + if (logger.isWarnEnabled()) { + logger.warn("Failure in forked send action: %s".formatted(ex.getMessage())); + } } else { for (Exception ctxEx : ctx.getExceptions()) { logger.warn(ctxEx.getMessage()); @@ -146,7 +140,9 @@ public void doExecute(final TestContext context) { if (StringUtils.hasText(message.getName())) { context.getMessageStore().storeMessage(message.getName(), message); } else { - context.getMessageStore().storeMessage(context.getMessageStore().constructMessageName(this, messageEndpoint), message); + context.getMessageStore() + .storeMessage(context.getMessageStore().constructMessageName(this, messageEndpoint), + message); } if (forkMode) { @@ -179,59 +175,14 @@ public void doExecute(final TestContext context) { } /** - * Validate the message against registered schemas. - * @param message + * Validate the message against registered schema validators. */ protected void validateMessage(Message message, TestContext context) { - List> schemaValidators = null; - SchemaValidationContext validationContext = null; - String payload = message.getPayload(String.class); - - if ((isSchemaValidation() || isJsonSchemaValidationEnabled()) && IsJsonPredicate.getInstance().test(payload)) { - schemaValidators = context.getMessageValidatorRegistry() - .findSchemaValidators(MessageType.JSON.name(), message); - validationContext = JsonMessageValidationContext.Builder.json() - .schemaValidation(this.schemaValidation) - .schema(this.schema) - .schemaRepository(this.schemaRepository).build(); - } else if ((isSchemaValidation() || isXmlSchemaValidationEnabled()) && IsXmlPredicate.getInstance().test(payload)) { - schemaValidators = context.getMessageValidatorRegistry() - .findSchemaValidators(MessageType.XML.name(), message); - validationContext = XmlMessageValidationContext.Builder.xml() - .schemaValidation(this.schemaValidation) - .schema(this.schema) - .schemaRepository(this.schemaRepository).build(); - } - - if (schemaValidators != null) { - for (SchemaValidator validator : schemaValidators) { - validator.validate(message, context, validationContext); - } - } - + context.getMessageValidatorRegistry().getSchemaValidators().values().stream() + .filter(validator -> validator.canValidate(message, isSchemaValidation())).forEach(validator -> + validator.validate(message, context, this.schemaRepository, this.schema)); } - /** - * Get setting to determine if json schema validation is enabled by default. - * @return - */ - private static boolean isJsonSchemaValidationEnabled() { - return Boolean.getBoolean(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_PROPERTY) - || Boolean.getBoolean(CitrusSettings.OUTBOUND_JSON_SCHEMA_VALIDATION_ENABLED_PROPERTY) - || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_ENV)) - || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_JSON_SCHEMA_VALIDATION_ENABLED_ENV)); - } - - /** - * Get setting to determine if xml schema validation is enabled by default. - * @return - */ - private static boolean isXmlSchemaValidationEnabled() { - return Boolean.getBoolean(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_PROPERTY) - || Boolean.getBoolean(CitrusSettings.OUTBOUND_XML_SCHEMA_VALIDATION_ENABLED_PROPERTY) - || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_ENV)) - || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_XML_SCHEMA_VALIDATION_ENABLED_ENV)); - } /** * {@inheritDoc} @@ -249,12 +200,13 @@ public boolean isDisabled(TestContext context) { @Override public boolean isDone(TestContext context) { return Optional.ofNullable(finished) - .map(future -> future.isDone() || isDisabled(context)) - .orElseGet(() -> isDisabled(context)); + .map(future -> future.isDone() || isDisabled(context)) + .orElseGet(() -> isDisabled(context)); } /** * Create message to be sent. + * * @param context * @param messageType * @return @@ -264,7 +216,7 @@ protected Message createMessage(TestContext context, String messageType) { if (message.getPayload() != null) { context.getMessageProcessors(MessageDirection.OUTBOUND) - .forEach(processor -> processor.process(message, context)); + .forEach(processor -> processor.process(message, context)); if (dataDictionary != null) { dataDictionary.process(message, context); @@ -278,6 +230,7 @@ protected Message createMessage(TestContext context, String messageType) { /** * Creates or gets the message endpoint instance. + * * @return the message endpoint */ public Endpoint getOrCreateEndpoint(TestContext context) { @@ -292,6 +245,7 @@ public Endpoint getOrCreateEndpoint(TestContext context) { /** * Gets the message endpoint. + * * @return */ public Endpoint getEndpoint() { @@ -300,6 +254,7 @@ public Endpoint getEndpoint() { /** * Get + * * @return true if schema validation is active for this message */ public boolean isSchemaValidation() { @@ -308,6 +263,7 @@ public boolean isSchemaValidation() { /** * Get the name of the schema repository used for validation + * * @return the schema repository name */ public String getSchemaRepository() { @@ -316,6 +272,7 @@ public String getSchemaRepository() { /** * Get the name of the schema used for validation + * * @return the schema */ public String getSchema() { @@ -324,6 +281,7 @@ public String getSchema() { /** * Get the variable extractors. + * * @return the variableExtractors */ public List getVariableExtractors() { @@ -332,6 +290,7 @@ public List getVariableExtractors() { /** * Obtains the message processors. + * * @return */ public List getMessageProcessors() { @@ -340,6 +299,7 @@ public List getMessageProcessors() { /** * Gets the messageBuilder. + * * @return the messageBuilder */ public MessageBuilder getMessageBuilder() { @@ -348,6 +308,7 @@ public MessageBuilder getMessageBuilder() { /** * Gets the forkMode. + * * @return the forkMode the forkMode to get. */ public boolean isForkMode() { @@ -356,6 +317,7 @@ public boolean isForkMode() { /** * Gets the message type for this receive action. + * * @return the messageType */ public String getMessageType() { @@ -364,6 +326,7 @@ public String getMessageType() { /** * Gets the data dictionary. + * * @return */ public DataDictionary getDataDictionary() { @@ -372,6 +335,7 @@ public DataDictionary getDataDictionary() { /** * Gets the endpoint uri. + * * @return */ public String getEndpointUri() { @@ -381,10 +345,12 @@ public String getEndpointUri() { /** * Action builder. */ - public static class Builder extends SendMessageActionBuilder { + public static final class Builder extends + SendMessageActionBuilder { /** * Fluent API action building entry method used in Java DSL. + * * @return */ public static Builder send() { @@ -393,6 +359,7 @@ public static Builder send() { /** * Fluent API action building entry method used in Java DSL. + * * @param messageEndpoint * @return */ @@ -404,6 +371,7 @@ public static Builder send(Endpoint messageEndpoint) { /** * Fluent API action building entry method used in Java DSL. + * * @param messageEndpointUri * @return */ @@ -428,7 +396,8 @@ public SendMessageAction doBuild() { } - public static class SendMessageActionBuilderSupport extends SendMessageBuilderSupport { + public static class SendMessageActionBuilderSupport extends + SendMessageBuilderSupport { public SendMessageActionBuilderSupport(SendMessageAction.Builder delegate) { super(delegate); @@ -438,14 +407,15 @@ public SendMessageActionBuilderSupport(SendMessageAction.Builder delegate) { /** * Base send message action builder also used by subclasses of base send message action. */ - public static abstract class SendMessageActionBuilder, B extends SendMessageActionBuilder> - extends MessageBuilderSupport.MessageActionBuilder { + public abstract static class SendMessageActionBuilder, B extends SendMessageActionBuilder> + extends MessageBuilderSupport.MessageActionBuilder { protected boolean forkMode = false; protected CompletableFuture finished; /** * Sets the fork mode for this send action builder. + * * @param forkMode * @return */ @@ -463,7 +433,8 @@ public final T build() { if (referenceResolver != null) { if (messageBuilderSupport.getDataDictionaryName() != null) { this.messageBuilderSupport.dictionary( - referenceResolver.resolve(messageBuilderSupport.getDataDictionaryName(), DataDictionary.class)); + referenceResolver.resolve(messageBuilderSupport.getDataDictionaryName(), + DataDictionary.class)); } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientRequestActionBuilder.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientRequestActionBuilder.java index cced421b22..479c6da50d 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientRequestActionBuilder.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientRequestActionBuilder.java @@ -17,7 +17,6 @@ package org.citrusframework.http.actions; import jakarta.servlet.http.Cookie; - import org.citrusframework.actions.SendMessageAction; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; @@ -57,11 +56,15 @@ protected HttpClientRequestActionBuilder(MessageBuilder messageBuilder, HttpMess @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { - messageBuilderSupport = new HttpMessageBuilderSupport(httpMessage, this); + messageBuilderSupport = createHttpMessageBuilderSupport(); } return super.getMessageBuilderSupport(); } + protected HttpMessageBuilderSupport createHttpMessageBuilderSupport() { + return new HttpMessageBuilderSupport(httpMessage, this); + } + /** * Sets the request path. * @param path diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientResponseActionBuilder.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientResponseActionBuilder.java index 292caf3016..3929df439c 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientResponseActionBuilder.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientResponseActionBuilder.java @@ -17,6 +17,7 @@ package org.citrusframework.http.actions; import jakarta.servlet.http.Cookie; +import java.util.Optional; import org.citrusframework.actions.ReceiveMessageAction; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; @@ -26,8 +27,6 @@ import org.citrusframework.message.builder.ReceiveMessageBuilderSupport; import org.springframework.http.HttpStatusCode; -import java.util.Optional; - /** * @since 2.4 */ @@ -59,11 +58,15 @@ public HttpClientResponseActionBuilder(MessageBuilder messageBuilder, HttpMessag @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { - messageBuilderSupport = new HttpMessageBuilderSupport(httpMessage, this); + messageBuilderSupport = createHttpMessageBuilderSupport(); } return super.getMessageBuilderSupport(); } + protected HttpMessageBuilderSupport createHttpMessageBuilderSupport() { + return new HttpMessageBuilderSupport(httpMessage, this); + } + public static class HttpMessageBuilderSupport extends ReceiveMessageBuilderSupport { private final HttpMessage httpMessage; diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerRequestActionBuilder.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerRequestActionBuilder.java index 39f22cc090..148af48b6a 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerRequestActionBuilder.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerRequestActionBuilder.java @@ -16,9 +16,8 @@ package org.citrusframework.http.actions; -import java.util.Optional; - import jakarta.servlet.http.Cookie; +import java.util.Optional; import org.citrusframework.actions.ReceiveMessageAction; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; @@ -61,11 +60,15 @@ public HttpServerRequestActionBuilder(MessageBuilder messageBuilder, HttpMessage @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { - messageBuilderSupport = new HttpMessageBuilderSupport(httpMessage, this); + messageBuilderSupport = createMessageBuilderSupport(); } return super.getMessageBuilderSupport(); } + protected HttpMessageBuilderSupport createMessageBuilderSupport() { + return new HttpMessageBuilderSupport(httpMessage, this); + } + /** * Sets the request path. * @param path diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerResponseActionBuilder.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerResponseActionBuilder.java index e0610ce5d0..8a0ed55d59 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerResponseActionBuilder.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerResponseActionBuilder.java @@ -57,11 +57,15 @@ public HttpServerResponseActionBuilder(MessageBuilder messageBuilder, HttpMessag @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { - messageBuilderSupport = new HttpMessageBuilderSupport(httpMessage, this); + messageBuilderSupport = createMessageBuilderSupport(); } return super.getMessageBuilderSupport(); } + protected HttpMessageBuilderSupport createMessageBuilderSupport() { + return new HttpMessageBuilderSupport(httpMessage, this); + } + public static class HttpMessageBuilderSupport extends SendMessageBuilderSupport { private final HttpMessage httpMessage; diff --git a/pom.xml b/pom.xml index cdf5301455..d44ce20516 100644 --- a/pom.xml +++ b/pom.xml @@ -195,7 +195,7 @@ 1.10.15 4.8.1 1.1.27 - com.atlassian.oai + 2.41.0 1.8.0 3.26.3 4.2.2 diff --git a/test-api-generator/pom.xml b/test-api-generator/pom.xml index b33d0f717e..c3776c4931 100644 --- a/test-api-generator/pom.xml +++ b/test-api-generator/pom.xml @@ -38,6 +38,7 @@ ${junit.jupiter.version} test + diff --git a/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/schema/JsonSchemaValidation.java b/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/schema/JsonSchemaValidation.java index 8cdfc89673..f2e8eaeed5 100644 --- a/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/schema/JsonSchemaValidation.java +++ b/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/schema/JsonSchemaValidation.java @@ -16,11 +16,10 @@ package org.citrusframework.validation.json.schema; -import static java.util.Collections.emptySet; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.ValidationMessage; +import org.citrusframework.CitrusSettings; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.ValidationException; @@ -31,6 +30,7 @@ import org.citrusframework.util.IsJsonPredicate; import org.citrusframework.validation.SchemaValidator; import org.citrusframework.validation.json.JsonMessageValidationContext; +import org.citrusframework.validation.json.JsonMessageValidationContext.Builder; import org.citrusframework.validation.json.report.GraciousProcessingReport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +40,8 @@ import java.util.List; import java.util.Set; +import static java.util.Collections.emptySet; + /** * This class is responsible for the validation of json messages against json schemas / json schema repositories. * @@ -71,7 +73,10 @@ public void validate(Message message, TestContext context, JsonMessageValidation context.getReferenceResolver()); if (!report.isSuccess()) { - logger.error("Failed to validate Json schema for message:\n{}", message.getPayload(String.class)); + if (logger.isErrorEnabled()) { + logger.error("Failed to validate Json schema for message:\n{}", + message.getPayload(String.class)); + } throw new ValidationException(constructErrorMessage(report)); } @@ -167,4 +172,31 @@ public boolean supportsMessageType(String messageType, Message message) { return "JSON".equals(messageType) || (message != null && IsJsonPredicate.getInstance().test(message.getPayload(String.class))); } + + @Override + public boolean canValidate(Message message, boolean schemaValidationEnabled) { + return (isJsonSchemaValidationEnabled() || schemaValidationEnabled) + && IsJsonPredicate.getInstance().test(message.getPayload(String.class)); + } + + /** + * Get setting to determine if json schema validation is enabled by default. + * @return + */ + private static boolean isJsonSchemaValidationEnabled() { + return Boolean.getBoolean(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_PROPERTY) + || Boolean.getBoolean(CitrusSettings.OUTBOUND_JSON_SCHEMA_VALIDATION_ENABLED_PROPERTY) + || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_ENV)) + || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_JSON_SCHEMA_VALIDATION_ENABLED_ENV)); + } + + @Override + public void validate(Message message, TestContext context, String schemaRepository, String schema) { + + JsonMessageValidationContext validationContext = Builder.json() + .schemaValidation(true) + .schema(schema) + .schemaRepository(schemaRepository).build(); + validate(message, context, validationContext); + } } diff --git a/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/SendMessageActionTest.java b/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/SendMessageActionTest.java index c4850d40c1..4183e3e2d2 100644 --- a/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/SendMessageActionTest.java +++ b/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/SendMessageActionTest.java @@ -16,10 +16,6 @@ package org.citrusframework.validation.json; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - import org.citrusframework.actions.SendMessageAction; import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; @@ -30,6 +26,7 @@ import org.citrusframework.message.MessageType; import org.citrusframework.message.builder.DefaultPayloadBuilder; import org.citrusframework.messaging.Producer; +import org.citrusframework.spi.ReferenceResolver; import org.citrusframework.testng.AbstractTestNGUnitTest; import org.citrusframework.validation.DefaultMessageHeaderValidator; import org.citrusframework.validation.MessageValidatorRegistry; @@ -40,11 +37,18 @@ import org.testng.Assert; import org.testng.annotations.Test; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; public class SendMessageActionTest extends AbstractTestNGUnitTest { @@ -64,16 +68,18 @@ protected TestContextFactory createTestContextFactory() { @SuppressWarnings("rawtypes") public void testSendMessageOverwriteMessageElementsJsonPath() { DefaultMessageBuilder messageBuilder = new DefaultMessageBuilder(); - messageBuilder.setPayloadBuilder(new DefaultPayloadBuilder("{ \"TestRequest\": { \"Message\": \"?\" }}")); + messageBuilder.setPayloadBuilder( + new DefaultPayloadBuilder("{ \"TestRequest\": { \"Message\": \"?\" }}")); Map overwriteElements = new HashMap<>(); overwriteElements.put("$.TestRequest.Message", "Hello World!"); JsonPathMessageProcessor processor = new JsonPathMessageProcessor.Builder() - .expressions(overwriteElements) - .build(); + .expressions(overwriteElements) + .build(); - final Message controlMessage = new DefaultMessage("{\"TestRequest\":{\"Message\":\"Hello World!\"}}"); + final Message controlMessage = new DefaultMessage( + "{\"TestRequest\":{\"Message\":\"Hello World!\"}}"); reset(endpoint, producer, endpointConfiguration); when(endpoint.createProducer()).thenReturn(producer); @@ -87,11 +93,11 @@ public void testSendMessageOverwriteMessageElementsJsonPath() { when(endpoint.getActor()).thenReturn(null); SendMessageAction sendAction = new SendMessageAction.Builder() - .endpoint(endpoint) - .message(messageBuilder) - .type(MessageType.JSON) - .process(processor) - .build(); + .endpoint(endpoint) + .message(messageBuilder) + .type(MessageType.JSON) + .process(processor) + .build(); sendAction.execute(context); } @@ -99,24 +105,32 @@ public void testSendMessageOverwriteMessageElementsJsonPath() { @Test public void testSendJsonMessageWithValidation() { - AtomicBoolean validated = new AtomicBoolean(false); + AtomicBoolean validated = new AtomicBoolean(false); SchemaValidator schemaValidator = mock(SchemaValidator.class); when(schemaValidator.supportsMessageType(eq("JSON"), any())).thenReturn(true); - doAnswer(invocation-> { - JsonMessageValidationContext argument = invocation.getArgument(2, JsonMessageValidationContext.class); + doAnswer(invocation -> { - Assert.assertEquals(argument.getSchema(), "fooSchema"); - Assert.assertEquals(argument.getSchemaRepository(), "fooRepository"); + Assert.assertEquals(invocation.getArgument(3, String.class), "fooSchema"); + Assert.assertEquals(invocation.getArgument(2, String.class), "fooRepository"); validated.set(true); return null; - }).when(schemaValidator).validate(any(), any(), any()); + }).when(schemaValidator) + .validate(isA(Message.class), isA(TestContext.class), isA(String.class), + isA(String.class)); + doReturn(true).when(schemaValidator).canValidate(isA(Message.class), isA(Boolean.class)); + + ReferenceResolver referenceResolverSpy = spy(context.getReferenceResolver()); + context.setReferenceResolver(referenceResolverSpy); + + doReturn(Map.of("jsonSchemaValidator", schemaValidator)).when(referenceResolverSpy).resolveAll(SchemaValidator.class); context.getMessageValidatorRegistry().addSchemaValidator("JSON", schemaValidator); DefaultMessageBuilder messageBuilder = new DefaultMessageBuilder(); - messageBuilder.setPayloadBuilder(new DefaultPayloadBuilder("{ \"TestRequest\": { \"Message\": \"?\" }}")); + messageBuilder.setPayloadBuilder( + new DefaultPayloadBuilder("{ \"TestRequest\": { \"Message\": \"?\" }}")); reset(endpoint, producer, endpointConfiguration); when(endpoint.createProducer()).thenReturn(producer); @@ -125,20 +139,21 @@ public void testSendJsonMessageWithValidation() { when(endpoint.getActor()).thenReturn(null); SendMessageAction sendAction = new SendMessageAction.Builder() - .endpoint(endpoint) - .message(messageBuilder) - .schemaValidation(true) - .schema("fooSchema") - .schemaRepository("fooRepository") - .type(MessageType.JSON) - .build(); + .endpoint(endpoint) + .message(messageBuilder) + .schemaValidation(true) + .schema("fooSchema") + .schemaRepository("fooRepository") + .type(MessageType.JSON) + .build(); sendAction.execute(context); Assert.assertTrue(validated.get()); } private void validateMessageToSend(Message toSend, Message controlMessage) { - Assert.assertEquals(toSend.getPayload(String.class).trim(), controlMessage.getPayload(String.class).trim()); + Assert.assertEquals(toSend.getPayload(String.class).trim(), + controlMessage.getPayload(String.class).trim()); DefaultMessageHeaderValidator validator = new DefaultMessageHeaderValidator(); validator.validateMessage(toSend, controlMessage, context, new HeaderValidationContext()); } diff --git a/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/schema/XmlSchemaValidation.java b/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/schema/XmlSchemaValidation.java index 75d9a67bc4..6423365b1a 100644 --- a/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/schema/XmlSchemaValidation.java +++ b/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/schema/XmlSchemaValidation.java @@ -16,6 +16,19 @@ package org.citrusframework.validation.xml.schema; +import static java.lang.String.format; +import static org.citrusframework.validation.xml.schema.ValidationStrategy.FAIL; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import org.citrusframework.CitrusSettings; import org.citrusframework.XmlValidationHelper; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -29,6 +42,7 @@ import org.citrusframework.util.XMLUtils; import org.citrusframework.validation.SchemaValidator; import org.citrusframework.validation.xml.XmlMessageValidationContext; +import org.citrusframework.validation.xml.XmlMessageValidationContext.Builder; import org.citrusframework.xml.XsdSchemaRepository; import org.citrusframework.xml.schema.AbstractSchemaCollection; import org.citrusframework.xml.schema.WsdlXsdSchema; @@ -41,19 +55,6 @@ import org.w3c.dom.Document; import org.xml.sax.SAXParseException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static java.lang.String.format; -import static org.citrusframework.validation.xml.schema.ValidationStrategy.FAIL; - public class XmlSchemaValidation implements SchemaValidator { public static final String NO_SCHEMA_FOUND_STRATEGY_PROPERTY_NAME = "citrus.xml.no.schema.found.strategy"; @@ -222,4 +223,31 @@ private static Optional extractEnvOrProperty(SystemProvider systemProvid return systemProvider.getEnv(envVarName) .or(() -> systemProvider.getProperty(fallbackPropertyName)); } + + @Override + public boolean canValidate(Message message, boolean schemaValidationEnabled) { + return (isXmlSchemaValidationEnabled() || schemaValidationEnabled) + && IsXmlPredicate.getInstance().test(message.getPayload(String.class)); + } + + /** + * Get setting to determine if xml schema validation is enabled by default. + * @return + */ + private static boolean isXmlSchemaValidationEnabled() { + return Boolean.getBoolean(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_PROPERTY) + || Boolean.getBoolean(CitrusSettings.OUTBOUND_XML_SCHEMA_VALIDATION_ENABLED_PROPERTY) + || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_ENV)) + || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_XML_SCHEMA_VALIDATION_ENABLED_ENV)); + } + + @Override + public void validate(Message message, TestContext context, String schemaRepository, String schema) { + + XmlMessageValidationContext validationContext = Builder.xml() + .schemaValidation(true) + .schema(schema) + .schemaRepository(schemaRepository).build(); + validate(message, context, validationContext); + } } diff --git a/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/SendMessageActionTest.java b/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/SendMessageActionTest.java index a5da64c31b..871c37301c 100644 --- a/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/SendMessageActionTest.java +++ b/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/SendMessageActionTest.java @@ -234,15 +234,14 @@ public void testSendXmlMessageWithValidation() { when(schemaValidator.supportsMessageType(eq("XML"), any())).thenReturn(true); doAnswer(invocation-> { - Object argument = invocation.getArgument(2); - - Assert.assertTrue(argument instanceof XmlMessageValidationContext); - Assert.assertEquals(((XmlMessageValidationContext)argument).getSchema(), "fooSchema"); - Assert.assertEquals(((XmlMessageValidationContext)argument).getSchemaRepository(), "fooRepository"); + Assert.assertEquals(invocation.getArgument(3, String.class), "fooSchema"); + Assert.assertEquals(invocation.getArgument(2, String.class), "fooRepository"); validated.set(true); return null; - }).when(schemaValidator).validate(any(), any(), any()); + }).when(schemaValidator) + .validate(isA(Message.class), isA(TestContext.class), isA(String.class), isA(String.class)); + doReturn(true).when(schemaValidator).canValidate(isA(Message.class), isA(Boolean.class)); context.getMessageValidatorRegistry().addSchemaValidator("XML", schemaValidator);