From d373c8118c703175c305a6270c665ce340e0f119 Mon Sep 17 00:00:00 2001 From: Christoph Deppisch Date: Wed, 22 Nov 2023 17:15:13 +0100 Subject: [PATCH] feat(#1083): Add OpenAPI connector - Connector implementation adding OpenAPI client and server support - Generates request/response message data based on given OpenAPI specification - Generates validation control message content based on the specification - Users reference operations by their id - Responses get identified by the Http status code as defined in the specification - Validation/generation also includes Content-Type and resource URL path settings - Supports path parameters and random test data --- catalog/citrus-bom/pom.xml | 5 + .../docker/UnitTestSupport.java | 7 +- .../kubernetes/UnitTestSupport.java | 7 +- connectors/citrus-openapi/pom.xml | 104 +++ .../openapi/OpenApiResourceLoader.java | 157 +++++ .../openapi/OpenApiSpecification.java | 196 ++++++ .../openapi/OpenApiSupport.java | 76 +++ .../openapi/OpenApiTestDataGenerator.java | 354 ++++++++++ .../openapi/actions/OpenApiActionBuilder.java | 178 +++++ .../actions/OpenApiClientActionBuilder.java | 147 +++++ .../OpenApiClientRequestActionBuilder.java | 146 +++++ .../OpenApiClientResponseActionBuilder.java | 132 ++++ .../actions/OpenApiServerActionBuilder.java | 148 +++++ .../OpenApiServerRequestActionBuilder.java | 150 +++++ .../OpenApiServerResponseActionBuilder.java | 135 ++++ .../openapi/model/OasModelHelper.java | 266 ++++++++ .../openapi/model/OpenApiVersion.java | 45 ++ .../openapi/model/v2/Oas20ModelHelper.java | 136 ++++ .../openapi/model/v3/Oas30ModelHelper.java | 258 ++++++++ .../openapi/xml/ObjectFactory.java | 56 ++ .../citrusframework/openapi/xml/OpenApi.java | 608 ++++++++++++++++++ .../openapi/xml/package-info.java | 21 + .../citrusframework/openapi/yaml/OpenApi.java | 547 ++++++++++++++++ .../src/main/resources/META-INF/LICENSE.txt | 201 ++++++ .../src/main/resources/META-INF/NOTICE.txt | 32 + .../META-INF/citrus/action/builder/openapi | 1 + .../META-INF/citrus/xml/builder/openapi | 2 + .../META-INF/citrus/yaml/builder/openapi | 1 + .../actions/OpenApiActionBuilderTest.java | 42 ++ .../groovy/AbstractGroovyActionDslTest.java | 86 +++ .../openapi/groovy/OpenApiClientTest.java | 246 +++++++ .../openapi/groovy/OpenApiServerTest.java | 205 ++++++ .../openapi/integration/OpenApiClientIT.java | 123 ++++ .../openapi/integration/OpenApiServerIT.java | 125 ++++ .../openapi/xml/AbstractXmlActionTest.java | 82 +++ .../openapi/xml/OpenApiClientTest.java | 256 ++++++++ .../openapi/xml/OpenApiServerTest.java | 205 ++++++ .../openapi/yaml/AbstractYamlActionTest.java | 82 +++ .../openapi/yaml/OpenApiClientTest.java | 253 ++++++++ .../openapi/yaml/OpenApiServerTest.java | 205 ++++++ .../src/test/resources/log4j2-test.xml | 56 ++ .../context/citrus-unit-context.xml | 8 + .../openapi/groovy/openapi-client.test.groovy | 51 ++ .../openapi/groovy/openapi-server.test.groovy | 50 ++ .../citrusframework/openapi/petstore/pet.json | 16 + .../openapi/petstore/petstore-v2.json | 287 +++++++++ .../openapi/petstore/petstore-v3.json | 292 +++++++++ .../openapi/petstore/petstore-v3.yaml | 197 ++++++ .../openapi/xml/openapi-client-test.xml | 44 ++ .../openapi/xml/openapi-server-test.xml | 44 ++ .../openapi/yaml/openapi-client-test.yaml | 32 + .../openapi/yaml/openapi-server-test.yaml | 33 + .../selenium/UnitTestSupport.java | 7 +- .../selenium/actions/AlertActionTest.java | 7 +- .../org/citrusframework/UnitTestSupport.java | 7 +- connectors/pom.xml | 1 + .../DefaultTextEqualsMessageValidator.java | 20 +- .../actions/SendMessageAction.java | 1 - .../context/TestContextFactory.java | 9 +- .../DefaultMessageValidatorRegistry.java | 1 - .../org/citrusframework/UnitTestSupport.java | 7 +- ...DefaultTextEqualsMessageValidatorTest.java | 14 + .../camel/UnitTestSupport.java | 7 +- .../HttpClientRequestActionBuilder.java | 14 +- .../HttpClientResponseActionBuilder.java | 17 +- .../HttpServerRequestActionBuilder.java | 16 +- .../HttpServerResponseActionBuilder.java | 14 +- .../org/citrusframework/http/xml/Http.java | 4 +- .../org/citrusframework/http/yaml/Http.java | 4 +- .../citrusframework/http/UnitTestSupport.java | 5 - .../HttpQueryParamHeaderValidatorTest.java | 7 +- .../citrusframework/jms/UnitTestSupport.java | 7 +- .../org/citrusframework/UnitTestSupport.java | 7 +- .../citrusframework/ws/UnitTestSupport.java | 5 - .../ws/groovy/SoapServerTest.java | 2 +- .../ws/xml/SoapServerTest.java | 2 +- .../ws/yaml/SoapServerTest.java | 2 +- .../zookeeper/UnitTestSupport.java | 7 +- pom.xml | 7 + .../org/citrusframework/UnitTestSupport.java | 5 - .../actions/ReceiveMessageActionTest.java | 11 +- .../actions/SendMessageActionTest.java | 7 +- .../org/citrusframework/UnitTestSupport.java | 4 - .../citrus-testcase-4.1.0-SNAPSHOT.xsd | 64 ++ .../schema/xml/testcase/citrus-testcase.xsd | 65 ++ src/main/assembly/dist-antlibs.xml | 1 + src/main/assembly/dist-release.xml | 8 + src/main/assembly/dist-sources.xml | 7 + src/manual/connector-openapi.adoc | 311 +++++++++ src/manual/connectors.adoc | 9 + src/manual/index.adoc | 2 + .../org/citrusframework/UnitTestSupport.java | 7 +- .../script/ReceiveMessageActionTest.java | 12 +- .../org/citrusframework/UnitTestSupport.java | 4 - .../validation/ValidationUtilsTest.java | 7 +- .../org/citrusframework/UnitTestSupport.java | 5 - .../json/ReceiveMessageActionTest.java | 5 - .../json/SendMessageActionTest.java | 19 +- .../text/PlainTextMessageValidatorTest.java | 8 +- .../org/citrusframework/UnitTestSupport.java | 5 - .../xml/ReceiveMessageActionTest.java | 5 - .../validation/xml/SendMessageActionTest.java | 22 +- 102 files changed, 7729 insertions(+), 201 deletions(-) create mode 100644 connectors/citrus-openapi/pom.xml create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiResourceLoader.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecification.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSupport.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiActionBuilder.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilder.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientRequestActionBuilder.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientResponseActionBuilder.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilder.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerRequestActionBuilder.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerResponseActionBuilder.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OpenApiVersion.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/v2/Oas20ModelHelper.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/v3/Oas30ModelHelper.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/ObjectFactory.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/OpenApi.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/package-info.java create mode 100644 connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/yaml/OpenApi.java create mode 100644 connectors/citrus-openapi/src/main/resources/META-INF/LICENSE.txt create mode 100644 connectors/citrus-openapi/src/main/resources/META-INF/NOTICE.txt create mode 100644 connectors/citrus-openapi/src/main/resources/META-INF/citrus/action/builder/openapi create mode 100644 connectors/citrus-openapi/src/main/resources/META-INF/citrus/xml/builder/openapi create mode 100644 connectors/citrus-openapi/src/main/resources/META-INF/citrus/yaml/builder/openapi create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiActionBuilderTest.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/AbstractGroovyActionDslTest.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiClientTest.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiServerTest.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientIT.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiServerIT.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/AbstractXmlActionTest.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiClientTest.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiServerTest.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/AbstractYamlActionTest.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiClientTest.java create mode 100644 connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiServerTest.java create mode 100644 connectors/citrus-openapi/src/test/resources/log4j2-test.xml create mode 100644 connectors/citrus-openapi/src/test/resources/org/citrusframework/context/citrus-unit-context.xml create mode 100644 connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/groovy/openapi-client.test.groovy create mode 100644 connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/groovy/openapi-server.test.groovy create mode 100644 connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/pet.json create mode 100644 connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v2.json create mode 100644 connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v3.json create mode 100644 connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v3.yaml create mode 100644 connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/xml/openapi-client-test.xml create mode 100644 connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/xml/openapi-server-test.xml create mode 100644 connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/yaml/openapi-client-test.yaml create mode 100644 connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/yaml/openapi-server-test.yaml create mode 100644 src/manual/connector-openapi.adoc create mode 100644 src/manual/connectors.adoc diff --git a/catalog/citrus-bom/pom.xml b/catalog/citrus-bom/pom.xml index e928bae235..266c3256eb 100644 --- a/catalog/citrus-bom/pom.xml +++ b/catalog/citrus-bom/pom.xml @@ -213,6 +213,11 @@ citrus-http ${project.version} + + org.citrusframework + citrus-openapi + ${project.version} + org.citrusframework citrus-jms diff --git a/connectors/citrus-docker/src/test/java/org/citrusframework/docker/UnitTestSupport.java b/connectors/citrus-docker/src/test/java/org/citrusframework/docker/UnitTestSupport.java index fc2ebd56a8..728a751b6c 100644 --- a/connectors/citrus-docker/src/test/java/org/citrusframework/docker/UnitTestSupport.java +++ b/connectors/citrus-docker/src/test/java/org/citrusframework/docker/UnitTestSupport.java @@ -2,8 +2,6 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.annotations.BeforeMethod; /** @@ -24,9 +22,6 @@ public void prepareTest() { } protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = TestContextFactory.newInstance(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } } diff --git a/connectors/citrus-kubernetes/src/test/java/org/citrusframework/kubernetes/UnitTestSupport.java b/connectors/citrus-kubernetes/src/test/java/org/citrusframework/kubernetes/UnitTestSupport.java index 815782e4ee..8d2cb542fb 100644 --- a/connectors/citrus-kubernetes/src/test/java/org/citrusframework/kubernetes/UnitTestSupport.java +++ b/connectors/citrus-kubernetes/src/test/java/org/citrusframework/kubernetes/UnitTestSupport.java @@ -2,8 +2,6 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.annotations.BeforeMethod; /** @@ -24,9 +22,6 @@ public void prepareTest() { } protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = TestContextFactory.newInstance(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } } diff --git a/connectors/citrus-openapi/pom.xml b/connectors/citrus-openapi/pom.xml new file mode 100644 index 0000000000..3cb4fe34f4 --- /dev/null +++ b/connectors/citrus-openapi/pom.xml @@ -0,0 +1,104 @@ + + 4.0.0 + + + citrus-connectors + org.citrusframework + 4.1.0-SNAPSHOT + ../pom.xml + + + citrus-openapi + Citrus :: Connectors :: OpenAPI + + + + org.citrusframework + citrus-base + ${project.version} + + + org.citrusframework + citrus-http + ${project.version} + + + org.citrusframework + citrus-spring + ${project.version} + provided + + + org.citrusframework + citrus-xml + ${project.version} + provided + + + org.citrusframework + citrus-yaml + ${project.version} + provided + + + + io.apicurio + apicurio-data-models + + + + + org.citrusframework + citrus-test-support + ${project.version} + test + + + org.citrusframework + citrus-testng + ${project.version} + test + + + org.citrusframework + citrus-groovy + ${project.version} + test + + + org.citrusframework + citrus-validation-json + ${project.version} + test + + + + + + schemagen + + + + org.codehaus.mojo + jaxb2-maven-plugin + + + + schemagen + + generate-resources + + + src/main/java/org/citrusframework/openapi/xml/Openapi.java + + ${project.build.directory}/schemas + + + + + + + + + diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiResourceLoader.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiResourceLoader.java new file mode 100644 index 0000000000..0e432761b0 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiResourceLoader.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.Objects; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; + +import com.fasterxml.jackson.databind.JsonNode; +import io.apicurio.datamodels.Library; +import io.apicurio.datamodels.openapi.models.OasDocument; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.TrustAllStrategy; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.ssl.SSLContexts; +import org.citrusframework.spi.Resource; +import org.citrusframework.util.FileUtils; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; + +/** + * Loads Open API specifications from different locations like file resource or web resource. + * @author Christoph Deppisch + */ +public final class OpenApiResourceLoader { + + /** + * Prevent instantiation of utility class. + */ + private OpenApiResourceLoader() { + super(); + } + + /** + * Loads the specification from a file resource. Either classpath or file system resource path is supported. + * @param resource + * @return + */ + public static OasDocument fromFile(String resource) { + return fromFile(FileUtils.getFileResource(resource)); + } + + /** + * Loads the specification from a file resource. Either classpath or file system resource path is supported. + * @param resource + * @return + */ + public static OasDocument fromFile(Resource resource) { + try { + return resolve(FileUtils.readToString(resource)); + } catch (IOException e) { + throw new IllegalStateException("Failed to parse Open API specification: " + resource, e); + } + } + + /** + * Loads specification from given web URL location. + * @param url + * @return + */ + public static OasDocument fromWebResource(URL url) { + HttpURLConnection con = null; + try { + con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod(HttpMethod.GET.name()); + con.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + + int status = con.getResponseCode(); + if (status > 299) { + throw new IllegalStateException("Failed to retrieve Open API specification: " + url.toString(), + new IOException(FileUtils.readToString(con.getErrorStream()))); + } else { + return resolve(FileUtils.readToString(con.getInputStream())); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to retrieve Open API specification: " + url.toString(), e); + } finally { + if (con != null) { + con.disconnect(); + } + } + } + + /** + * Loads specification from given web URL location using secured Http connection. + * @param url + * @return + */ + public static OasDocument fromSecuredWebResource(URL url) { + Objects.requireNonNull(url); + + HttpsURLConnection con = null; + try { + SSLContext sslcontext = SSLContexts + .custom() + .loadTrustMaterial(TrustAllStrategy.INSTANCE) + .build(); + + HttpsURLConnection.setDefaultSSLSocketFactory(sslcontext.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier(NoopHostnameVerifier.INSTANCE); + + con = (HttpsURLConnection) url.openConnection(); + con.setRequestMethod(HttpMethod.GET.name()); + con.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + + int status = con.getResponseCode(); + if (status > 299) { + throw new IllegalStateException("Failed to retrieve Open API specification: " + url.toString(), + new IOException(FileUtils.readToString(con.getErrorStream()))); + } else { + return resolve(FileUtils.readToString(con.getInputStream())); + } + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new IllegalStateException("Failed to create https client for ssl connection", e); + } catch (IOException e) { + throw new IllegalStateException("Failed to retrieve Open API specification: " + url.toString(), e); + } finally { + if (con != null) { + con.disconnect(); + } + } + } + + private static OasDocument resolve(String specification) { + if (isJsonSpec(specification)) { + return (OasDocument) Library.readDocumentFromJSONString(specification); + } + + final JsonNode node = OpenApiSupport.json().convertValue(OpenApiSupport.yaml().load(specification), JsonNode.class); + return (OasDocument) Library.readDocument(node); + } + + private static boolean isJsonSpec(final String specification) { + return specification.trim().startsWith("{"); + } +} 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 new file mode 100644 index 0000000000..235142fdb6 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecification.java @@ -0,0 +1,196 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Optional; + +import io.apicurio.datamodels.openapi.models.OasDocument; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.http.client.HttpClient; +import org.citrusframework.openapi.model.OasModelHelper; +import org.citrusframework.spi.Resource; +import org.citrusframework.spi.Resources; + +/** + * OpenApi specification resolves URL or local file resources to a specification document. + */ +public class OpenApiSpecification { + + /** URL to load the OpenAPI specification */ + private String specUrl; + + private String httpClient; + private String requestUrl; + + private OasDocument openApiDoc; + + private boolean generateOptionalFields = true; + + private boolean validateOptionalFields = true; + + public static OpenApiSpecification from(String specUrl) { + OpenApiSpecification specification = new OpenApiSpecification(); + specification.setSpecUrl(specUrl); + + return specification; + } + + public static OpenApiSpecification from(URL specUrl) { + OpenApiSpecification specification = new OpenApiSpecification(); + OasDocument openApiDoc; + if (specUrl.getProtocol().startsWith("https")) { + openApiDoc = OpenApiResourceLoader.fromSecuredWebResource(specUrl); + } else { + openApiDoc = OpenApiResourceLoader.fromWebResource(specUrl); + } + + specification.setSpecUrl(specUrl.toString()); + specification.setOpenApiDoc(openApiDoc); + specification.setRequestUrl(String.format("%s://%s%s%s", specUrl.getProtocol(), specUrl.getHost(), specUrl.getPort() > 0 ? ":" + specUrl.getPort() : "", OasModelHelper.getBasePath(openApiDoc))); + + return specification; + } + + public static OpenApiSpecification from(Resource resource) { + OpenApiSpecification specification = new OpenApiSpecification(); + OasDocument openApiDoc = OpenApiResourceLoader.fromFile(resource); + + specification.setOpenApiDoc(openApiDoc); + + String schemeToUse = Optional.ofNullable(OasModelHelper.getSchemes(openApiDoc)) + .orElse(Collections.singletonList("http")) + .stream() + .filter(s -> s.equals("http") || s.equals("https")) + .findFirst() + .orElse("http"); + + specification.setSpecUrl(resource.getLocation()); + specification.setRequestUrl(String.format("%s://%s%s", schemeToUse, OasModelHelper.getHost(openApiDoc), OasModelHelper.getBasePath(openApiDoc))); + + return specification; + } + + public OasDocument getOpenApiDoc(TestContext context) { + if (openApiDoc != null) { + return openApiDoc; + } + + if (specUrl != null) { + String resolvedSpecUrl = context.replaceDynamicContentInString(specUrl); + + if (resolvedSpecUrl.startsWith("/")) { + // relative path URL - try to resolve with given request URL + if (requestUrl != null) { + resolvedSpecUrl = requestUrl.endsWith("/") ? requestUrl + resolvedSpecUrl.substring(1) : requestUrl + resolvedSpecUrl; + } else if (httpClient != null && context.getReferenceResolver().isResolvable(httpClient, HttpClient.class)) { + String baseUrl = context.getReferenceResolver().resolve(httpClient, HttpClient.class).getEndpointConfiguration().getRequestUrl(); + resolvedSpecUrl = baseUrl.endsWith("/") ? baseUrl + resolvedSpecUrl.substring(1) : baseUrl + resolvedSpecUrl;; + } else { + throw new CitrusRuntimeException(("Failed to resolve OpenAPI spec URL from relative path %s - " + + "make sure to provide a proper base URL when using relative paths").formatted(resolvedSpecUrl)); + } + } + + if (resolvedSpecUrl.startsWith("http")) { + try { + URL specWebResource = new URL(resolvedSpecUrl); + if (resolvedSpecUrl.startsWith("https")) { + openApiDoc = OpenApiResourceLoader.fromSecuredWebResource(specWebResource); + } else { + openApiDoc = OpenApiResourceLoader.fromWebResource(specWebResource); + } + + if (requestUrl == null) { + setRequestUrl(String.format("%s://%s%s%s", specWebResource.getProtocol(), specWebResource.getHost(), specWebResource.getPort() > 0 ? ":" + specWebResource.getPort() : "", OasModelHelper.getBasePath(openApiDoc))); + } + } catch (MalformedURLException e) { + throw new IllegalStateException("Failed to retrieve Open API specification as web resource: " + specUrl, e); + } + } else { + openApiDoc = OpenApiResourceLoader.fromFile(Resources.create(resolvedSpecUrl)); + + if (requestUrl == null) { + String schemeToUse = Optional.ofNullable(OasModelHelper.getSchemes(openApiDoc)) + .orElse(Collections.singletonList("http")) + .stream() + .filter(s -> s.equals("http") || s.equals("https")) + .findFirst() + .orElse("http"); + + setRequestUrl(String.format("%s://%s%s", schemeToUse, OasModelHelper.getHost(openApiDoc), OasModelHelper.getBasePath(openApiDoc))); + } + } + } + + return openApiDoc; + } + + public void setOpenApiDoc(OasDocument openApiDoc) { + this.openApiDoc = openApiDoc; + } + + public String getSpecUrl() { + return specUrl; + } + + public void setSpecUrl(String specUrl) { + this.specUrl = specUrl; + } + + public void setHttpClient(String httpClient) { + this.httpClient = httpClient; + } + + public String getHttpClient() { + return httpClient; + } + + public String getRequestUrl() { + if (requestUrl == null) { + return specUrl; + } + + return requestUrl; + } + + public void setRequestUrl(String requestUrl) { + this.requestUrl = requestUrl; + } + + public boolean isGenerateOptionalFields() { + return generateOptionalFields; + } + + public void setGenerateOptionalFields(boolean generateOptionalFields) { + this.generateOptionalFields = generateOptionalFields; + } + + public boolean isValidateOptionalFields() { + return validateOptionalFields; + } + + public void setValidateOptionalFields(boolean validateOptionalFields) { + this.validateOptionalFields = validateOptionalFields; + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSupport.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSupport.java new file mode 100644 index 0000000000..70a3c120e2 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSupport.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import java.util.Collection; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +public class OpenApiSupport { + + private static final ObjectMapper OBJECT_MAPPER; + + static { + OBJECT_MAPPER = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING) + .enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .disable(JsonParser.Feature.AUTO_CLOSE_SOURCE) + .enable(MapperFeature.BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES) + .build() + .setDefaultPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY)); + } + + private OpenApiSupport() { + // prevent instantiation of utility class + } + + public static ObjectMapper json() { + return OBJECT_MAPPER; + } + + public static Yaml yaml() { + Representer representer = new Representer(new DumperOptions()) { + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) { + // if value of property is null, ignore it. + if (propertyValue == null || (propertyValue instanceof Collection && ((Collection) propertyValue).isEmpty()) || + (propertyValue instanceof Map && ((Map) propertyValue).isEmpty())) { + return null; + } else { + return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + } + } + }; + representer.getPropertyUtils().setSkipMissingProperties(true); + return new Yaml(representer); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java new file mode 100644 index 0000000000..944e35224f --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java @@ -0,0 +1,354 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import java.util.Map; +import java.util.stream.Collectors; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import org.citrusframework.CitrusSettings; +import org.citrusframework.context.TestContext; +import org.citrusframework.openapi.model.OasModelHelper; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Generates proper payloads and validation expressions based on Open API specification rules. Creates outbound payloads + * with generated random test data according to specification and creates inbound payloads with proper validation expressions to + * enforce the specification rules. + * + * @author Christoph Deppisch + */ +public class OpenApiTestDataGenerator { + + /** + * Creates payload from schema for outbound message. + * @param schema + * @param definitions + * @return + */ + public static String createOutboundPayload(OasSchema schema, Map definitions, + OpenApiSpecification specification) { + if (OasModelHelper.isReferenceType(schema)) { + OasSchema resolved = definitions.get(OasModelHelper.getReferenceName(schema.$ref)); + return createOutboundPayload(resolved, definitions, specification); + } + + StringBuilder payload = new StringBuilder(); + if (OasModelHelper.isObjectType(schema)) { + payload.append("{"); + + if (schema.properties != null) { + for (Map.Entry entry : schema.properties.entrySet()) { + if (specification.isGenerateOptionalFields() || isRequired(schema, entry.getKey())) { + payload.append("\"") + .append(entry.getKey()) + .append("\": ") + .append(createRandomValueExpression(entry.getValue(), definitions, true, specification)) + .append(","); + } + } + } + + if (payload.toString().endsWith(",")) { + payload.replace(payload.length() - 1, payload.length(), ""); + } + + payload.append("}"); + } else if (OasModelHelper.isArrayType(schema)) { + payload.append("["); + payload.append(createRandomValueExpression((OasSchema) schema.items, definitions, true, specification)); + payload.append("]"); + } else { + payload.append(createRandomValueExpression(schema, definitions, true, specification)); + } + + return payload.toString(); + } + + /** + * Use test variable with given name if present or create value from schema with random values + * @param schema + * @param definitions + * @param quotes + * @return + */ + public static String createRandomValueExpression(String name, OasSchema schema, Map definitions, + boolean quotes, OpenApiSpecification specification, TestContext context) { + if (context.getVariables().containsKey(name)) { + return CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX; + } + + return createRandomValueExpression(schema, definitions, quotes, specification); + } + + /** + * Create payload from schema with random values. + * @param schema + * @param definitions + * @param quotes + * @return + */ + public static String createRandomValueExpression(OasSchema schema, Map definitions, boolean quotes, + OpenApiSpecification specification) { + if (OasModelHelper.isReferenceType(schema)) { + OasSchema resolved = definitions.get(OasModelHelper.getReferenceName(schema.$ref)); + return createRandomValueExpression(resolved, definitions, quotes, specification); + } + + StringBuilder payload = new StringBuilder(); + if (OasModelHelper.isObjectType(schema) || OasModelHelper.isArrayType(schema)) { + payload.append(createOutboundPayload(schema, definitions, specification)); + } else if ("string".equals(schema.type)) { + if (quotes) { + payload.append("\""); + } + + if (schema.format != null && schema.format.equals("date")) { + payload.append("citrus:currentDate()"); + } else if (schema.format != null && schema.format.equals("date-time")) { + payload.append("citrus:currentDate('yyyy-MM-dd'T'hh:mm:ss')"); + } else if (StringUtils.hasText(schema.pattern)) { + payload.append("citrus:randomValue(").append(schema.pattern).append(")"); + } else if (!CollectionUtils.isEmpty(schema.enum_)) { + payload.append("citrus:randomEnumValue(").append(schema.enum_.stream().map(value -> "'" + value + "'").collect(Collectors.joining(","))).append(")"); + } else if (schema.format != null && schema.format.equals("uuid")) { + payload.append("citrus:randomUUID()"); + } else { + payload.append("citrus:randomString(").append(schema.maxLength != null && schema.maxLength.intValue() > 0 ? schema.maxLength : (schema.minLength != null && schema.minLength.intValue() > 0 ? schema.minLength : 10)).append(")"); + } + + if (quotes) { + payload.append("\""); + } + } else if ("integer".equals(schema.type) || "number".equals(schema.type)) { + payload.append("citrus:randomNumber(8)"); + } else if ("boolean".equals(schema.type)) { + payload.append("citrus:randomEnumValue('true', 'false')"); + } else if (quotes) { + payload.append("\"\""); + } + + return payload.toString(); + } + + /** + * Creates control payload from schema for validation. + * @param schema + * @param definitions + * @return + */ + public static String createInboundPayload(OasSchema schema, Map definitions, + OpenApiSpecification specification) { + if (OasModelHelper.isReferenceType(schema)) { + OasSchema resolved = definitions.get(OasModelHelper.getReferenceName(schema.$ref)); + return createInboundPayload(resolved, definitions, specification); + } + + StringBuilder payload = new StringBuilder(); + if (OasModelHelper.isObjectType(schema)) { + payload.append("{"); + + if (schema.properties != null) { + for (Map.Entry entry : schema.properties.entrySet()) { + if (specification.isValidateOptionalFields() || isRequired(schema, entry.getKey())) { + payload.append("\"") + .append(entry.getKey()) + .append("\": ") + .append(createValidationExpression(entry.getValue(), definitions, true, specification)) + .append(","); + } + } + } + + if (payload.toString().endsWith(",")) { + payload.replace(payload.length() - 1, payload.length(), ""); + } + + payload.append("}"); + } else if (OasModelHelper.isArrayType(schema)) { + payload.append("["); + payload.append(createValidationExpression((OasSchema) schema.items, definitions, true, specification)); + payload.append("]"); + } else { + payload.append(createValidationExpression(schema, definitions, false, specification)); + } + + return payload.toString(); + } + + /** + * Checks if given field name is in list of required fields for this schema. + * @param schema + * @param field + * @return + */ + private static boolean isRequired(OasSchema schema, String field) { + if (schema.required == null) { + return true; + } + + return schema.required.contains(field); + } + + /** + * Use test variable with given name if present or create validation expression using functions according to schema type and format. + * @param name + * @param schema + * @param definitions + * @param quotes + * @param context + * @return + */ + public static String createValidationExpression(String name, OasSchema schema, Map definitions, + boolean quotes, OpenApiSpecification specification, + TestContext context) { + if (context.getVariables().containsKey(name)) { + return CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX; + } + + return createValidationExpression(schema, definitions, quotes, specification); + } + + /** + * Create validation expression using functions according to schema type and format. + * @param schema + * @param definitions + * @param quotes + * @return + */ + public static String createValidationExpression(OasSchema schema, Map definitions, boolean quotes, + OpenApiSpecification specification) { + if (OasModelHelper.isReferenceType(schema)) { + OasSchema resolved = definitions.get(OasModelHelper.getReferenceName(schema.$ref)); + return createValidationExpression(resolved, definitions, quotes, specification); + } + + StringBuilder payload = new StringBuilder(); + if (OasModelHelper.isObjectType(schema)) { + payload.append("{"); + + if (schema.properties != null) { + for (Map.Entry entry : schema.properties.entrySet()) { + if (specification.isValidateOptionalFields() || isRequired(schema, entry.getKey())) { + payload.append("\"") + .append(entry.getKey()) + .append("\": ") + .append(createValidationExpression(entry.getValue(), definitions, quotes, specification)) + .append(","); + } + } + } + + if (payload.toString().endsWith(",")) { + payload.replace(payload.length() - 1, payload.length(), ""); + } + + payload.append("}"); + } else { + if (quotes) { + payload.append("\""); + } + + payload.append(createValidationExpression(schema)); + + if (quotes) { + payload.append("\""); + } + } + + return payload.toString(); + } + + /** + * Create validation expression using functions according to schema type and format. + * @param schema + * @return + */ + private static String createValidationExpression(OasSchema schema) { + switch (schema.type) { + case "string": + if (schema.format != null && schema.format.equals("date")) { + return "@matchesDatePattern('yyyy-MM-dd')@"; + } else if (schema.format != null && schema.format.equals("date-time")) { + return "@matchesDatePattern('yyyy-MM-dd'T'hh:mm:ss')@"; + } else if (StringUtils.hasText(schema.pattern)) { + return String.format("@matches(%s)@", schema.pattern); + } else if (!CollectionUtils.isEmpty(schema.enum_)) { + return String.format("@matches(%s)@", String.join("|", schema.enum_)); + } else { + return "@notEmpty()@"; + } + case "number": + case "integer": + return "@isNumber()@"; + case "boolean": + return "@matches(true|false)@"; + default: + return "@ignore@"; + } + } + + /** + * Use test variable with given name (if present) or create random value expression using functions according to + * schema type and format. + * @param name + * @param schema + * @param context + * @return + */ + public static String createRandomValueExpression(String name, OasSchema schema, TestContext context) { + if (context.getVariables().containsKey(name)) { + return CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX; + } + + return createRandomValueExpression(schema); + } + + /** + * Create random value expression using functions according to schema type and format. + * @param schema + * @return + */ + public static String createRandomValueExpression(OasSchema schema) { + switch (schema.type) { + case "string": + if (schema.format != null && schema.format.equals("date")) { + return "\"citrus:currentDate('yyyy-MM-dd')\""; + } else if (schema.format != null && schema.format.equals("date-time")) { + return "\"citrus:currentDate('yyyy-MM-dd'T'hh:mm:ss')\""; + } else if (StringUtils.hasText(schema.pattern)) { + return "\"citrus:randomValue(" + schema.pattern + ")\""; + } else if (!CollectionUtils.isEmpty(schema.enum_)) { + return "\"citrus:randomEnumValue(" + (String.join(",", schema.enum_)) + ")\""; + } else if (schema.format != null && schema.format.equals("uuid")){ + return "citrus:randomUUID()"; + } else { + return "citrus:randomString(10)"; + } + case "number": + case "integer": + return "citrus:randomNumber(8)"; + case "boolean": + return "citrus:randomEnumValue('true', 'false')"; + default: + return ""; + } + } + +} 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 new file mode 100644 index 0000000000..6d2ecb89dc --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiActionBuilder.java @@ -0,0 +1,178 @@ +/* + * Copyright 2006-2015 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.actions; + +import java.net.URL; + +import org.citrusframework.TestAction; +import org.citrusframework.TestActionBuilder; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.http.client.HttpClient; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.spi.ReferenceResolver; +import org.citrusframework.spi.ReferenceResolverAware; +import org.citrusframework.util.ObjectHelper; + +/** + * Action executes client and server operations using given OpenApi specification. + * Action creates proper request and response data from given specification rules. + * + * @author Christoph Deppisch + * @since 4.1 + */ +public class OpenApiActionBuilder implements TestActionBuilder.DelegatingTestActionBuilder, ReferenceResolverAware { + + /** Bean reference resolver */ + private ReferenceResolver referenceResolver; + + private TestActionBuilder delegate; + + private OpenApiSpecification specification; + + public OpenApiActionBuilder() { + } + + public OpenApiActionBuilder(OpenApiSpecification specification) { + this.specification = specification; + } + + /** + * Static entrance method for the OpenApi fluent action builder. + * @return + */ + public static OpenApiActionBuilder openapi() { + return new OpenApiActionBuilder(); + } + + public static OpenApiActionBuilder openapi(OpenApiSpecification specification) { + return new OpenApiActionBuilder(specification); + } + + public OpenApiActionBuilder specification(OpenApiSpecification specification) { + this.specification = specification; + return this; + } + + public OpenApiActionBuilder specification(URL specUrl) { + return specification(OpenApiSpecification.from(specUrl)); + } + + public OpenApiActionBuilder specification(String specUrl) { + return specification(OpenApiSpecification.from(specUrl)); + } + + public OpenApiClientActionBuilder client() { + assertSpecification(); + return client(specification.getRequestUrl()); + } + + /** + * Initiate http client action. + */ + public OpenApiClientActionBuilder client(HttpClient httpClient) { + assertSpecification(); + + if (httpClient.getEndpointConfiguration().getRequestUrl() != null) { + specification.setRequestUrl(httpClient.getEndpointConfiguration().getRequestUrl()); + } + + OpenApiClientActionBuilder clientActionBuilder = new OpenApiClientActionBuilder(httpClient, specification) + .withReferenceResolver(referenceResolver); + this.delegate = clientActionBuilder; + return clientActionBuilder; + } + + /** + * Initiate http client action. + */ + public OpenApiClientActionBuilder client(String httpClient) { + assertSpecification(); + + specification.setHttpClient(httpClient); + + OpenApiClientActionBuilder clientActionBuilder = new OpenApiClientActionBuilder(httpClient, specification) + .withReferenceResolver(referenceResolver); + this.delegate = clientActionBuilder; + return clientActionBuilder; + } + + /** + * Initiate http server action. + */ + public OpenApiServerActionBuilder server(Endpoint endpoint) { + assertSpecification(); + + OpenApiServerActionBuilder serverActionBuilder = new OpenApiServerActionBuilder(endpoint, specification) + .withReferenceResolver(referenceResolver); + this.delegate = serverActionBuilder; + return serverActionBuilder; + } + + private void assertSpecification() { + if (specification == null) { + throw new CitrusRuntimeException("Invalid OpenApi specification - please set specification first"); + } + } + + /** + * Initiate http server action. + */ + public OpenApiServerActionBuilder server(String httpServer) { + assertSpecification(); + + OpenApiServerActionBuilder serverActionBuilder = new OpenApiServerActionBuilder(httpServer, specification) + .withReferenceResolver(referenceResolver); + this.delegate = serverActionBuilder; + return serverActionBuilder; + } + + /** + * Sets the bean reference resolver. + * @param referenceResolver + */ + public OpenApiActionBuilder withReferenceResolver(ReferenceResolver referenceResolver) { + this.referenceResolver = referenceResolver; + return this; + } + + @Override + public TestAction build() { + ObjectHelper.assertNotNull(delegate, "Missing delegate action to build"); + return delegate.build(); + } + + @Override + public TestActionBuilder getDelegate() { + return delegate; + } + + /** + * Specifies the referenceResolver. + * @param referenceResolver + */ + @Override + public void setReferenceResolver(ReferenceResolver referenceResolver) { + if (referenceResolver == null) { + this.referenceResolver = referenceResolver; + + if (delegate instanceof ReferenceResolverAware) { + ((ReferenceResolverAware) delegate).setReferenceResolver(referenceResolver); + } + } + } +} 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 new file mode 100644 index 0000000000..2f150d6c90 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilder.java @@ -0,0 +1,147 @@ +/* + * Copyright 2006-2015 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.actions; + +import org.citrusframework.TestAction; +import org.citrusframework.TestActionBuilder; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.spi.ReferenceResolver; +import org.citrusframework.spi.ReferenceResolverAware; +import org.citrusframework.util.ObjectHelper; +import org.springframework.http.HttpStatus; + +/** + * Action executes http client operations such as sending requests and receiving responses. + * + * @author Christoph Deppisch + * @since 4.1 + */ +public class OpenApiClientActionBuilder implements TestActionBuilder.DelegatingTestActionBuilder, ReferenceResolverAware { + + private final OpenApiSpecification specification; + + /** Bean reference resolver */ + private ReferenceResolver referenceResolver; + + /** Target http client instance */ + private Endpoint httpClient; + private String httpClientUri; + + private TestActionBuilder delegate; + + /** + * Default constructor. + */ + public OpenApiClientActionBuilder(Endpoint httpClient, OpenApiSpecification specification) { + this.httpClient = httpClient; + this.specification = specification; + } + + /** + * Default constructor. + */ + public OpenApiClientActionBuilder(String httpClientUri, OpenApiSpecification specification) { + this.httpClientUri = httpClientUri; + this.specification = specification; + } + + /** + * Sends Http requests as client. + */ + public OpenApiClientRequestActionBuilder send(String operationId) { + OpenApiClientRequestActionBuilder builder = new OpenApiClientRequestActionBuilder(specification, operationId); + if (httpClient != null) { + builder.endpoint(httpClient); + } else { + builder.endpoint(httpClientUri); + } + + builder.name("openapi:send-request"); + builder.withReferenceResolver(referenceResolver); + + this.delegate = builder; + return builder; + } + + /** + * Receives Http response messages as client. + * Uses default Http status 200 OK. + */ + public OpenApiClientResponseActionBuilder receive(String operationId) { + return receive(operationId, HttpStatus.OK); + } + + /** + * Receives Http response messages as client. + */ + public OpenApiClientResponseActionBuilder receive(String operationId, HttpStatus status) { + return receive(operationId, String.valueOf(status.value())); + } + + /** + * Receives Http response messages as client. + */ + public OpenApiClientResponseActionBuilder receive(String operationId, String statusCode) { + OpenApiClientResponseActionBuilder builder = new OpenApiClientResponseActionBuilder(specification, operationId, statusCode); + if (httpClient != null) { + builder.endpoint(httpClient); + } else { + builder.endpoint(httpClientUri); + } + + builder.name("openapi:receive-response"); + builder.withReferenceResolver(referenceResolver); + this.delegate = builder; + return builder; + } + + /** + * Sets the bean reference resolver. + * @param referenceResolver + */ + public OpenApiClientActionBuilder withReferenceResolver(ReferenceResolver referenceResolver) { + this.referenceResolver = referenceResolver; + return this; + } + + @Override + public TestAction build() { + ObjectHelper.assertNotNull(delegate, "Missing delegate action to build"); + return delegate.build(); + } + + @Override + public TestActionBuilder getDelegate() { + return delegate; + } + + /** + * Specifies the referenceResolver. + * @param referenceResolver + */ + @Override + public void setReferenceResolver(ReferenceResolver referenceResolver) { + if (referenceResolver == null) { + this.referenceResolver = referenceResolver; + + if (delegate instanceof ReferenceResolverAware) { + ((ReferenceResolverAware) delegate).setReferenceResolver(referenceResolver); + } + } + } +} 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 new file mode 100644 index 0000000000..e689eaa7d4 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientRequestActionBuilder.java @@ -0,0 +1,146 @@ +/* + * Copyright 2006-2015 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.actions; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +import io.apicurio.datamodels.openapi.models.OasDocument; +import io.apicurio.datamodels.openapi.models.OasOperation; +import io.apicurio.datamodels.openapi.models.OasParameter; +import io.apicurio.datamodels.openapi.models.OasPathItem; +import io.apicurio.datamodels.openapi.models.OasSchema; +import org.citrusframework.CitrusSettings; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.http.actions.HttpClientRequestActionBuilder; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.message.Message; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.OpenApiTestDataGenerator; +import org.citrusframework.openapi.model.OasModelHelper; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * @author Christoph Deppisch + * @since 4.1 + */ +public class OpenApiClientRequestActionBuilder extends HttpClientRequestActionBuilder { + + /** + * Default constructor initializes http request message builder. + */ + public OpenApiClientRequestActionBuilder(OpenApiSpecification openApiSpec, String operationId) { + this(new HttpMessage(), openApiSpec, operationId); + } + + public OpenApiClientRequestActionBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, + String operationId) { + super(new OpenApiClientRequestMessageBuilder(httpMessage, openApiSpec, operationId), httpMessage); + } + + private static class OpenApiClientRequestMessageBuilder extends HttpMessageBuilder { + + private final OpenApiSpecification openApiSpec; + private final String operationId; + + private final HttpMessage httpMessage; + + public OpenApiClientRequestMessageBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, + String operationId) { + super(httpMessage); + this.openApiSpec = openApiSpec; + this.operationId = operationId; + this.httpMessage = httpMessage; + } + + @Override + public Message build(TestContext context, String messageType) { + OasDocument oasDocument = openApiSpec.getOpenApiDoc(context); + OasOperation operation = null; + OasPathItem pathItem = null; + HttpMethod method = null; + + for (OasPathItem path : OasModelHelper.getPathItems(oasDocument.paths)) { + Optional> operationEntry = OasModelHelper.getOperationMap(path).entrySet().stream() + .filter(op -> operationId.equals(op.getValue().operationId)) + .findFirst(); + + if (operationEntry.isPresent()) { + method = HttpMethod.valueOf(operationEntry.get().getKey().toUpperCase(Locale.US)); + operation = operationEntry.get().getValue(); + pathItem = path; + break; + } + } + + if (operation == null) { + throw new CitrusRuntimeException("Unable to locate operation with id '%s' in OpenAPI specification %s".formatted(operationId, openApiSpec.getSpecUrl())); + } + + if (operation.parameters != null) { + operation.parameters.stream() + .filter(param -> "header".equals(param.in)) + .filter(param -> (param.required != null && param.required) || context.getVariables().containsKey(param.getName())) + .forEach(param -> httpMessage.setHeader(param.getName(), + OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), (OasSchema) param.schema, + OasModelHelper.getSchemaDefinitions(oasDocument), false, openApiSpec, context))); + + operation.parameters.stream() + .filter(param -> "query".equals(param.in)) + .filter(param -> (param.required != null && param.required) || context.getVariables().containsKey(param.getName())) + .forEach(param -> httpMessage.queryParam(param.getName(), + OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), (OasSchema) param.schema, context))); + } + + Optional body = OasModelHelper.getRequestBodySchema(oasDocument, operation); + body.ifPresent(oasSchema -> httpMessage.setPayload(OpenApiTestDataGenerator.createOutboundPayload(oasSchema, + OasModelHelper.getSchemaDefinitions(oasDocument), openApiSpec))); + + String randomizedPath = pathItem.getPath(); + if (operation.parameters != null) { + List pathParams = operation.parameters.stream() + .filter(p -> "path".equals(p.in)).toList(); + + for (OasParameter parameter : pathParams) { + String parameterValue; + if (context.getVariables().containsKey(parameter.getName())) { + parameterValue = "\\" + CitrusSettings.VARIABLE_PREFIX + parameter.getName() + CitrusSettings.VARIABLE_SUFFIX; + } else { + parameterValue = OpenApiTestDataGenerator.createRandomValueExpression((OasSchema) parameter.schema); + } + randomizedPath = Pattern.compile("\\{" + parameter.getName() + "}") + .matcher(randomizedPath) + .replaceAll(parameterValue); + } + } + + OasModelHelper.getRequestContentType(operation) + .ifPresent(contentType -> httpMessage.setHeader(HttpHeaders.CONTENT_TYPE, contentType)); + + httpMessage.path(randomizedPath); + httpMessage.method(method); + + return super.build(context, messageType); + } + } +} 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 new file mode 100644 index 0000000000..80d0a5c4c3 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientResponseActionBuilder.java @@ -0,0 +1,132 @@ +/* + * Copyright 2006-2015 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.actions; + +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +import io.apicurio.datamodels.openapi.models.OasDocument; +import io.apicurio.datamodels.openapi.models.OasOperation; +import io.apicurio.datamodels.openapi.models.OasPathItem; +import io.apicurio.datamodels.openapi.models.OasResponse; +import io.apicurio.datamodels.openapi.models.OasSchema; +import org.citrusframework.CitrusSettings; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.http.actions.HttpClientResponseActionBuilder; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.message.Message; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.OpenApiTestDataGenerator; +import org.citrusframework.openapi.model.OasModelHelper; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; + +/** + * @author Christoph Deppisch + * @since 4.1 + */ +public class OpenApiClientResponseActionBuilder extends HttpClientResponseActionBuilder { + + /** + * Default constructor initializes http response message builder. + */ + public OpenApiClientResponseActionBuilder(OpenApiSpecification openApiSpec, String operationId, String statusCode) { + this(new HttpMessage(), openApiSpec, operationId, statusCode); + } + + public OpenApiClientResponseActionBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, + String operationId, String statusCode) { + super(new OpenApiClientResponseMessageBuilder(httpMessage, openApiSpec, operationId, statusCode), httpMessage); + } + + private static class OpenApiClientResponseMessageBuilder extends HttpMessageBuilder { + + private final OpenApiSpecification openApiSpec; + private final String operationId; + private final String statusCode; + + private final HttpMessage httpMessage; + + public OpenApiClientResponseMessageBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, + String operationId, String statusCode) { + super(httpMessage); + this.openApiSpec = openApiSpec; + this.operationId = operationId; + this.statusCode = statusCode; + this.httpMessage = httpMessage; + } + + @Override + public Message build(TestContext context, String messageType) { + OasOperation operation = null; + OasDocument oasDocument = openApiSpec.getOpenApiDoc(context); + + for (OasPathItem path : OasModelHelper.getPathItems(oasDocument.paths)) { + Optional> operationEntry = OasModelHelper.getOperationMap(path).entrySet().stream() + .filter(op -> operationId.equals(op.getValue().operationId)) + .findFirst(); + + if (operationEntry.isPresent()) { + operation = operationEntry.get().getValue(); + break; + } + } + + if (operation == null) { + throw new CitrusRuntimeException("Unable to locate operation with id '%s' in OpenAPI specification %s".formatted(operationId, openApiSpec.getSpecUrl())); + } + + if (operation.responses != null) { + OasResponse response = Optional.ofNullable(operation.responses.getItem(statusCode)) + .orElse(operation.responses.default_); + + if (response != null) { + Map requiredHeaders = OasModelHelper.getRequiredHeaders(response); + for (Map.Entry header : requiredHeaders.entrySet()) { + httpMessage.setHeader(header.getKey(), OpenApiTestDataGenerator.createValidationExpression(header.getKey(), header.getValue(), + OasModelHelper.getSchemaDefinitions(oasDocument), false, openApiSpec, context)); + } + + Map headers = OasModelHelper.getHeaders(response); + for (Map.Entry header : headers.entrySet()) { + if (!requiredHeaders.containsKey(header.getKey()) && context.getVariables().containsKey(header.getKey())) { + httpMessage.setHeader(header.getKey(), CitrusSettings.VARIABLE_PREFIX + header.getKey() + CitrusSettings.VARIABLE_SUFFIX); + } + } + + Optional responseSchema = OasModelHelper.getSchema(response); + responseSchema.ifPresent(oasSchema -> httpMessage.setPayload(OpenApiTestDataGenerator.createInboundPayload(oasSchema, OasModelHelper.getSchemaDefinitions(oasDocument), openApiSpec))); + } + } + + OasModelHelper.getResponseContentType(oasDocument, operation) + .ifPresent(contentType -> httpMessage.setHeader(HttpHeaders.CONTENT_TYPE, contentType)); + + if (Pattern.compile("[0-9]+").matcher(statusCode).matches()) { + httpMessage.status(HttpStatus.valueOf(Integer.parseInt(statusCode))); + } else { + httpMessage.status(HttpStatus.OK); + } + + return super.build(context, messageType); + } + } + +} 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 new file mode 100644 index 0000000000..16b098875f --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilder.java @@ -0,0 +1,148 @@ +/* + * Copyright 2006-2015 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.actions; + +import org.citrusframework.TestAction; +import org.citrusframework.TestActionBuilder; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.spi.ReferenceResolver; +import org.citrusframework.spi.ReferenceResolverAware; +import org.citrusframework.util.ObjectHelper; +import org.springframework.http.HttpStatus; + +/** + * Action executes http server operations such as receiving requests and sending response messages. + * + * @author Christoph Deppisch + * @since 4.1 + */ +public class OpenApiServerActionBuilder implements TestActionBuilder.DelegatingTestActionBuilder, ReferenceResolverAware { + + private final OpenApiSpecification specification; + + /** Bean reference resolver */ + private ReferenceResolver referenceResolver; + + /** Target http client instance */ + private Endpoint httpServer; + private String httpServerUri; + + private TestActionBuilder delegate; + + /** + * Default constructor. + */ + public OpenApiServerActionBuilder(Endpoint httpServer, OpenApiSpecification specification) { + this.httpServer = httpServer; + this.specification = specification; + } + + /** + * Default constructor. + */ + public OpenApiServerActionBuilder(String httpServerUri, OpenApiSpecification specification) { + this.httpServerUri = httpServerUri; + this.specification = specification; + } + + /** + * Receive Http requests as server. + */ + public OpenApiServerRequestActionBuilder receive(String operationId) { + OpenApiServerRequestActionBuilder builder = new OpenApiServerRequestActionBuilder(specification, operationId); + if (httpServer != null) { + builder.endpoint(httpServer); + } else { + builder.endpoint(httpServerUri); + } + + builder.name("openapi:receive-request"); + builder.withReferenceResolver(referenceResolver); + + this.delegate = builder; + return builder; + } + + /** + * Sends Http response messages as server. + * Uses default Http status 200 OK. + */ + public OpenApiServerResponseActionBuilder send(String operationId) { + return send(operationId, HttpStatus.OK); + } + + /** + * Send Http response messages as server to client. + */ + public OpenApiServerResponseActionBuilder send(String operationId, HttpStatus status) { + return send(operationId, String.valueOf(status.value())); + } + + /** + * Send Http response messages as server to client. + */ + public OpenApiServerResponseActionBuilder send(String operationId, String statusCode) { + OpenApiServerResponseActionBuilder builder = new OpenApiServerResponseActionBuilder(specification, operationId, statusCode); + if (httpServer != null) { + builder.endpoint(httpServer); + } else { + builder.endpoint(httpServerUri); + } + + builder.name("openapi:send-response"); + builder.withReferenceResolver(referenceResolver); + + this.delegate = builder; + return builder; + } + + /** + * Sets the Spring bean application context. + * @param referenceResolver + */ + public OpenApiServerActionBuilder withReferenceResolver(ReferenceResolver referenceResolver) { + this.referenceResolver = referenceResolver; + return this; + } + + @Override + public TestAction build() { + ObjectHelper.assertNotNull(delegate, "Missing delegate action to build"); + return delegate.build(); + } + + @Override + public TestActionBuilder getDelegate() { + return delegate; + } + + /** + * Specifies the referenceResolver. + * @param referenceResolver + */ + @Override + public void setReferenceResolver(ReferenceResolver referenceResolver) { + if (referenceResolver == null) { + this.referenceResolver = referenceResolver; + + if (delegate instanceof ReferenceResolverAware) { + ((ReferenceResolverAware) delegate).setReferenceResolver(referenceResolver); + } + } + } +} 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 new file mode 100644 index 0000000000..c2749e743b --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerRequestActionBuilder.java @@ -0,0 +1,150 @@ +/* + * Copyright 2006-2015 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.actions; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +import io.apicurio.datamodels.openapi.models.OasDocument; +import io.apicurio.datamodels.openapi.models.OasOperation; +import io.apicurio.datamodels.openapi.models.OasParameter; +import io.apicurio.datamodels.openapi.models.OasPathItem; +import io.apicurio.datamodels.openapi.models.OasSchema; +import org.citrusframework.CitrusSettings; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.http.actions.HttpServerRequestActionBuilder; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.message.Message; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.OpenApiTestDataGenerator; +import org.citrusframework.openapi.model.OasModelHelper; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * @author Christoph Deppisch + * @since 4.1 + */ +public class OpenApiServerRequestActionBuilder extends HttpServerRequestActionBuilder { + + /** + * Default constructor initializes http request message builder. + */ + public OpenApiServerRequestActionBuilder(OpenApiSpecification openApiSpec, String operationId) { + this(new HttpMessage(), openApiSpec, operationId); + } + + public OpenApiServerRequestActionBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, + String operationId) { + super(new OpenApiServerRequestMessageBuilder(httpMessage, openApiSpec, operationId), httpMessage); + } + + private static class OpenApiServerRequestMessageBuilder extends HttpMessageBuilder { + + private final OpenApiSpecification openApiSpec; + private final String operationId; + + private final HttpMessage httpMessage; + + public OpenApiServerRequestMessageBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, + String operationId) { + super(httpMessage); + this.openApiSpec = openApiSpec; + this.operationId = operationId; + this.httpMessage = httpMessage; + } + + @Override + public Message build(TestContext context, String messageType) { + OasDocument oasDocument = openApiSpec.getOpenApiDoc(context); + OasOperation operation = null; + OasPathItem pathItem = null; + HttpMethod method = null; + + for (OasPathItem path : OasModelHelper.getPathItems(oasDocument.paths)) { + Optional> operationEntry = OasModelHelper.getOperationMap(path).entrySet().stream() + .filter(op -> operationId.equals(op.getValue().operationId)) + .findFirst(); + + if (operationEntry.isPresent()) { + method = HttpMethod.valueOf(operationEntry.get().getKey().toUpperCase(Locale.US)); + operation = operationEntry.get().getValue(); + pathItem = path; + break; + } + } + + if (operation == null) { + throw new CitrusRuntimeException("Unable to locate operation with id '%s' in OpenAPI specification %s".formatted(operationId, openApiSpec.getSpecUrl())); + } + + if (operation.parameters != null) { + operation.parameters.stream() + .filter(param -> "header".equals(param.in)) + .filter(param -> (param.required != null && param.required) || context.getVariables().containsKey(param.getName())) + .forEach(param -> httpMessage.setHeader(param.getName(), + OpenApiTestDataGenerator.createValidationExpression(param.getName(), (OasSchema) param.schema, + OasModelHelper.getSchemaDefinitions(oasDocument), false, openApiSpec, context))); + + operation.parameters.stream() + .filter(param -> "query".equals(param.in)) + .filter(param -> (param.required != null && param.required) || context.getVariables().containsKey(param.getName())) + .forEach(param -> httpMessage.queryParam(param.getName(), + OpenApiTestDataGenerator.createValidationExpression(param.getName(), (OasSchema) param.schema, + OasModelHelper.getSchemaDefinitions(oasDocument), false, openApiSpec, context))); + } + + Optional body = OasModelHelper.getRequestBodySchema(oasDocument, operation); + body.ifPresent(oasSchema -> httpMessage.setPayload(OpenApiTestDataGenerator.createInboundPayload(oasSchema, OasModelHelper.getSchemaDefinitions(oasDocument), openApiSpec))); + + String randomizedPath = OasModelHelper.getBasePath(oasDocument) + pathItem.getPath(); + randomizedPath = randomizedPath.replaceAll("//", "/"); + + if (operation.parameters != null) { + List pathParams = operation.parameters.stream() + .filter(p -> "path".equals(p.in)).toList(); + + for (OasParameter parameter : pathParams) { + String parameterValue; + if (context.getVariables().containsKey(parameter.getName())) { + parameterValue = "\\" + CitrusSettings.VARIABLE_PREFIX + parameter.getName() + CitrusSettings.VARIABLE_SUFFIX; + } else { + parameterValue = OpenApiTestDataGenerator.createValidationExpression((OasSchema) parameter.schema, + OasModelHelper.getSchemaDefinitions(oasDocument), false, openApiSpec); + } + randomizedPath = Pattern.compile("\\{" + parameter.getName() + "}") + .matcher(randomizedPath) + .replaceAll(parameterValue); + } + } + + OasModelHelper.getRequestContentType(operation) + .ifPresent(contentType -> httpMessage.setHeader(HttpHeaders.CONTENT_TYPE, String.format("@startsWith(%s)@", contentType))); + + httpMessage.path(randomizedPath); + httpMessage.method(method); + + return super.build(context, messageType); + } + } + +} 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 new file mode 100644 index 0000000000..c332862003 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerResponseActionBuilder.java @@ -0,0 +1,135 @@ +/* + * Copyright 2006-2015 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.actions; + +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +import io.apicurio.datamodels.openapi.models.OasDocument; +import io.apicurio.datamodels.openapi.models.OasOperation; +import io.apicurio.datamodels.openapi.models.OasPathItem; +import io.apicurio.datamodels.openapi.models.OasResponse; +import io.apicurio.datamodels.openapi.models.OasSchema; +import org.citrusframework.CitrusSettings; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.http.actions.HttpServerResponseActionBuilder; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.message.Message; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.OpenApiTestDataGenerator; +import org.citrusframework.openapi.model.OasModelHelper; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; + +/** + * @author Christoph Deppisch + * @since 4.1 + */ +public class OpenApiServerResponseActionBuilder extends HttpServerResponseActionBuilder { + + /** + * Default constructor initializes http response message builder. + */ + public OpenApiServerResponseActionBuilder(OpenApiSpecification openApiSpec, String operationId, String statusCode) { + this(new HttpMessage(), openApiSpec, operationId, statusCode); + } + + public OpenApiServerResponseActionBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, + String operationId, String statusCode) { + super(new OpenApiServerResponseMessageBuilder(httpMessage, openApiSpec, operationId, statusCode), httpMessage); + } + + private static class OpenApiServerResponseMessageBuilder extends HttpMessageBuilder { + + private final OpenApiSpecification openApiSpec; + private final String operationId; + private final String statusCode; + + private final HttpMessage httpMessage; + + public OpenApiServerResponseMessageBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, + String operationId, String statusCode) { + super(httpMessage); + this.openApiSpec = openApiSpec; + this.operationId = operationId; + this.statusCode = statusCode; + this.httpMessage = httpMessage; + } + + @Override + public Message build(TestContext context, String messageType) { + OasOperation operation = null; + OasDocument oasDocument = openApiSpec.getOpenApiDoc(context); + + for (OasPathItem path : OasModelHelper.getPathItems(oasDocument.paths)) { + Optional> operationEntry = OasModelHelper.getOperationMap(path).entrySet().stream() + .filter(op -> operationId.equals(op.getValue().operationId)) + .findFirst(); + + if (operationEntry.isPresent()) { + operation = operationEntry.get().getValue(); + break; + } + } + + if (operation == null) { + throw new CitrusRuntimeException(("Unable to locate operation with id '%s' " + + "in OpenAPI specification %s").formatted(operationId, openApiSpec.getSpecUrl())); + } + + if (operation.responses != null) { + OasResponse response = Optional.ofNullable(operation.responses.getItem(statusCode)) + .orElse(operation.responses.default_); + + if (response != null) { + Map requiredHeaders = OasModelHelper.getRequiredHeaders(response); + for (Map.Entry header : requiredHeaders.entrySet()) { + httpMessage.setHeader(header.getKey(), + OpenApiTestDataGenerator.createRandomValueExpression(header.getKey(), header.getValue(), + OasModelHelper.getSchemaDefinitions(oasDocument), false, openApiSpec, context)); + } + + Map headers = OasModelHelper.getHeaders(response); + for (Map.Entry header : headers.entrySet()) { + if (!requiredHeaders.containsKey(header.getKey()) && context.getVariables().containsKey(header.getKey())) { + httpMessage.setHeader(header.getKey(), CitrusSettings.VARIABLE_PREFIX + header.getKey() + CitrusSettings.VARIABLE_SUFFIX); + } + } + + Optional responseSchema = OasModelHelper.getSchema(response); + responseSchema.ifPresent(oasSchema -> httpMessage.setPayload(OpenApiTestDataGenerator.createOutboundPayload(oasSchema, + OasModelHelper.getSchemaDefinitions(oasDocument), openApiSpec))); + } + } + + OasModelHelper.getResponseContentType(oasDocument, operation) + .ifPresent(contentType -> httpMessage.setHeader(HttpHeaders.CONTENT_TYPE, contentType)); + + if (Pattern.compile("[0-9]+").matcher(statusCode).matches()) { + httpMessage.status(HttpStatus.valueOf(Integer.parseInt(statusCode))); + } else { + httpMessage.status(HttpStatus.OK); + } + + return super.build(context, messageType); + } + } + +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java new file mode 100644 index 0000000000..7aad4dcfa7 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java @@ -0,0 +1,266 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.model; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; + +import io.apicurio.datamodels.openapi.models.OasDocument; +import io.apicurio.datamodels.openapi.models.OasOperation; +import io.apicurio.datamodels.openapi.models.OasPathItem; +import io.apicurio.datamodels.openapi.models.OasPaths; +import io.apicurio.datamodels.openapi.models.OasResponse; +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v2.models.Oas20Document; +import io.apicurio.datamodels.openapi.v2.models.Oas20Operation; +import io.apicurio.datamodels.openapi.v2.models.Oas20Response; +import io.apicurio.datamodels.openapi.v3.models.Oas30Document; +import io.apicurio.datamodels.openapi.v3.models.Oas30Operation; +import io.apicurio.datamodels.openapi.v3.models.Oas30Response; +import org.citrusframework.openapi.model.v2.Oas20ModelHelper; +import org.citrusframework.openapi.model.v3.Oas30ModelHelper; + +/** + * @author Christoph Deppisch + */ +public final class OasModelHelper { + + private OasModelHelper() { + // utility class + } + + /** + * Determines if given schema is of type object. + * @param schema to check + * @return true if given schema is an object. + */ + public static boolean isObjectType(OasSchema schema) { + return "object".equals(schema.type); + } + + /** + * Determines if given schema is of type array. + * @param schema to check + * @return true if given schema is an array. + */ + public static boolean isArrayType(OasSchema schema) { + return "array".equals(schema.type); + } + + /** + * Determines if given schema has a reference to another schema object. + * @param schema to check + * @return true if given schema has a reference. + */ + public static boolean isReferenceType(OasSchema schema) { + return schema.$ref != null; + } + + public static String getHost(OasDocument openApiDoc) { + return delegate(openApiDoc, Oas20ModelHelper::getHost, Oas30ModelHelper::getHost); + } + + public static List getSchemes(OasDocument openApiDoc) { + return delegate(openApiDoc, Oas20ModelHelper::getSchemes, Oas30ModelHelper::getSchemes); + } + + public static String getBasePath(OasDocument openApiDoc) { + return delegate(openApiDoc, Oas20ModelHelper::getBasePath, Oas30ModelHelper::getBasePath); + } + + public static Map getSchemaDefinitions(OasDocument openApiDoc) { + return delegate(openApiDoc, Oas20ModelHelper::getSchemaDefinitions, Oas30ModelHelper::getSchemaDefinitions); + } + + /** + * Iterate through list of generic path items and collect path items of given type. + * @param paths given path items. + * @return typed list of path items. + */ + public static List getPathItems(OasPaths paths) { + if (paths == null) { + return Collections.emptyList(); + } + + return paths.getItems(); + } + + /** + * Construct map of all specified operations for given path item. Only non-null operations are added to the + * map where the key is the http method name. + * @param pathItem path holding operations. + * @return map of operations on the given path where Http method name is the key. + */ + public static Map getOperationMap(OasPathItem pathItem) { + Map operations = new LinkedHashMap<>(); + + if (pathItem.get != null) { + operations.put("get", pathItem.get); + } + + if (pathItem.put != null) { + operations.put("put", pathItem.put); + } + + if (pathItem.post != null) { + operations.put("post", pathItem.post); + } + + if (pathItem.delete != null) { + operations.put("delete", pathItem.delete); + } + + if (pathItem.options != null) { + operations.put("options", pathItem.options); + } + + if (pathItem.head != null) { + operations.put("head", pathItem.head); + } + + if (pathItem.patch != null) { + operations.put("patch", pathItem.patch); + } + + return operations; + } + + /** + * Get pure name from reference path. Usually reference definitions start with '#/definitions/' for OpenAPI 2.x and + * '#/components/schemas/' for OpenAPI 3.x and this method removes the basic reference path part and just returns the + * reference object name. + * @param reference path expression. + * @return the name of the reference object. + */ + public static String getReferenceName(String reference) { + if (reference != null) { + return reference.replaceAll("^.*/", ""); + } + + return null; + } + + public static Optional getSchema(OasResponse response) { + return delegate(response, Oas20ModelHelper::getSchema, Oas30ModelHelper::getSchema); + } + + public static Map getRequiredHeaders(OasResponse response) { + return delegate(response, Oas20ModelHelper::getHeaders, Oas30ModelHelper::getRequiredHeaders); + } + + public static Map getHeaders(OasResponse response) { + return delegate(response, Oas20ModelHelper::getHeaders, Oas30ModelHelper::getHeaders); + } + + public static Optional getRequestContentType(OasOperation operation) { + return delegate(operation, Oas20ModelHelper::getRequestContentType, Oas30ModelHelper::getRequestContentType); + } + + public static Optional getRequestBodySchema(OasDocument openApiDoc, OasOperation operation) { + return delegate(openApiDoc, operation, Oas20ModelHelper::getRequestBodySchema, Oas30ModelHelper::getRequestBodySchema); + } + + public static Optional getResponseContentType(OasDocument openApiDoc, OasOperation operation) { + return delegate(openApiDoc, operation, Oas20ModelHelper::getResponseContentType, Oas30ModelHelper::getResponseContentType); + } + + /** + * Delegate method to version specific model helpers for Open API v2 or v3. + * @param openApiDoc the open api document either v2 or v3 + * @param oas20Function function to apply in case of v2 + * @param oas30Function function to apply in case of v3 + * @param generic return value + * @return + */ + private static T delegate(OasDocument openApiDoc, Function oas20Function, Function oas30Function) { + if (isOas20(openApiDoc)) { + return oas20Function.apply((Oas20Document) openApiDoc); + } else if (isOas30(openApiDoc)) { + return oas30Function.apply((Oas30Document) openApiDoc); + } + + throw new IllegalArgumentException(String.format("Unsupported Open API document type: %s", openApiDoc.getClass())); + } + + /** + * Delegate method to version specific model helpers for Open API v2 or v3. + * @param response + * @param oas20Function function to apply in case of v2 + * @param oas30Function function to apply in case of v3 + * @param generic return value + * @return + */ + private static T delegate(OasResponse response, Function oas20Function, Function oas30Function) { + if (response instanceof Oas20Response) { + return oas20Function.apply((Oas20Response) response); + } else if (response instanceof Oas30Response) { + return oas30Function.apply((Oas30Response) response); + } + + throw new IllegalArgumentException(String.format("Unsupported operation response type: %s", response.getClass())); + } + + /** + * Delegate method to version specific model helpers for Open API v2 or v3. + * @param operation + * @param oas20Function function to apply in case of v2 + * @param oas30Function function to apply in case of v3 + * @param generic return value + * @return + */ + private static T delegate(OasOperation operation, Function oas20Function, Function oas30Function) { + if (operation instanceof Oas20Operation) { + return oas20Function.apply((Oas20Operation) operation); + } else if (operation instanceof Oas30Operation) { + return oas30Function.apply((Oas30Operation) operation); + } + + throw new IllegalArgumentException(String.format("Unsupported operation type: %s", operation.getClass())); + } + + /** + * Delegate method to version specific model helpers for Open API v2 or v3. + * @param operation + * @param oas20Function function to apply in case of v2 + * @param oas30Function function to apply in case of v3 + * @param generic return value + * @return + */ + private static T delegate(OasDocument openApiDoc, OasOperation operation, BiFunction oas20Function, BiFunction oas30Function) { + if (isOas20(openApiDoc)) { + return oas20Function.apply((Oas20Document) openApiDoc, (Oas20Operation) operation); + } else if (isOas30(openApiDoc)) { + return oas30Function.apply((Oas30Document) openApiDoc, (Oas30Operation) operation); + } + + throw new IllegalArgumentException(String.format("Unsupported Open API document type: %s", openApiDoc.getClass())); + } + + private static boolean isOas30(OasDocument openApiDoc) { + return OpenApiVersion.fromDocumentType(openApiDoc).equals(OpenApiVersion.V3); + } + + private static boolean isOas20(OasDocument openApiDoc) { + return OpenApiVersion.fromDocumentType(openApiDoc).equals(OpenApiVersion.V2); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OpenApiVersion.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OpenApiVersion.java new file mode 100644 index 0000000000..d9958fe5b4 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OpenApiVersion.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.model; + +import java.util.Arrays; + +import io.apicurio.datamodels.openapi.models.OasDocument; +import io.apicurio.datamodels.openapi.v2.models.Oas20Document; +import io.apicurio.datamodels.openapi.v3.models.Oas30Document; + +/** + * List of supported OpenAPI specification versions and their corresponding model document types. + */ +public enum OpenApiVersion { + V2("2.0", Oas20Document.class), + V3("3.0", Oas30Document.class); + + private final Class documentType; + + OpenApiVersion(String majorVersion, Class documentType) { + this.documentType = documentType; + } + + static OpenApiVersion fromDocumentType(OasDocument model) { + return Arrays.stream(values()) + .filter(version -> version.documentType.isInstance(model)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unable get OpenAPI version from given document type")); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/v2/Oas20ModelHelper.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/v2/Oas20ModelHelper.java new file mode 100644 index 0000000000..9f11f0d5b6 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/v2/Oas20ModelHelper.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.model.v2; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import io.apicurio.datamodels.openapi.models.OasHeader; +import io.apicurio.datamodels.openapi.models.OasParameter; +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v2.models.Oas20Document; +import io.apicurio.datamodels.openapi.v2.models.Oas20Header; +import io.apicurio.datamodels.openapi.v2.models.Oas20Operation; +import io.apicurio.datamodels.openapi.v2.models.Oas20Response; +import io.apicurio.datamodels.openapi.v2.models.Oas20Schema; +import io.apicurio.datamodels.openapi.v2.models.Oas20SchemaDefinition; + +/** + * @author Christoph Deppisch + */ +public final class Oas20ModelHelper { + + private Oas20ModelHelper() { + // utility class + } + + public static String getHost(Oas20Document openApiDoc) { + return openApiDoc.host; + } + + public static List getSchemes(Oas20Document openApiDoc) { + return openApiDoc.schemes; + } + + public static String getBasePath(Oas20Document openApiDoc) { + return Optional.ofNullable(openApiDoc.basePath) + .map(basePath -> basePath.startsWith("/") ? basePath : "/" + basePath).orElse("/"); + } + + public static Map getSchemaDefinitions(Oas20Document openApiDoc) { + if (openApiDoc == null + || openApiDoc.definitions == null) { + return Collections.emptyMap(); + } + + return openApiDoc.definitions.getDefinitions().stream().collect(Collectors.toMap(Oas20SchemaDefinition::getName, definition -> definition)); + } + + public static Optional getSchema(Oas20Response response) { + return Optional.ofNullable(response.schema); + } + + public static Optional getRequestBodySchema(Oas20Document openApiDoc, Oas20Operation operation) { + if (operation.parameters == null) { + return Optional.empty(); + } + + final List operationParameters = operation.parameters; + + Optional body = operationParameters.stream() + .filter(p -> "body".equals(p.in) && p.schema != null) + .findFirst(); + + return body.map(oasParameter -> (OasSchema) oasParameter.schema); + } + + public static Optional getRequestContentType(Oas20Operation operation) { + if (operation.consumes != null) { + return Optional.of(operation.consumes.get(0)); + } + + return Optional.empty(); + } + + public static Optional getResponseContentType(Oas20Document openApiDoc, Oas20Operation operation) { + if (operation.produces != null) { + return Optional.of(operation.produces.get(0)); + } + + return Optional.empty(); + } + + public static Map getHeaders(Oas20Response response) { + if (response.headers == null) { + return Collections.emptyMap(); + } + + return response.headers.getHeaders().stream() + .collect(Collectors.toMap(OasHeader::getName, Oas20ModelHelper::getHeaderSchema)); + } + + private static OasSchema getHeaderSchema(Oas20Header header) { + Oas20Schema schema = new Oas20Schema(); + schema.title = header.getName(); + schema.type = header.type; + schema.format = header.format; + schema.items = header.items; + schema.multipleOf = header.multipleOf; + + schema.default_ = header.default_; + schema.enum_ = header.enum_; + + schema.pattern = header.pattern; + schema.description = header.description; + schema.uniqueItems = header.uniqueItems; + + schema.maximum = header.maximum; + schema.maxItems = header.maxItems; + schema.maxLength = header.maxLength; + schema.exclusiveMaximum = header.exclusiveMaximum; + + schema.minimum = header.minimum; + schema.minItems = header.minItems; + schema.minLength = header.minLength; + schema.exclusiveMinimum = header.exclusiveMinimum; + return schema; + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/v3/Oas30ModelHelper.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/v3/Oas30ModelHelper.java new file mode 100644 index 0000000000..c8b74f3236 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/v3/Oas30ModelHelper.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.model.v3; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import io.apicurio.datamodels.core.models.common.Server; +import io.apicurio.datamodels.core.models.common.ServerVariable; +import io.apicurio.datamodels.openapi.models.OasResponse; +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Document; +import io.apicurio.datamodels.openapi.v3.models.Oas30MediaType; +import io.apicurio.datamodels.openapi.v3.models.Oas30Operation; +import io.apicurio.datamodels.openapi.v3.models.Oas30RequestBody; +import io.apicurio.datamodels.openapi.v3.models.Oas30Response; +import org.citrusframework.openapi.model.OasModelHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Christoph Deppisch + */ +public final class Oas30ModelHelper { + + /** Logger */ + private static final Logger LOG = LoggerFactory.getLogger(Oas30ModelHelper.class); + + private Oas30ModelHelper() { + // utility class + } + + public static String getHost(Oas30Document openApiDoc) { + if (openApiDoc.servers == null || openApiDoc.servers.isEmpty()) { + return "localhost"; + } + + String serverUrl = resolveUrl(openApiDoc.servers.get(0)); + if (serverUrl.startsWith("http")) { + try { + return new URL(serverUrl).getHost(); + } catch (MalformedURLException e) { + throw new IllegalStateException(String.format("Unable to determine base path from server URL: %s", serverUrl)); + } + } + + return "localhost"; + } + + public static List getSchemes(Oas30Document openApiDoc) { + if (openApiDoc.servers == null || openApiDoc.servers.isEmpty()) { + return Collections.emptyList(); + } + + return openApiDoc.servers.stream() + .map(Oas30ModelHelper::resolveUrl) + .map(serverUrl -> { + try { + return new URL(serverUrl).getProtocol(); + } catch (MalformedURLException e) { + LOG.warn(String.format("Unable to determine base path from server URL: %s", serverUrl)); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + public static String getBasePath(Oas30Document openApiDoc) { + if (openApiDoc.servers == null || openApiDoc.servers.isEmpty()) { + return "/"; + } + + Server server = openApiDoc.servers.get(0); + String basePath; + + String serverUrl = resolveUrl(server); + if (serverUrl.startsWith("http")) { + try { + basePath = new URL(serverUrl).getPath(); + } catch (MalformedURLException e) { + throw new IllegalStateException(String.format("Unable to determine base path from server URL: %s", serverUrl)); + } + } else { + basePath = serverUrl; + } + + return basePath.startsWith("/") ? basePath : "/" + basePath; + } + + public static Map getSchemaDefinitions(Oas30Document openApiDoc) { + if (openApiDoc.components == null || openApiDoc.components.schemas == null) { + return Collections.emptyMap(); + } + + return openApiDoc.components.schemas.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (OasSchema) entry.getValue())); + } + + public static Optional getSchema(Oas30Response response) { + Map content = response.content; + if (content == null) { + return Optional.empty(); + } + + return content.entrySet() + .stream() + .filter(entry -> !isFormDataMediaType(entry.getKey())) + .filter(entry -> entry.getValue().schema != null) + .map(entry -> (OasSchema) entry.getValue().schema) + .findFirst(); + } + + public static Optional getRequestBodySchema(Oas30Document openApiDoc, Oas30Operation operation) { + if (operation.requestBody == null) { + return Optional.empty(); + } + + Oas30RequestBody bodyToUse = operation.requestBody; + + if (openApiDoc.components != null + && openApiDoc.components.requestBodies != null + && bodyToUse.$ref != null) { + bodyToUse = openApiDoc.components.requestBodies.get(OasModelHelper.getReferenceName(bodyToUse.$ref)); + } + + if (bodyToUse.content == null) { + return Optional.empty(); + } + + return bodyToUse.content.entrySet() + .stream() + .filter(entry -> !isFormDataMediaType(entry.getKey())) + .filter(entry -> entry.getValue().schema != null) + .findFirst() + .map(Map.Entry::getValue) + .map(oas30MediaType -> oas30MediaType.schema); + } + + public static Optional getRequestContentType(Oas30Operation operation) { + if (operation.requestBody == null || operation.requestBody.content == null) { + return Optional.empty(); + } + + return operation.requestBody.content.entrySet() + .stream() + .filter(entry -> entry.getValue().schema != null) + .map(Map.Entry::getKey) + .findFirst(); + } + + public static Optional getResponseContentType(Oas30Document openApiDoc, Oas30Operation operation) { + if (operation.responses == null) { + return Optional.empty(); + } + + List responses = new ArrayList<>(); + + for (OasResponse response : operation.responses.getResponses()) { + if (response.$ref != null) { + responses.add(openApiDoc.components.responses.get(OasModelHelper.getReferenceName(response.$ref))); + } else { + responses.add(response); + } + } + + // Pick the response object related to the first 2xx return code found + Optional response = responses.stream() + .filter(Oas30Response.class::isInstance) + .filter(r -> r.getStatusCode() != null && r.getStatusCode().startsWith("2")) + .map(Oas30Response.class::cast) + .filter(res -> Oas30ModelHelper.getSchema(res).isPresent()) + .findFirst(); + + // No 2xx response given so pick the first one no matter what status code + if (!response.isPresent()) { + response = responses.stream() + .filter(Oas30Response.class::isInstance) + .map(Oas30Response.class::cast) + .filter(res -> Oas30ModelHelper.getSchema(res).isPresent()) + .findFirst(); + } + + return response.flatMap(res -> res.content.entrySet() + .stream() + .filter(entry -> entry.getValue().schema != null) + .map(Map.Entry::getKey) + .findFirst()); + + } + + public static Map getRequiredHeaders(Oas30Response response) { + if (response.headers == null) { + return Collections.emptyMap(); + } + + return response.headers.entrySet() + .stream() + .filter(entry -> entry.getValue().required) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().schema)); + } + + public static Map getHeaders(Oas30Response response) { + if (response.headers == null) { + return Collections.emptyMap(); + } + + return response.headers.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().schema)); + } + + private static boolean isFormDataMediaType(String type) { + return Arrays.asList("application/x-www-form-urlencoded", "multipart/form-data").contains(type); + } + + /** + * Resolve given server url and replace variable placeholders if any with default variable values. Open API 3.x + * supports variables with placeholders in form {variable_name} (e.g. "http://{hostname}:{port}/api/v1"). + * @param server the server holding a URL with maybe variable placeholders. + * @return the server URL with all placeholders resolved or "/" by default. + */ + private static String resolveUrl(Server server) { + String url = Optional.ofNullable(server.url).orElse("/"); + if (server.variables != null) { + for (Map.Entry variable: server.variables.entrySet()) { + String defaultValue = Optional.ofNullable(variable.getValue().default_).orElse(""); + url = url.replaceAll(String.format("\\{%s\\}", variable.getKey()), defaultValue); + } + } + + return url; + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/ObjectFactory.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/ObjectFactory.java new file mode 100644 index 0000000000..9c946542c2 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/ObjectFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.xml; + +import jakarta.xml.bind.annotation.XmlRegistry; + +/** + * This object contains factory methods for each + * Java content interface and Java element interface + * generated in the org.citrusframework.ftp.model package. + *

An ObjectFactory allows you to programatically + * construct new instances of the Java representation + * for XML content. The Java representation of XML + * content can consist of schema derived interfaces + * and classes representing the binding of schema + * type definitions, element declarations and model + * groups. Factory methods for each of these are + * provided in this class. + * + */ +@XmlRegistry +public class ObjectFactory { + + /** + * Create a new ObjectFactory that can be used to create new instances of schema derived classes for package: org.citrusframework.xml.actions + * + */ + public ObjectFactory() { + } + + /** + * Create an instance of {@link OpenApi } + * + */ + public OpenApi createOpenApi() { + return new OpenApi(); + } + +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/OpenApi.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/OpenApi.java new file mode 100644 index 0000000000..860a163925 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/OpenApi.java @@ -0,0 +1,608 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.xml; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlAttribute; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import jakarta.xml.bind.annotation.XmlType; +import org.citrusframework.TestAction; +import org.citrusframework.TestActionBuilder; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.resolver.EndpointUriResolver; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.http.actions.HttpServerResponseActionBuilder; +import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.openapi.actions.OpenApiActionBuilder; +import org.citrusframework.openapi.actions.OpenApiClientActionBuilder; +import org.citrusframework.openapi.actions.OpenApiClientRequestActionBuilder; +import org.citrusframework.openapi.actions.OpenApiClientResponseActionBuilder; +import org.citrusframework.openapi.actions.OpenApiServerActionBuilder; +import org.citrusframework.openapi.actions.OpenApiServerRequestActionBuilder; +import org.citrusframework.spi.ReferenceResolver; +import org.citrusframework.spi.ReferenceResolverAware; +import org.citrusframework.xml.actions.Message; +import org.citrusframework.xml.actions.Receive; +import org.citrusframework.xml.actions.Send; + +/** + * @author Christoph Deppisch + */ +@XmlRootElement(name = "openapi") +public class OpenApi implements TestActionBuilder, ReferenceResolverAware { + + private TestActionBuilder builder; + + private Receive receive; + private Send send; + + private String description; + private String actor; + + private ReferenceResolver referenceResolver; + + @XmlElement + public OpenApi setDescription(String value) { + this.description = value; + return this; + } + + @XmlAttribute(name = "actor") + public OpenApi setActor(String actor) { + this.actor = actor; + return this; + } + + @XmlAttribute(name = "specification", required = true) + public OpenApi setSpecification(String specification) { + builder = new OpenApiActionBuilder().specification(specification); + return this; + } + + @XmlAttribute(name = "client") + public OpenApi setHttpClient(String httpClient) { + builder = ((OpenApiActionBuilder) builder).client(httpClient); + return this; + } + + @XmlAttribute(name = "server") + public OpenApi setHttpServer(String httpServer) { + builder = ((OpenApiActionBuilder) builder).server(httpServer); + return this; + } + + @XmlElement(name = "send-request") + public OpenApi setSendRequest(ClientRequest request) { + OpenApiClientRequestActionBuilder requestBuilder = + asClientBuilder().send(request.getOperation()); + + requestBuilder.name("openapi:send-request"); + requestBuilder.description(description); + + send = new Send(requestBuilder) { + @Override + protected SendMessageAction doBuild() { + // do not build inside delegate. the actual build is called directly on the builder. + return null; + } + }; + + if (request.fork != null) { + send.setFork(request.fork); + } + + if (request.extract != null) { + send.setExtract(request.extract); + } + + if (request.uri != null) { + requestBuilder.message().header(EndpointUriResolver.ENDPOINT_URI_HEADER_NAME, request.uri); + } + + builder = requestBuilder; + return this; + } + + @XmlElement(name = "receive-response") + public OpenApi setReceiveResponse(ClientResponse response) { + OpenApiClientResponseActionBuilder responseBuilder = + asClientBuilder().receive(response.getOperation(), response.getStatus()); + + responseBuilder.name("openapi:receive-response"); + responseBuilder.description(description); + + receive = new Receive(responseBuilder) { + @Override + protected ReceiveMessageAction doBuild() { + // do not build inside delegate. the actual build is called directly on the builder. + return null; + } + }; + + if (response.timeout != null) { + receive.setTimeout(response.timeout); + } + + receive.setSelect(response.select); + receive.setValidator(response.validator); + receive.setValidators(response.validators); + receive.setHeaderValidator(response.headerValidator); + receive.setHeaderValidators(response.headerValidators); + + if (response.selector != null) { + receive.setSelector(response.selector); + } + + receive.setSelect(response.select); + + response.getValidates().forEach(receive.getValidates()::add); + + if (response.extract != null) { + receive.setExtract(response.extract); + } + + builder = responseBuilder; + return this; + } + + @XmlElement(name = "receive-request") + public OpenApi setReceiveRequest(ServerRequest request) { + OpenApiServerRequestActionBuilder requestBuilder = + asServerBuilder().receive(request.getOperation()); + + requestBuilder.name("openapi:receive-request"); + requestBuilder.description(description); + + receive = new Receive(requestBuilder) { + @Override + protected ReceiveMessageAction doBuild() { + // do not build inside delegate. the actual build is called directly on the builder. + return null; + } + }; + + if (request.selector != null) { + receive.setSelector(request.selector); + } + + receive.setSelect(request.select); + receive.setValidator(request.validator); + receive.setValidators(request.validators); + receive.setHeaderValidator(request.headerValidator); + receive.setHeaderValidators(request.headerValidators); + + if (request.timeout != null) { + receive.setTimeout(request.timeout); + } + + request.getValidates().forEach(receive.getValidates()::add); + + if (request.extract != null) { + receive.setExtract(request.extract); + } + + builder = requestBuilder; + return this; + } + + @XmlElement(name = "send-response") + public OpenApi setSendResponse(ServerResponse response) { + HttpServerResponseActionBuilder responseBuilder = + asServerBuilder().send(response.getOperation(), response.getStatus()); + + responseBuilder.name("openapi:send-response"); + responseBuilder.description(description); + + send = new Send(responseBuilder) { + @Override + protected SendMessageAction doBuild() { + // do not build inside delegate. the actual build is called directly on the builder. + return null; + } + }; + + if (response.extract != null) { + send.setExtract(response.extract); + } + + responseBuilder.message().header(HttpMessageHeaders.HTTP_STATUS_CODE, response.getStatus()); + + builder = responseBuilder; + return this; + } + + @Override + public TestAction build() { + if (builder == null) { + throw new CitrusRuntimeException("Missing client or server Http action - please provide proper action details"); + } + + if (send != null) { + send.setReferenceResolver(referenceResolver); + send.setActor(actor); + send.build(); + } + + if (receive != null) { + receive.setReferenceResolver(referenceResolver); + receive.setActor(actor); + receive.build(); + } + + return builder.build(); + } + + @Override + public void setReferenceResolver(ReferenceResolver referenceResolver) { + this.referenceResolver = referenceResolver; + } + + /** + * Converts current builder to client builder. + * @return + */ + private OpenApiClientActionBuilder asClientBuilder() { + if (builder instanceof OpenApiClientActionBuilder clientBuilder) { + return clientBuilder; + } + + throw new CitrusRuntimeException(String.format("Failed to convert '%s' to openapi client action builder", + Optional.ofNullable(builder).map(Object::getClass).map(Class::getName).orElse("null"))); + } + + /** + * Converts current builder to server builder. + * @return + */ + private OpenApiServerActionBuilder asServerBuilder() { + if (builder instanceof OpenApiServerActionBuilder serverBuilder) { + return serverBuilder; + } + + throw new CitrusRuntimeException(String.format("Failed to convert '%s' to openapi server action builder", + Optional.ofNullable(builder).map(Object::getClass).map(Class::getName).orElse("null"))); + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "", propOrder = {}) + public static class ClientRequest { + @XmlAttribute(name = "operation", required = true) + protected String operation; + @XmlAttribute(name = "uri") + protected String uri; + @XmlAttribute(name = "fork") + protected Boolean fork; + + @XmlElement + protected Message.Extract extract; + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public Boolean getFork() { + return fork; + } + + public void setFork(Boolean fork) { + this.fork = fork; + } + + public Message.Extract getExtract() { + return extract; + } + + public void setExtract(Message.Extract extract) { + this.extract = extract; + } + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "", propOrder = {}) + public static class ServerRequest { + @XmlAttribute + protected Integer timeout; + + @XmlAttribute(name = "operation", required = true) + protected String operation; + + @XmlAttribute + protected String select; + + @XmlAttribute + protected String validator; + + @XmlAttribute + protected String validators; + + @XmlAttribute(name = "header-validator") + protected String headerValidator; + + @XmlAttribute(name = "header-validators") + protected String headerValidators; + + @XmlElement + protected Receive.Selector selector; + + @XmlElement(name = "validate") + protected List validates; + + @XmlElement + protected Message.Extract extract; + + public Integer getTimeout() { + return timeout; + } + + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getSelect() { + return select; + } + + public void setSelect(String select) { + this.select = select; + } + + public String getValidator() { + return validator; + } + + public void setValidator(String validator) { + this.validator = validator; + } + + public String getValidators() { + return validators; + } + + public void setValidators(String validators) { + this.validators = validators; + } + + public String getHeaderValidator() { + return headerValidator; + } + + public void setHeaderValidator(String headerValidator) { + this.headerValidator = headerValidator; + } + + public String getHeaderValidators() { + return headerValidators; + } + + public void setHeaderValidators(String headerValidators) { + this.headerValidators = headerValidators; + } + + public List getValidates() { + if (validates == null) { + validates = new ArrayList<>(); + } + + return validates; + } + + public void setValidates(List validates) { + this.validates = validates; + } + + public Message.Extract getExtract() { + return extract; + } + + public void setExtract(Message.Extract extract) { + this.extract = extract; + } + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "", propOrder = {}) + public static class ServerResponse { + @XmlAttribute(name = "operation", required = true) + protected String operation; + + @XmlAttribute + protected String status = "200"; + + @XmlElement + protected Message.Extract extract; + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Message.Extract getExtract() { + return extract; + } + + public void setExtract(Message.Extract extract) { + this.extract = extract; + } + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "", propOrder = {}) + public static class ClientResponse { + @XmlAttribute + protected Integer timeout; + + @XmlAttribute + protected String operation; + + @XmlAttribute + protected String status = "200"; + + @XmlAttribute + protected String select; + + @XmlAttribute + protected String validator; + + @XmlAttribute + protected String validators; + + @XmlAttribute(name = "header-validator") + protected String headerValidator; + + @XmlAttribute(name = "header-validators") + protected String headerValidators; + + @XmlElement + protected Receive.Selector selector; + + @XmlElement(name = "validate") + protected List validates; + + @XmlElement + protected Message.Extract extract; + + public Integer getTimeout() { + return timeout; + } + + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getSelect() { + return select; + } + + public void setSelect(String select) { + this.select = select; + } + + public Receive.Selector getSelector() { + return selector; + } + + public void setSelector(Receive.Selector selector) { + this.selector = selector; + } + + public String getValidator() { + return validator; + } + + public void setValidator(String validator) { + this.validator = validator; + } + + public String getValidators() { + return validators; + } + + public void setValidators(String validators) { + this.validators = validators; + } + + public String getHeaderValidator() { + return headerValidator; + } + + public void setHeaderValidator(String headerValidator) { + this.headerValidator = headerValidator; + } + + public String getHeaderValidators() { + return headerValidators; + } + + public void setHeaderValidators(String headerValidators) { + this.headerValidators = headerValidators; + } + + public List getValidates() { + if (validates == null) { + validates = new ArrayList<>(); + } + + return validates; + } + + public Message.Extract getExtract() { + return extract; + } + + public void setExtract(Message.Extract extract) { + this.extract = extract; + } + } + +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/package-info.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/package-info.java new file mode 100644 index 0000000000..28e5bc9430 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/xml/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +@jakarta.xml.bind.annotation.XmlSchema(namespace = "http://citrusframework.org/schema/xml/testcase", elementFormDefault = jakarta.xml.bind.annotation.XmlNsForm.QUALIFIED) +package org.citrusframework.openapi.xml; diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/yaml/OpenApi.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/yaml/OpenApi.java new file mode 100644 index 0000000000..0f930ff700 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/yaml/OpenApi.java @@ -0,0 +1,547 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.yaml; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.citrusframework.TestAction; +import org.citrusframework.TestActionBuilder; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.resolver.EndpointUriResolver; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.http.actions.HttpServerResponseActionBuilder; +import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.openapi.actions.OpenApiActionBuilder; +import org.citrusframework.openapi.actions.OpenApiClientActionBuilder; +import org.citrusframework.openapi.actions.OpenApiClientRequestActionBuilder; +import org.citrusframework.openapi.actions.OpenApiClientResponseActionBuilder; +import org.citrusframework.openapi.actions.OpenApiServerActionBuilder; +import org.citrusframework.openapi.actions.OpenApiServerRequestActionBuilder; +import org.citrusframework.spi.ReferenceResolver; +import org.citrusframework.spi.ReferenceResolverAware; +import org.citrusframework.yaml.actions.Receive; +import org.citrusframework.yaml.actions.Send; +import org.citrusframework.yaml.actions.Message; + +/** + * @author Christoph Deppisch + */ +public class OpenApi implements TestActionBuilder, ReferenceResolverAware { + + private TestActionBuilder builder; + + private Receive receive; + private Send send; + + private String description; + private String actor; + + private ReferenceResolver referenceResolver; + + public void setDescription(String value) { + this.description = value; + } + + public void setActor(String actor) { + this.actor = actor; + } + + public void setSpecification(String specification) { + builder = new OpenApiActionBuilder().specification(specification); + } + + public void setClient(String httpClient) { + builder = ((OpenApiActionBuilder) builder).client(httpClient); + } + + public void setServer(String httpServer) { + builder = ((OpenApiActionBuilder) builder).server(httpServer); + } + + public void setSendRequest(ClientRequest request) { + OpenApiClientRequestActionBuilder requestBuilder = + asClientBuilder().send(request.getOperation()); + + requestBuilder.name("openapi:send-request"); + requestBuilder.description(description); + + send = new Send(requestBuilder) { + @Override + protected SendMessageAction doBuild() { + // do not build inside delegate. the actual build is called directly on the builder. + return null; + } + }; + + if (request.fork != null) { + send.setFork(request.fork); + } + + if (request.extract != null) { + send.setExtract(request.extract); + } + + if (request.uri != null) { + requestBuilder.message().header(EndpointUriResolver.ENDPOINT_URI_HEADER_NAME, request.uri); + } + + builder = requestBuilder; + } + + public void setReceiveResponse(ClientResponse response) { + OpenApiClientResponseActionBuilder responseBuilder = + asClientBuilder().receive(response.getOperation(), response.getStatus()); + + responseBuilder.name("openapi:receive-response"); + responseBuilder.description(description); + + receive = new Receive(responseBuilder) { + @Override + protected ReceiveMessageAction doBuild() { + // do not build inside delegate. the actual build is called directly on the builder. + return null; + } + }; + + if (response.timeout != null) { + receive.setTimeout(response.timeout); + } + + receive.setSelect(response.select); + receive.setValidator(response.validator); + receive.setValidators(response.validators); + receive.setHeaderValidator(response.headerValidator); + receive.setHeaderValidators(response.headerValidators); + + if (response.selector != null) { + receive.setSelector(response.selector); + } + + receive.setSelect(response.select); + + response.getValidates().forEach(receive.getValidate()::add); + + if (response.extract != null) { + receive.setExtract(response.extract); + } + + builder = responseBuilder; + } + + public void setReceiveRequest(ServerRequest request) { + OpenApiServerRequestActionBuilder requestBuilder = + asServerBuilder().receive(request.getOperation()); + + requestBuilder.name("openapi:receive-request"); + requestBuilder.description(description); + + receive = new Receive(requestBuilder) { + @Override + protected ReceiveMessageAction doBuild() { + // do not build inside delegate. the actual build is called directly on the builder. + return null; + } + }; + + if (request.selector != null) { + receive.setSelector(request.selector); + } + + receive.setSelect(request.select); + receive.setValidator(request.validator); + receive.setValidators(request.validators); + receive.setHeaderValidator(request.headerValidator); + receive.setHeaderValidators(request.headerValidators); + + if (request.timeout != null) { + receive.setTimeout(request.timeout); + } + + request.getValidates().forEach(receive.getValidate()::add); + + if (request.extract != null) { + receive.setExtract(request.extract); + } + + builder = requestBuilder; + } + + public void setSendResponse(ServerResponse response) { + HttpServerResponseActionBuilder responseBuilder = + asServerBuilder().send(response.getOperation(), response.getStatus()); + + responseBuilder.name("openapi:send-response"); + responseBuilder.description(description); + + send = new Send(responseBuilder) { + @Override + protected SendMessageAction doBuild() { + // do not build inside delegate. the actual build is called directly on the builder. + return null; + } + }; + + if (response.extract != null) { + send.setExtract(response.extract); + } + + responseBuilder.message().header(HttpMessageHeaders.HTTP_STATUS_CODE, response.getStatus()); + + builder = responseBuilder; + } + + @Override + public TestAction build() { + if (builder == null) { + throw new CitrusRuntimeException("Missing client or server Http action - please provide proper action details"); + } + + if (send != null) { + send.setReferenceResolver(referenceResolver); + send.setActor(actor); + send.build(); + } + + if (receive != null) { + receive.setReferenceResolver(referenceResolver); + receive.setActor(actor); + receive.build(); + } + + return builder.build(); + } + + @Override + public void setReferenceResolver(ReferenceResolver referenceResolver) { + this.referenceResolver = referenceResolver; + } + + /** + * Converts current builder to client builder. + * @return + */ + private OpenApiClientActionBuilder asClientBuilder() { + if (builder instanceof OpenApiClientActionBuilder) { + return (OpenApiClientActionBuilder) builder; + } + + throw new CitrusRuntimeException(String.format("Failed to convert '%s' to openapi client action builder", + Optional.ofNullable(builder).map(Object::getClass).map(Class::getName).orElse("null"))); + } + + /** + * Converts current builder to server builder. + * @return + */ + private OpenApiServerActionBuilder asServerBuilder() { + if (builder instanceof OpenApiServerActionBuilder) { + return (OpenApiServerActionBuilder) builder; + } + + throw new CitrusRuntimeException(String.format("Failed to convert '%s' to openapi server action builder", + Optional.ofNullable(builder).map(Object::getClass).map(Class::getName).orElse("null"))); + } + + public static class ClientRequest { + protected String operation; + protected String uri; + protected Boolean fork; + + protected Message.Extract extract; + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public Boolean getFork() { + return fork; + } + + public void setFork(Boolean fork) { + this.fork = fork; + } + + public Message.Extract getExtract() { + return extract; + } + + public void setExtract(Message.Extract extract) { + this.extract = extract; + } + } + + public static class ServerRequest { + protected Integer timeout; + + protected String operation; + + protected String select; + + protected String validator; + + protected String validators; + + protected String headerValidator; + + protected String headerValidators; + + protected Receive.Selector selector; + + protected List validates; + + protected Message.Extract extract; + + public Integer getTimeout() { + return timeout; + } + + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getSelect() { + return select; + } + + public void setSelect(String select) { + this.select = select; + } + + public String getValidator() { + return validator; + } + + public void setValidator(String validator) { + this.validator = validator; + } + + public String getValidators() { + return validators; + } + + public void setValidators(String validators) { + this.validators = validators; + } + + public String getHeaderValidator() { + return headerValidator; + } + + public void setHeaderValidator(String headerValidator) { + this.headerValidator = headerValidator; + } + + public String getHeaderValidators() { + return headerValidators; + } + + public void setHeaderValidators(String headerValidators) { + this.headerValidators = headerValidators; + } + + public List getValidates() { + if (validates == null) { + validates = new ArrayList<>(); + } + + return validates; + } + + public void setValidates(List validates) { + this.validates = validates; + } + + public Message.Extract getExtract() { + return extract; + } + + public void setExtract(Message.Extract extract) { + this.extract = extract; + } + } + + public static class ServerResponse { + protected String operation; + + protected String status = "200"; + + protected Message.Extract extract; + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Message.Extract getExtract() { + return extract; + } + + public void setExtract(Message.Extract extract) { + this.extract = extract; + } + } + + public static class ClientResponse { + protected Integer timeout; + + protected String operation; + + protected String status = "200"; + + protected String select; + + protected String validator; + + protected String validators; + + protected String headerValidator; + + protected String headerValidators; + + protected Receive.Selector selector; + + protected List validates; + + protected Message.Extract extract; + + public Integer getTimeout() { + return timeout; + } + + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getSelect() { + return select; + } + + public void setSelect(String select) { + this.select = select; + } + + public Receive.Selector getSelector() { + return selector; + } + + public void setSelector(Receive.Selector selector) { + this.selector = selector; + } + + public String getValidator() { + return validator; + } + + public void setValidator(String validator) { + this.validator = validator; + } + + public String getValidators() { + return validators; + } + + public void setValidators(String validators) { + this.validators = validators; + } + + public String getHeaderValidator() { + return headerValidator; + } + + public void setHeaderValidator(String headerValidator) { + this.headerValidator = headerValidator; + } + + public String getHeaderValidators() { + return headerValidators; + } + + public void setHeaderValidators(String headerValidators) { + this.headerValidators = headerValidators; + } + + public List getValidates() { + if (validates == null) { + validates = new ArrayList<>(); + } + + return validates; + } + + public Message.Extract getExtract() { + return extract; + } + + public void setExtract(Message.Extract extract) { + this.extract = extract; + } + } + +} diff --git a/connectors/citrus-openapi/src/main/resources/META-INF/LICENSE.txt b/connectors/citrus-openapi/src/main/resources/META-INF/LICENSE.txt new file mode 100644 index 0000000000..0dfbfdbe76 --- /dev/null +++ b/connectors/citrus-openapi/src/main/resources/META-INF/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2006-2023 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. diff --git a/connectors/citrus-openapi/src/main/resources/META-INF/NOTICE.txt b/connectors/citrus-openapi/src/main/resources/META-INF/NOTICE.txt new file mode 100644 index 0000000000..ad3886b5ab --- /dev/null +++ b/connectors/citrus-openapi/src/main/resources/META-INF/NOTICE.txt @@ -0,0 +1,32 @@ + ======================================================================== + == NOTICE file corresponding to section 4 d of the Apache License, == + == Version 2.0, in this case for the Citrus distribution. == + ======================================================================== + + This product includes software developed by the Citrus + project (https://citrusframework.org). + + The end-user documentation included with a redistribution, if any, + must include the following acknowledgement: + + "This product includes software developed by the Citrus + Project (https://citrusframework.org)." + + Alternatively, this acknowledgement may appear in the software itself, + if and wherever such third-party acknowledgements normally appear. + + The names "Citrus" and "Citrus Framework" must not be used to endorse or + promote products derived from this software without prior written permission. + For written permission, please contact user@citrusframework.org. + + Copyright (C) 2006-2023 the original author or authors. + + Citrus is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. + + You should have received a copy of the Apache License Version 2.0 + along with Citrus. If not, see . + + dev@citrusframework.org + https://citrusframework.org diff --git a/connectors/citrus-openapi/src/main/resources/META-INF/citrus/action/builder/openapi b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/action/builder/openapi new file mode 100644 index 0000000000..4644db0b4d --- /dev/null +++ b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/action/builder/openapi @@ -0,0 +1 @@ +type=org.citrusframework.openapi.actions.OpenApiActionBuilder diff --git a/connectors/citrus-openapi/src/main/resources/META-INF/citrus/xml/builder/openapi b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/xml/builder/openapi new file mode 100644 index 0000000000..65d167e13a --- /dev/null +++ b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/xml/builder/openapi @@ -0,0 +1,2 @@ +type=org.citrusframework.openapi.xml.OpenApi +ns=http://citrusframework.org/schema/xml/testcase diff --git a/connectors/citrus-openapi/src/main/resources/META-INF/citrus/yaml/builder/openapi b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/yaml/builder/openapi new file mode 100644 index 0000000000..11386b8507 --- /dev/null +++ b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/yaml/builder/openapi @@ -0,0 +1 @@ +type=org.citrusframework.openapi.yaml.OpenApi diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiActionBuilderTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiActionBuilderTest.java new file mode 100644 index 0000000000..7a7fa7e7a4 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiActionBuilderTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.actions; + +import java.util.Map; + +import org.citrusframework.TestActionBuilder; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * @author Christoph Deppisch + */ +public class OpenApiActionBuilderTest { + + @Test + public void shouldLookupTestActionBuilder() { + Map> endpointBuilders = TestActionBuilder.lookup(); + Assert.assertTrue(endpointBuilders.containsKey("openapi")); + + Assert.assertTrue(TestActionBuilder.lookup("openapi").isPresent()); + Assert.assertEquals(TestActionBuilder.lookup("openapi").get().getClass(), OpenApiActionBuilder.class); + } + +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/AbstractGroovyActionDslTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/AbstractGroovyActionDslTest.java new file mode 100644 index 0000000000..c37dadd746 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/AbstractGroovyActionDslTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.groovy; + +import org.citrusframework.Citrus; +import org.citrusframework.CitrusContext; +import org.citrusframework.CitrusInstanceManager; +import org.citrusframework.DefaultTestCaseRunner; +import org.citrusframework.annotations.CitrusAnnotations; +import org.citrusframework.context.StaticTestContextFactory; +import org.citrusframework.context.TestContext; +import org.citrusframework.groovy.GroovyTestLoader; +import org.citrusframework.testng.AbstractTestNGUnitTest; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.BeforeClass; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +/** + * @author Christoph Deppisch + */ +public class AbstractGroovyActionDslTest extends AbstractTestNGUnitTest { + + protected Citrus citrus; + + @Mock + protected CitrusContext citrusContext; + + @BeforeClass + public void setupMocks() { + MockitoAnnotations.openMocks(this); + citrus = CitrusInstanceManager.newInstance(() -> citrusContext); + } + + @Override + protected TestContext createTestContext() { + TestContext context = super.createTestContext(); + doAnswer(invocation -> { + context.getReferenceResolver().bind(invocation.getArgument(0, String.class), invocation.getArgument(1)); + return null; + }).when(citrusContext).bind(any(String.class), any()); + + when(citrusContext.getReferenceResolver()).thenReturn(context.getReferenceResolver()); + when(citrusContext.getMessageValidatorRegistry()).thenReturn(context.getMessageValidatorRegistry()); + when(citrusContext.getTestContextFactory()).thenReturn(new StaticTestContextFactory(context)); + doAnswer(invocationOnMock -> { + CitrusAnnotations.parseConfiguration(invocationOnMock.getArgument(0, Object.class), citrusContext); + return null; + }).when(citrusContext).parseConfiguration((Object) any()); + doAnswer(invocationOnMock-> { + context.getReferenceResolver().bind(invocationOnMock.getArgument(0), invocationOnMock.getArgument(1)); + return null; + }).when(citrusContext).addComponent(anyString(), any()); + CitrusAnnotations.injectAll(this, citrus, context); + return context; + } + + protected GroovyTestLoader createTestLoader(String sourcePath) { + GroovyTestLoader testLoader = new GroovyTestLoader().source(sourcePath); + CitrusAnnotations.injectAll(testLoader, citrus, context); + CitrusAnnotations.injectTestRunner(testLoader, new DefaultTestCaseRunner(context)); + + return testLoader; + } +} 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 new file mode 100644 index 0000000000..747c8015f2 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiClientTest.java @@ -0,0 +1,246 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.groovy; + +import java.io.IOException; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; + +import org.citrusframework.TestActor; +import org.citrusframework.TestCase; +import org.citrusframework.TestCaseMetaInfo; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.EndpointAdapter; +import org.citrusframework.endpoint.direct.DirectEndpointAdapter; +import org.citrusframework.endpoint.direct.DirectSyncEndpointConfiguration; +import org.citrusframework.endpoint.resolver.EndpointUriResolver; +import org.citrusframework.groovy.GroovyTestLoader; +import org.citrusframework.http.client.HttpClient; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.http.server.HttpServer; +import org.citrusframework.message.DefaultMessage; +import org.citrusframework.message.DefaultMessageQueue; +import org.citrusframework.message.Message; +import org.citrusframework.message.MessageHeaders; +import org.citrusframework.message.MessageQueue; +import org.citrusframework.spi.BindToRegistry; +import org.citrusframework.util.SocketUtils; +import org.citrusframework.validation.DefaultMessageHeaderValidator; +import org.citrusframework.validation.DefaultTextEqualsMessageValidator; +import org.citrusframework.validation.context.DefaultValidationContext; +import org.citrusframework.validation.context.HeaderValidationContext; +import org.citrusframework.validation.json.JsonMessageValidationContext; +import org.citrusframework.validation.xml.XmlMessageValidationContext; +import org.mockito.Mockito; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; + +/** + * @author Christoph Deppisch + */ +public class OpenApiClientTest extends AbstractGroovyActionDslTest { + + @BindToRegistry + final TestActor testActor = Mockito.mock(TestActor.class); + + @BindToRegistry + private final DefaultMessageHeaderValidator headerValidator = new DefaultMessageHeaderValidator(); + + @BindToRegistry + private final DefaultTextEqualsMessageValidator validator = new DefaultTextEqualsMessageValidator().enableTrim(); + + private final int port = SocketUtils.findAvailableTcpPort(8080); + private final String uri = "http://localhost:" + port + "/test"; + + private HttpServer httpServer; + private HttpClient httpClient; + + private final MessageQueue inboundQueue = new DefaultMessageQueue("inboundQueue"); + + private final Queue responses = new ArrayBlockingQueue<>(6); + + @BeforeClass + public void setupEndpoints() { + EndpointAdapter endpointAdapter = new DirectEndpointAdapter(new DirectSyncEndpointConfiguration()) { + @Override + public Message handleMessageInternal(Message request) { + inboundQueue.send(request); + return responses.isEmpty() ? new HttpMessage().status(HttpStatus.OK) : responses.remove(); + } + }; + + httpServer = http().server() + .port(port) + .timeout(500L) + .endpointAdapter(endpointAdapter) + .autoStart(true) + .name("httpServer") + .build(); + httpServer.initialize(); + + httpClient = http().client() + .requestUrl(uri) + .name("httpClient") + .build(); + } + + @AfterClass(alwaysRun = true) + public void cleanupEndpoints() { + if (httpServer != null) { + httpServer.stop(); + } + } + + @Test + public void shouldLoadOpenApiClientActions() throws IOException { + GroovyTestLoader testLoader = createTestLoader("classpath:org/citrusframework/openapi/groovy/openapi-client.test.groovy"); + + context.setVariable("port", port); + + context.getReferenceResolver().bind("httpClient", httpClient); + context.getReferenceResolver().bind("httpServer", httpServer); + + responses.add(new HttpMessage(""" + { + "id": 1000, + "name": "hasso", + "category": { + "id": 1000, + "name": "dog" + }, + "photoUrls": [ "http://localhost:8080/photos/1000" ], + "tags": [ + { + "id": 1000, + "name": "generated" + } + ], + "status": "available" + } + """).status(HttpStatus.OK).contentType("application/json")); + responses.add(new HttpMessage().status(HttpStatus.CREATED)); + + testLoader.load(); + + TestCase result = testLoader.getTestCase(); + Assert.assertEquals(result.getName(), "OpenApiClientTest"); + Assert.assertEquals(result.getMetaInfo().getAuthor(), "Christoph"); + Assert.assertEquals(result.getMetaInfo().getStatus(), TestCaseMetaInfo.Status.FINAL); + Assert.assertEquals(result.getActionCount(), 4L); + Assert.assertEquals(result.getTestAction(0).getClass(), SendMessageAction.class); + Assert.assertEquals(result.getTestAction(0).getName(), "openapi:send-request"); + + Assert.assertEquals(result.getTestAction(1).getClass(), ReceiveMessageAction.class); + Assert.assertEquals(result.getTestAction(1).getName(), "openapi:receive-response"); + + int actionIndex = 0; + + SendMessageAction sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); + Assert.assertFalse(sendMessageAction.isForkMode()); + Assert.assertTrue(sendMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); + HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()), ""); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.GET.name()); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/pet/${petId}"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_URI), "/pet/${petId}"); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_QUERY_PARAMS)); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.ENDPOINT_URI_HEADER_NAME)); + Assert.assertEquals(sendMessageAction.getEndpoint(), httpClient); + + Message controlMessage = new DefaultMessage(""); + Message request = inboundQueue.receive(); + headerValidator.validateMessage(request, controlMessage, context, new HeaderValidationContext()); + validator.validateMessage(request, controlMessage, context, new DefaultValidationContext()); + + ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + + httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), + "{\"id\": \"@isNumber()@\",\"category\": {\"id\": \"@isNumber()@\",\"name\": \"@notEmpty()@\"},\"name\": \"@notEmpty()@\",\"photoUrls\": \"@ignore@\",\"tags\": \"@ignore@\",\"status\": \"@matches(available|pending|sold)@\"}"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_STATUS_CODE), 200); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REASON_PHRASE), "OK"); + 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.getControlMessageProcessors().size(), 0); + + sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); + Assert.assertFalse(sendMessageAction.isForkMode()); + httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertTrue(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()).toString().startsWith("{\"id\": ")); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + + Map requestHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(requestHeaders.size(), 4L); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.POST.name()); + Assert.assertEquals(requestHeaders.get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_URI), "/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "application/json"); + Assert.assertNull(sendMessageAction.getEndpointUri()); + Assert.assertEquals(sendMessageAction.getEndpoint(), httpClient); + + receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + + httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Map responseHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(responseHeaders.size(), 2L); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_STATUS_CODE), 201); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_REASON_PHRASE), "CREATED"); + Assert.assertNull(receiveMessageAction.getEndpoint()); + Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpClient"); + + Assert.assertEquals(receiveMessageAction.getVariableExtractors().size(), 0L); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiServerTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiServerTest.java new file mode 100644 index 0000000000..e7c7856a91 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiServerTest.java @@ -0,0 +1,205 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.groovy; + +import java.util.Map; + +import org.citrusframework.TestActor; +import org.citrusframework.TestCase; +import org.citrusframework.TestCaseMetaInfo; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.AbstractEndpointAdapter; +import org.citrusframework.endpoint.EndpointAdapter; +import org.citrusframework.endpoint.direct.DirectEndpointAdapter; +import org.citrusframework.endpoint.resolver.EndpointUriResolver; +import org.citrusframework.groovy.GroovyTestLoader; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.http.server.HttpServer; +import org.citrusframework.message.DefaultMessageQueue; +import org.citrusframework.message.MessageHeaders; +import org.citrusframework.message.MessageQueue; +import org.citrusframework.spi.BindToRegistry; +import org.citrusframework.validation.context.HeaderValidationContext; +import org.citrusframework.validation.json.JsonMessageValidationContext; +import org.citrusframework.validation.xml.XmlMessageValidationContext; +import org.mockito.Mockito; +import org.springframework.http.HttpMethod; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.citrusframework.endpoint.direct.DirectEndpoints.direct; +import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; + +/** + * @author Christoph Deppisch + */ +public class OpenApiServerTest extends AbstractGroovyActionDslTest { + + @BindToRegistry + final TestActor testActor = Mockito.mock(TestActor.class); + + private HttpServer httpServer; + + private final MessageQueue inboundQueue = new DefaultMessageQueue("inboundQueue"); + private final EndpointAdapter endpointAdapter = new DirectEndpointAdapter(direct() + .synchronous() + .timeout(100L) + .queue(inboundQueue) + .build()); + + @BeforeClass + public void setupEndpoints() { + ((AbstractEndpointAdapter) endpointAdapter).setTestContextFactory(testContextFactory); + + httpServer = http().server() + .timeout(100L) + .endpointAdapter(endpointAdapter) + .autoStart(true) + .name("httpServer") + .build(); + } + + @Test + public void shouldLoadOpenApiServerActions() { + GroovyTestLoader testLoader = createTestLoader("classpath:org/citrusframework/openapi/groovy/openapi-server.test.groovy"); + + context.getReferenceResolver().bind("httpServer", httpServer); + + endpointAdapter.handleMessage(new HttpMessage() + .method(HttpMethod.GET) + .path("/petstore/v3/pet/12345") + .version("HTTP/1.1") + .accept("application/json") + .contentType("application/json")); + endpointAdapter.handleMessage(new HttpMessage(""" + { + "id": 1000, + "name": "hasso", + "category": { + "id": 1000, + "name": "dog" + }, + "photoUrls": [ "http://localhost:8080/photos/1000" ], + "tags": [ + { + "id": 1000, + "name": "generated" + } + ], + "status": "available" + } + """) + .method(HttpMethod.POST) + .path("/petstore/v3/pet") + .contentType("application/json")); + + testLoader.load(); + + TestCase result = testLoader.getTestCase(); + Assert.assertEquals(result.getName(), "OpenApiServerTest"); + Assert.assertEquals(result.getMetaInfo().getAuthor(), "Christoph"); + Assert.assertEquals(result.getMetaInfo().getStatus(), TestCaseMetaInfo.Status.FINAL); + Assert.assertEquals(result.getActionCount(), 4L); + Assert.assertEquals(result.getTestAction(0).getClass(), ReceiveMessageAction.class); + Assert.assertEquals(result.getTestAction(0).getName(), "openapi:receive-request"); + + Assert.assertEquals(result.getTestAction(1).getClass(), SendMessageAction.class); + Assert.assertEquals(result.getTestAction(1).getName(), "openapi:send-response"); + + int actionIndex = 0; + + ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); + + Assert.assertTrue(receiveMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); + HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.GET.name()); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/petstore/v3/pet/${petId}"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_URI), "/petstore/v3/pet/${petId}"); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_QUERY_PARAMS)); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.ENDPOINT_URI_HEADER_NAME)); + Assert.assertEquals(receiveMessageAction.getEndpoint(), httpServer); + Assert.assertEquals(receiveMessageAction.getControlMessageProcessors().size(), 0); + + SendMessageAction sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); + httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + + Assert.assertTrue(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()).toString().startsWith("{\"id\": ")); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_STATUS_CODE), 200); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REASON_PHRASE), "OK"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "application/json"); + Assert.assertNull(sendMessageAction.getEndpoint()); + Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); + Assert.assertEquals(sendMessageAction.getMessageProcessors().size(), 0); + + receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 2000L); + + httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), + "{\"id\": \"@isNumber()@\",\"category\": {\"id\": \"@isNumber()@\",\"name\": \"@notEmpty()@\"},\"name\": \"@notEmpty()@\",\"photoUrls\": \"@ignore@\",\"tags\": \"@ignore@\",\"status\": \"@matches(available|pending|sold)@\"}"); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + + Map requestHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(requestHeaders.size(), 4L); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.POST.name()); + Assert.assertEquals(requestHeaders.get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/petstore/v3/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_URI), "/petstore/v3/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "@startsWith(application/json)@"); + Assert.assertNull(receiveMessageAction.getEndpointUri()); + Assert.assertEquals(receiveMessageAction.getEndpoint(), httpServer); + + sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex); + httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()), ""); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Map responseHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(responseHeaders.size(), 2L); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_STATUS_CODE), 201); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_REASON_PHRASE), "CREATED"); + Assert.assertNull(sendMessageAction.getEndpoint()); + Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); + } +} 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 new file mode 100644 index 0000000000..c0963e37f4 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientIT.java @@ -0,0 +1,123 @@ +/* + * Copyright 2006-2023 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.integration; + +import org.citrusframework.annotations.CitrusTest; +import org.citrusframework.http.client.HttpClient; +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.spi.BindToRegistry; +import org.citrusframework.spi.Resources; +import org.citrusframework.testng.spring.TestNGCitrusSpringSupport; +import org.citrusframework.util.SocketUtils; +import org.springframework.http.HttpStatus; +import org.testng.annotations.Test; + +import static org.citrusframework.http.actions.HttpActionBuilder.http; +import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi; + +/** + * @author Christoph Deppisch + */ +@Test +public class OpenApiClientIT extends TestNGCitrusSpringSupport { + + private final int port = SocketUtils.findAvailableTcpPort(8080); + + @BindToRegistry + private final HttpServer httpServer = new HttpServerBuilder() + .port(port) + .timeout(5000L) + .autoStart(true) + .defaultStatus(HttpStatus.NO_CONTENT) + .build(); + + @BindToRegistry + private final HttpClient httpClient = new HttpClientBuilder() + .requestUrl("http://localhost:%d".formatted(port)) + .build(); + + private final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( + Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); + + @CitrusTest + public void getPetById() { + variable("petId", "1001"); + + when(openapi(petstoreSpec) + .client(httpClient) + .send("getPetById") + .fork(true)); + + then(http().server(httpServer) + .receive() + .get("/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + then(http().server(httpServer) + .send() + .response(HttpStatus.OK) + .message() + .body(Resources.create("classpath:org/citrusframework/openapi/petstore/pet.json")) + .contentType("application/json")); + + then(openapi(petstoreSpec) + .client(httpClient) + .receive("getPetById", HttpStatus.OK)); + } + + @CitrusTest + public void getAddPet() { + variable("petId", "1001"); + + when(openapi(petstoreSpec) + .client(httpClient) + .send("addPet") + .fork(true)); + + then(http().server(httpServer) + .receive() + .post("/pet") + .message() + .body(""" + { + "id": "@isNumber()@", + "name": "@notEmpty()@", + "category": { + "id": "@isNumber()@", + "name": "@notEmpty()@" + }, + "photoUrls": "@notEmpty()@", + "tags": "@ignore@", + "status": "@matches(sold|pending|available)@" + } + """) + .contentType("application/json;charset=UTF-8")); + + then(http().server(httpServer) + .send() + .response(HttpStatus.CREATED) + .message()); + + then(openapi(petstoreSpec) + .client(httpClient) + .receive("addPet", HttpStatus.CREATED)); + } +} 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 new file mode 100644 index 0000000000..ef13430407 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiServerIT.java @@ -0,0 +1,125 @@ +/* + * Copyright 2006-2023 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.integration; + +import org.citrusframework.annotations.CitrusTest; +import org.citrusframework.http.client.HttpClient; +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.spi.BindToRegistry; +import org.citrusframework.spi.Resources; +import org.citrusframework.testng.spring.TestNGCitrusSpringSupport; +import org.citrusframework.util.SocketUtils; +import org.springframework.http.HttpStatus; +import org.testng.annotations.Test; + +import static org.citrusframework.http.actions.HttpActionBuilder.http; +import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi; + +/** + * @author Christoph Deppisch + */ +@Test +public class OpenApiServerIT extends TestNGCitrusSpringSupport { + + private final int port = SocketUtils.findAvailableTcpPort(8080); + + @BindToRegistry + private final HttpServer httpServer = new HttpServerBuilder() + .port(port) + .timeout(5000L) + .autoStart(true) + .defaultStatus(HttpStatus.NO_CONTENT) + .build(); + + @BindToRegistry + private final HttpClient httpClient = new HttpClientBuilder() + .requestUrl("http://localhost:%d/petstore/v3".formatted(port)) + .build(); + + private final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( + Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); + + @CitrusTest + public void getPetById() { + variable("petId", "1001"); + + when(http() + .client(httpClient) + .send() + .get("/pet/${petId}") + .message() + .accept("application/json") + .fork(true)); + + then(openapi(petstoreSpec) + .server(httpServer) + .receive("getPetById")); + + then(openapi(petstoreSpec) + .server(httpServer) + .send("getPetById", HttpStatus.OK)); + + then(http() + .client(httpClient) + .receive() + .response(HttpStatus.OK) + .message() + .body(""" + { + "id": "@isNumber()@", + "name": "@notEmpty()@", + "category": { + "id": "@isNumber()@", + "name": "@notEmpty()@" + }, + "photoUrls": "@notEmpty()@", + "tags": "@ignore@", + "status": "@matches(sold|pending|available)@" + } + """)); + } + + @CitrusTest + public void getAddPet() { + variable("petId", "1001"); + + when(http() + .client(httpClient) + .send() + .post("/pet") + .message() + .body(Resources.create("classpath:org/citrusframework/openapi/petstore/pet.json")) + .contentType("application/json") + .fork(true)); + + then(openapi(petstoreSpec) + .server(httpServer) + .receive("addPet")); + + then(openapi(petstoreSpec) + .server(httpServer) + .send("addPet", HttpStatus.CREATED)); + + then(http() + .client(httpClient) + .receive() + .response(HttpStatus.CREATED)); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/AbstractXmlActionTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/AbstractXmlActionTest.java new file mode 100644 index 0000000000..ff52174e31 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/AbstractXmlActionTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.xml; + +import org.citrusframework.Citrus; +import org.citrusframework.CitrusContext; +import org.citrusframework.CitrusInstanceManager; +import org.citrusframework.DefaultTestCaseRunner; +import org.citrusframework.annotations.CitrusAnnotations; +import org.citrusframework.context.StaticTestContextFactory; +import org.citrusframework.context.TestContext; +import org.citrusframework.testng.AbstractTestNGUnitTest; +import org.citrusframework.xml.XmlTestLoader; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.BeforeClass; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +/** + * @author Christoph Deppisch + */ +public class AbstractXmlActionTest extends AbstractTestNGUnitTest { + + protected Citrus citrus; + + @Mock + protected CitrusContext citrusContext; + + @BeforeClass + public void setupMocks() { + MockitoAnnotations.openMocks(this); + citrus = CitrusInstanceManager.newInstance(() -> citrusContext); + } + + @Override + protected TestContext createTestContext() { + TestContext context = super.createTestContext(); + when(citrusContext.getReferenceResolver()).thenReturn(context.getReferenceResolver()); + when(citrusContext.getMessageValidatorRegistry()).thenReturn(context.getMessageValidatorRegistry()); + when(citrusContext.getTestContextFactory()).thenReturn(new StaticTestContextFactory(context)); + doAnswer(invocationOnMock -> { + CitrusAnnotations.parseConfiguration(invocationOnMock.getArgument(0, Object.class), citrusContext); + return null; + }).when(citrusContext).parseConfiguration((Object) any()); + doAnswer(invocationOnMock-> { + context.getReferenceResolver().bind(invocationOnMock.getArgument(0), invocationOnMock.getArgument(1)); + return null; + }).when(citrusContext).addComponent(anyString(), any()); + CitrusAnnotations.injectAll(this, citrus, context); + return context; + } + + protected XmlTestLoader createTestLoader(String sourcePath) { + XmlTestLoader testLoader = new XmlTestLoader(this.getClass(), "Test", this.getClass().getPackageName()); + CitrusAnnotations.injectAll(testLoader, citrus, context); + CitrusAnnotations.injectTestRunner(testLoader, new DefaultTestCaseRunner(context)); + testLoader.setSource(sourcePath); + + return testLoader; + } +} 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 new file mode 100644 index 0000000000..aa9a5debdd --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiClientTest.java @@ -0,0 +1,256 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.xml; + +import java.io.IOException; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; + +import org.citrusframework.TestActor; +import org.citrusframework.TestCase; +import org.citrusframework.TestCaseMetaInfo; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.EndpointAdapter; +import org.citrusframework.endpoint.direct.DirectEndpointAdapter; +import org.citrusframework.endpoint.direct.DirectSyncEndpointConfiguration; +import org.citrusframework.endpoint.resolver.EndpointUriResolver; +import org.citrusframework.http.client.HttpClient; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.http.server.HttpServer; +import org.citrusframework.message.DefaultMessage; +import org.citrusframework.message.DefaultMessageQueue; +import org.citrusframework.message.Message; +import org.citrusframework.message.MessageHeaders; +import org.citrusframework.message.MessageQueue; +import org.citrusframework.spi.BindToRegistry; +import org.citrusframework.spi.Resources; +import org.citrusframework.util.FileUtils; +import org.citrusframework.util.SocketUtils; +import org.citrusframework.validation.DefaultMessageHeaderValidator; +import org.citrusframework.validation.DefaultTextEqualsMessageValidator; +import org.citrusframework.validation.context.DefaultValidationContext; +import org.citrusframework.validation.context.HeaderValidationContext; +import org.citrusframework.validation.json.JsonMessageValidationContext; +import org.citrusframework.validation.xml.XmlMessageValidationContext; +import org.citrusframework.xml.XmlTestLoader; +import org.citrusframework.xml.actions.XmlTestActionBuilder; +import org.mockito.Mockito; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; + +/** + * @author Christoph Deppisch + */ +public class OpenApiClientTest extends AbstractXmlActionTest { + + @BindToRegistry + final TestActor testActor = Mockito.mock(TestActor.class); + + @BindToRegistry + private final DefaultMessageHeaderValidator headerValidator = new DefaultMessageHeaderValidator(); + + @BindToRegistry + private final DefaultTextEqualsMessageValidator validator = new DefaultTextEqualsMessageValidator().enableTrim(); + + private final int port = SocketUtils.findAvailableTcpPort(8080); + private final String uri = "http://localhost:" + port + "/test"; + + private HttpServer httpServer; + private HttpClient httpClient; + + private final MessageQueue inboundQueue = new DefaultMessageQueue("inboundQueue"); + + private final Queue responses = new ArrayBlockingQueue<>(6); + + @BeforeClass + public void setupEndpoints() { + EndpointAdapter endpointAdapter = new DirectEndpointAdapter(new DirectSyncEndpointConfiguration()) { + @Override + public Message handleMessageInternal(Message request) { + inboundQueue.send(request); + return responses.isEmpty() ? new HttpMessage().status(HttpStatus.OK) : responses.remove(); + } + }; + + httpServer = http().server() + .port(port) + .timeout(500L) + .endpointAdapter(endpointAdapter) + .autoStart(true) + .name("httpServer") + .build(); + httpServer.initialize(); + + httpClient = http().client() + .requestUrl(uri) + .name("httpClient") + .build(); + } + + @AfterClass(alwaysRun = true) + public void cleanupEndpoints() { + if (httpServer != null) { + httpServer.stop(); + } + } + + @Test + public void shouldLoadOpenApiClientActions() throws IOException { + XmlTestLoader testLoader = createTestLoader("classpath:org/citrusframework/openapi/xml/openapi-client-test.xml"); + + context.setVariable("port", port); + + context.getReferenceResolver().bind("httpClient", httpClient); + context.getReferenceResolver().bind("httpServer", httpServer); + + responses.add(new HttpMessage(FileUtils.readToString(Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml")))); + responses.add(new HttpMessage(""" + { + "id": 1000, + "name": "hasso", + "category": { + "id": 1000, + "name": "dog" + }, + "photoUrls": [ "http://localhost:8080/photos/1000" ], + "tags": [ + { + "id": 1000, + "name": "generated" + } + ], + "status": "available" + } + """).status(HttpStatus.OK).contentType("application/json")); + responses.add(new HttpMessage().status(HttpStatus.CREATED)); + + testLoader.load(); + + TestCase result = testLoader.getTestCase(); + Assert.assertEquals(result.getName(), "OpenApiClientTest"); + Assert.assertEquals(result.getMetaInfo().getAuthor(), "Christoph"); + Assert.assertEquals(result.getMetaInfo().getStatus(), TestCaseMetaInfo.Status.FINAL); + Assert.assertEquals(result.getActionCount(), 4L); + Assert.assertEquals(result.getTestAction(0).getClass(), SendMessageAction.class); + Assert.assertEquals(result.getTestAction(0).getName(), "openapi:send-request"); + + Assert.assertEquals(result.getTestAction(1).getClass(), ReceiveMessageAction.class); + Assert.assertEquals(result.getTestAction(1).getName(), "openapi:receive-response"); + + int actionIndex = 0; + + SendMessageAction sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); + Assert.assertFalse(sendMessageAction.isForkMode()); + Assert.assertTrue(sendMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); + HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()), ""); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.GET.name()); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/pet/${petId}"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_URI), "/pet/${petId}"); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_QUERY_PARAMS)); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.ENDPOINT_URI_HEADER_NAME)); + Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpClient"); + + Message controlMessage = new DefaultMessage(""); + Message request = inboundQueue.receive(); + headerValidator.validateMessage(request, controlMessage, context, new HeaderValidationContext()); + validator.validateMessage(request, controlMessage, context, new DefaultValidationContext()); + + ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof HeaderValidationContext); + + httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), + "{\"id\": \"@isNumber()@\",\"category\": {\"id\": \"@isNumber()@\",\"name\": \"@notEmpty()@\"},\"name\": \"@notEmpty()@\",\"photoUrls\": \"@ignore@\",\"tags\": \"@ignore@\",\"status\": \"@matches(available|pending|sold)@\"}"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_STATUS_CODE), 200); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REASON_PHRASE), "OK"); + 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.getControlMessageProcessors().size(), 0); + + sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); + Assert.assertFalse(sendMessageAction.isForkMode()); + httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertTrue(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()).toString().startsWith("{\"id\": ")); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + + Map requestHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(requestHeaders.size(), 4L); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.POST.name()); + Assert.assertEquals(requestHeaders.get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_URI), "/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "application/json"); + Assert.assertNull(sendMessageAction.getEndpoint()); + Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpClient"); + + receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof HeaderValidationContext); + + httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Map responseHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(responseHeaders.size(), 2L); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_STATUS_CODE), 201); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_REASON_PHRASE), "CREATED"); + Assert.assertNull(receiveMessageAction.getEndpoint()); + Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpClient"); + + Assert.assertEquals(receiveMessageAction.getVariableExtractors().size(), 0L); + } + + @Test + public void shouldLookupTestActionBuilder() { + Assert.assertTrue(XmlTestActionBuilder.lookup("openapi").isPresent()); + Assert.assertEquals(XmlTestActionBuilder.lookup("openapi").get().getClass(), OpenApi.class); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiServerTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiServerTest.java new file mode 100644 index 0000000000..a38dcf5acc --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiServerTest.java @@ -0,0 +1,205 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.xml; + +import java.util.Map; + +import org.citrusframework.TestActor; +import org.citrusframework.TestCase; +import org.citrusframework.TestCaseMetaInfo; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.AbstractEndpointAdapter; +import org.citrusframework.endpoint.EndpointAdapter; +import org.citrusframework.endpoint.direct.DirectEndpointAdapter; +import org.citrusframework.endpoint.resolver.EndpointUriResolver; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.http.server.HttpServer; +import org.citrusframework.message.DefaultMessageQueue; +import org.citrusframework.message.MessageHeaders; +import org.citrusframework.message.MessageQueue; +import org.citrusframework.spi.BindToRegistry; +import org.citrusframework.validation.context.HeaderValidationContext; +import org.citrusframework.validation.json.JsonMessageValidationContext; +import org.citrusframework.validation.xml.XmlMessageValidationContext; +import org.citrusframework.xml.XmlTestLoader; +import org.mockito.Mockito; +import org.springframework.http.HttpMethod; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.citrusframework.endpoint.direct.DirectEndpoints.direct; +import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; + +/** + * @author Christoph Deppisch + */ +public class OpenApiServerTest extends AbstractXmlActionTest { + + @BindToRegistry + final TestActor testActor = Mockito.mock(TestActor.class); + + private HttpServer httpServer; + + private final MessageQueue inboundQueue = new DefaultMessageQueue("inboundQueue"); + private final EndpointAdapter endpointAdapter = new DirectEndpointAdapter(direct() + .synchronous() + .timeout(100L) + .queue(inboundQueue) + .build()); + + @BeforeClass + public void setupEndpoints() { + ((AbstractEndpointAdapter) endpointAdapter).setTestContextFactory(testContextFactory); + + httpServer = http().server() + .timeout(100L) + .endpointAdapter(endpointAdapter) + .autoStart(true) + .name("httpServer") + .build(); + } + + @Test + public void shouldLoadOpenApiServerActions() { + XmlTestLoader testLoader = createTestLoader("classpath:org/citrusframework/openapi/xml/openapi-server-test.xml"); + + context.getReferenceResolver().bind("httpServer", httpServer); + + endpointAdapter.handleMessage(new HttpMessage() + .method(HttpMethod.GET) + .path("/petstore/v3/pet/12345") + .version("HTTP/1.1") + .accept("application/json") + .contentType("application/json")); + endpointAdapter.handleMessage(new HttpMessage(""" + { + "id": 1000, + "name": "hasso", + "category": { + "id": 1000, + "name": "dog" + }, + "photoUrls": [ "http://localhost:8080/photos/1000" ], + "tags": [ + { + "id": 1000, + "name": "generated" + } + ], + "status": "available" + } + """) + .method(HttpMethod.POST) + .path("/petstore/v3/pet") + .contentType("application/json")); + + testLoader.load(); + + TestCase result = testLoader.getTestCase(); + Assert.assertEquals(result.getName(), "OpenApiServerTest"); + Assert.assertEquals(result.getMetaInfo().getAuthor(), "Christoph"); + Assert.assertEquals(result.getMetaInfo().getStatus(), TestCaseMetaInfo.Status.FINAL); + Assert.assertEquals(result.getActionCount(), 4L); + Assert.assertEquals(result.getTestAction(0).getClass(), ReceiveMessageAction.class); + Assert.assertEquals(result.getTestAction(0).getName(), "openapi:receive-request"); + + Assert.assertEquals(result.getTestAction(1).getClass(), SendMessageAction.class); + Assert.assertEquals(result.getTestAction(1).getName(), "openapi:send-response"); + + int actionIndex = 0; + + ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); + + Assert.assertTrue(receiveMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); + HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.GET.name()); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/petstore/v3/pet/${petId}"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_URI), "/petstore/v3/pet/${petId}"); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_QUERY_PARAMS)); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.ENDPOINT_URI_HEADER_NAME)); + Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpServer"); + Assert.assertEquals(receiveMessageAction.getControlMessageProcessors().size(), 0); + + SendMessageAction sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); + httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + + Assert.assertTrue(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()).toString().startsWith("{\"id\": ")); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_STATUS_CODE), 200); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REASON_PHRASE), "OK"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "application/json"); + Assert.assertNull(sendMessageAction.getEndpoint()); + Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); + Assert.assertEquals(sendMessageAction.getMessageProcessors().size(), 0); + + receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 2000L); + + httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), + "{\"id\": \"@isNumber()@\",\"category\": {\"id\": \"@isNumber()@\",\"name\": \"@notEmpty()@\"},\"name\": \"@notEmpty()@\",\"photoUrls\": \"@ignore@\",\"tags\": \"@ignore@\",\"status\": \"@matches(available|pending|sold)@\"}"); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + + Map requestHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(requestHeaders.size(), 4L); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.POST.name()); + Assert.assertEquals(requestHeaders.get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/petstore/v3/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_URI), "/petstore/v3/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "@startsWith(application/json)@"); + Assert.assertNull(receiveMessageAction.getEndpoint()); + Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpServer"); + + sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex); + httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()), ""); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Map responseHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(responseHeaders.size(), 2L); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_STATUS_CODE), 201); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_REASON_PHRASE), "CREATED"); + Assert.assertNull(sendMessageAction.getEndpoint()); + Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/AbstractYamlActionTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/AbstractYamlActionTest.java new file mode 100644 index 0000000000..a2f1f71aa9 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/AbstractYamlActionTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.yaml; + +import org.citrusframework.Citrus; +import org.citrusframework.CitrusContext; +import org.citrusframework.CitrusInstanceManager; +import org.citrusframework.DefaultTestCaseRunner; +import org.citrusframework.annotations.CitrusAnnotations; +import org.citrusframework.context.StaticTestContextFactory; +import org.citrusframework.context.TestContext; +import org.citrusframework.testng.AbstractTestNGUnitTest; +import org.citrusframework.yaml.YamlTestLoader; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.BeforeClass; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +/** + * @author Christoph Deppisch + */ +public class AbstractYamlActionTest extends AbstractTestNGUnitTest { + + protected Citrus citrus; + + @Mock + protected CitrusContext citrusContext; + + @BeforeClass + public void setupMocks() { + MockitoAnnotations.openMocks(this); + citrus = CitrusInstanceManager.newInstance(() -> citrusContext); + } + + @Override + protected TestContext createTestContext() { + TestContext context = super.createTestContext(); + when(citrusContext.getReferenceResolver()).thenReturn(context.getReferenceResolver()); + when(citrusContext.getMessageValidatorRegistry()).thenReturn(context.getMessageValidatorRegistry()); + when(citrusContext.getTestContextFactory()).thenReturn(new StaticTestContextFactory(context)); + doAnswer(invocationOnMock -> { + CitrusAnnotations.parseConfiguration(invocationOnMock.getArgument(0, Object.class), citrusContext); + return null; + }).when(citrusContext).parseConfiguration((Object) any()); + doAnswer(invocationOnMock-> { + context.getReferenceResolver().bind(invocationOnMock.getArgument(0), invocationOnMock.getArgument(1)); + return null; + }).when(citrusContext).addComponent(anyString(), any()); + CitrusAnnotations.injectAll(this, citrus, context); + return context; + } + + protected YamlTestLoader createTestLoader(String sourcePath) { + YamlTestLoader testLoader = new YamlTestLoader(this.getClass(), "Test", this.getClass().getPackageName()); + CitrusAnnotations.injectAll(testLoader, citrus, context); + CitrusAnnotations.injectTestRunner(testLoader, new DefaultTestCaseRunner(context)); + testLoader.setSource(sourcePath); + + return testLoader; + } +} 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 new file mode 100644 index 0000000000..a4ef108ff3 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiClientTest.java @@ -0,0 +1,253 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.yaml; + +import java.io.IOException; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; + +import org.citrusframework.TestActor; +import org.citrusframework.TestCase; +import org.citrusframework.TestCaseMetaInfo; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.EndpointAdapter; +import org.citrusframework.endpoint.direct.DirectEndpointAdapter; +import org.citrusframework.endpoint.direct.DirectSyncEndpointConfiguration; +import org.citrusframework.endpoint.resolver.EndpointUriResolver; +import org.citrusframework.http.client.HttpClient; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.http.server.HttpServer; +import org.citrusframework.message.DefaultMessage; +import org.citrusframework.message.DefaultMessageQueue; +import org.citrusframework.message.Message; +import org.citrusframework.message.MessageHeaders; +import org.citrusframework.message.MessageQueue; +import org.citrusframework.spi.BindToRegistry; +import org.citrusframework.util.SocketUtils; +import org.citrusframework.validation.DefaultMessageHeaderValidator; +import org.citrusframework.validation.DefaultTextEqualsMessageValidator; +import org.citrusframework.validation.context.DefaultValidationContext; +import org.citrusframework.validation.context.HeaderValidationContext; +import org.citrusframework.validation.json.JsonMessageValidationContext; +import org.citrusframework.validation.xml.XmlMessageValidationContext; +import org.citrusframework.yaml.YamlTestLoader; +import org.citrusframework.yaml.actions.YamlTestActionBuilder; +import org.mockito.Mockito; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; + +/** + * @author Christoph Deppisch + */ +public class OpenApiClientTest extends AbstractYamlActionTest { + + @BindToRegistry + final TestActor testActor = Mockito.mock(TestActor.class); + + @BindToRegistry + private final DefaultMessageHeaderValidator headerValidator = new DefaultMessageHeaderValidator(); + + @BindToRegistry + private final DefaultTextEqualsMessageValidator validator = new DefaultTextEqualsMessageValidator().enableTrim(); + + private final int port = SocketUtils.findAvailableTcpPort(8080); + private final String uri = "http://localhost:" + port + "/test"; + + private HttpServer httpServer; + private HttpClient httpClient; + + private final MessageQueue inboundQueue = new DefaultMessageQueue("inboundQueue"); + + private final Queue responses = new ArrayBlockingQueue<>(6); + + @BeforeClass + public void setupEndpoints() { + EndpointAdapter endpointAdapter = new DirectEndpointAdapter(new DirectSyncEndpointConfiguration()) { + @Override + public Message handleMessageInternal(Message request) { + inboundQueue.send(request); + return responses.isEmpty() ? new HttpMessage().status(HttpStatus.OK) : responses.remove(); + } + }; + + httpServer = http().server() + .port(port) + .timeout(500L) + .endpointAdapter(endpointAdapter) + .autoStart(true) + .name("httpServer") + .build(); + httpServer.initialize(); + + httpClient = http().client() + .requestUrl(uri) + .name("httpClient") + .build(); + } + + @AfterClass(alwaysRun = true) + public void cleanupEndpoints() { + if (httpServer != null) { + httpServer.stop(); + } + } + + @Test + public void shouldLoadOpenApiClientActions() throws IOException { + YamlTestLoader testLoader = createTestLoader("classpath:org/citrusframework/openapi/yaml/openapi-client-test.yaml"); + + context.setVariable("port", port); + + context.getReferenceResolver().bind("httpClient", httpClient); + context.getReferenceResolver().bind("httpServer", httpServer); + + responses.add(new HttpMessage(""" + { + "id": 1000, + "name": "hasso", + "category": { + "id": 1000, + "name": "dog" + }, + "photoUrls": [ "http://localhost:8080/photos/1000" ], + "tags": [ + { + "id": 1000, + "name": "generated" + } + ], + "status": "available" + } + """).status(HttpStatus.OK).contentType("application/json")); + responses.add(new HttpMessage().status(HttpStatus.CREATED)); + + testLoader.load(); + + TestCase result = testLoader.getTestCase(); + Assert.assertEquals(result.getName(), "OpenApiClientTest"); + Assert.assertEquals(result.getMetaInfo().getAuthor(), "Christoph"); + Assert.assertEquals(result.getMetaInfo().getStatus(), TestCaseMetaInfo.Status.FINAL); + Assert.assertEquals(result.getActionCount(), 4L); + Assert.assertEquals(result.getTestAction(0).getClass(), SendMessageAction.class); + Assert.assertEquals(result.getTestAction(0).getName(), "openapi:send-request"); + + Assert.assertEquals(result.getTestAction(1).getClass(), ReceiveMessageAction.class); + Assert.assertEquals(result.getTestAction(1).getName(), "openapi:receive-response"); + + int actionIndex = 0; + + SendMessageAction sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); + Assert.assertFalse(sendMessageAction.isForkMode()); + Assert.assertTrue(sendMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); + HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()), ""); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.GET.name()); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/pet/${petId}"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_URI), "/pet/${petId}"); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_QUERY_PARAMS)); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.ENDPOINT_URI_HEADER_NAME)); + Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpClient"); + + Message controlMessage = new DefaultMessage(""); + Message request = inboundQueue.receive(); + headerValidator.validateMessage(request, controlMessage, context, new HeaderValidationContext()); + validator.validateMessage(request, controlMessage, context, new DefaultValidationContext()); + + ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof HeaderValidationContext); + + httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), + "{\"id\": \"@isNumber()@\",\"category\": {\"id\": \"@isNumber()@\",\"name\": \"@notEmpty()@\"},\"name\": \"@notEmpty()@\",\"photoUrls\": \"@ignore@\",\"tags\": \"@ignore@\",\"status\": \"@matches(available|pending|sold)@\"}"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_STATUS_CODE), 200); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REASON_PHRASE), "OK"); + 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.getControlMessageProcessors().size(), 0); + + sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); + Assert.assertFalse(sendMessageAction.isForkMode()); + httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertTrue(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()).toString().startsWith("{\"id\": ")); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + + Map requestHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(requestHeaders.size(), 4L); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.POST.name()); + Assert.assertEquals(requestHeaders.get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_URI), "/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "application/json"); + Assert.assertNull(sendMessageAction.getEndpoint()); + Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpClient"); + + receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof HeaderValidationContext); + + httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Map responseHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(responseHeaders.size(), 2L); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_STATUS_CODE), 201); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_REASON_PHRASE), "CREATED"); + Assert.assertNull(receiveMessageAction.getEndpoint()); + Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpClient"); + + Assert.assertEquals(receiveMessageAction.getVariableExtractors().size(), 0L); + } + + @Test + public void shouldLookupTestActionBuilder() { + Assert.assertTrue(YamlTestActionBuilder.lookup("openapi").isPresent()); + Assert.assertEquals(YamlTestActionBuilder.lookup("openapi").get().getClass(), OpenApi.class); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiServerTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiServerTest.java new file mode 100644 index 0000000000..a15e70fc96 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiServerTest.java @@ -0,0 +1,205 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.yaml; + +import java.util.Map; + +import org.citrusframework.TestActor; +import org.citrusframework.TestCase; +import org.citrusframework.TestCaseMetaInfo; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.AbstractEndpointAdapter; +import org.citrusframework.endpoint.EndpointAdapter; +import org.citrusframework.endpoint.direct.DirectEndpointAdapter; +import org.citrusframework.endpoint.resolver.EndpointUriResolver; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.http.server.HttpServer; +import org.citrusframework.message.DefaultMessageQueue; +import org.citrusframework.message.MessageHeaders; +import org.citrusframework.message.MessageQueue; +import org.citrusframework.spi.BindToRegistry; +import org.citrusframework.validation.context.HeaderValidationContext; +import org.citrusframework.validation.json.JsonMessageValidationContext; +import org.citrusframework.validation.xml.XmlMessageValidationContext; +import org.citrusframework.yaml.YamlTestLoader; +import org.mockito.Mockito; +import org.springframework.http.HttpMethod; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.citrusframework.endpoint.direct.DirectEndpoints.direct; +import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; + +/** + * @author Christoph Deppisch + */ +public class OpenApiServerTest extends AbstractYamlActionTest { + + @BindToRegistry + final TestActor testActor = Mockito.mock(TestActor.class); + + private HttpServer httpServer; + + private final MessageQueue inboundQueue = new DefaultMessageQueue("inboundQueue"); + private final EndpointAdapter endpointAdapter = new DirectEndpointAdapter(direct() + .synchronous() + .timeout(100L) + .queue(inboundQueue) + .build()); + + @BeforeClass + public void setupEndpoints() { + ((AbstractEndpointAdapter) endpointAdapter).setTestContextFactory(testContextFactory); + + httpServer = http().server() + .timeout(100L) + .endpointAdapter(endpointAdapter) + .autoStart(true) + .name("httpServer") + .build(); + } + + @Test + public void shouldLoadOpenApiServerActions() { + YamlTestLoader testLoader = createTestLoader("classpath:org/citrusframework/openapi/yaml/openapi-server-test.yaml"); + + context.getReferenceResolver().bind("httpServer", httpServer); + + endpointAdapter.handleMessage(new HttpMessage() + .method(HttpMethod.GET) + .path("/petstore/v3/pet/12345") + .version("HTTP/1.1") + .accept("application/json") + .contentType("application/json")); + endpointAdapter.handleMessage(new HttpMessage(""" + { + "id": 1000, + "name": "hasso", + "category": { + "id": 1000, + "name": "dog" + }, + "photoUrls": [ "http://localhost:8080/photos/1000" ], + "tags": [ + { + "id": 1000, + "name": "generated" + } + ], + "status": "available" + } + """) + .method(HttpMethod.POST) + .path("/petstore/v3/pet") + .contentType("application/json")); + + testLoader.load(); + + TestCase result = testLoader.getTestCase(); + Assert.assertEquals(result.getName(), "OpenApiServerTest"); + Assert.assertEquals(result.getMetaInfo().getAuthor(), "Christoph"); + Assert.assertEquals(result.getMetaInfo().getStatus(), TestCaseMetaInfo.Status.FINAL); + Assert.assertEquals(result.getActionCount(), 4L); + Assert.assertEquals(result.getTestAction(0).getClass(), ReceiveMessageAction.class); + Assert.assertEquals(result.getTestAction(0).getName(), "openapi:receive-request"); + + Assert.assertEquals(result.getTestAction(1).getClass(), SendMessageAction.class); + Assert.assertEquals(result.getTestAction(1).getName(), "openapi:send-response"); + + int actionIndex = 0; + + ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); + + Assert.assertTrue(receiveMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); + HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.GET.name()); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/petstore/v3/pet/${petId}"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_URI), "/petstore/v3/pet/${petId}"); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_QUERY_PARAMS)); + Assert.assertNull(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.ENDPOINT_URI_HEADER_NAME)); + Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpServer"); + Assert.assertEquals(receiveMessageAction.getControlMessageProcessors().size(), 0); + + SendMessageAction sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); + httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + + Assert.assertTrue(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()).toString().startsWith("{\"id\": ")); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_STATUS_CODE), 200); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REASON_PHRASE), "OK"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "application/json"); + Assert.assertNull(sendMessageAction.getEndpoint()); + Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); + Assert.assertEquals(sendMessageAction.getMessageProcessors().size(), 0); + + receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 2000L); + + httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), + "{\"id\": \"@isNumber()@\",\"category\": {\"id\": \"@isNumber()@\",\"name\": \"@notEmpty()@\"},\"name\": \"@notEmpty()@\",\"photoUrls\": \"@ignore@\",\"tags\": \"@ignore@\",\"status\": \"@matches(available|pending|sold)@\"}"); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + + Map requestHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(requestHeaders.size(), 4L); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.POST.name()); + Assert.assertEquals(requestHeaders.get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/petstore/v3/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_URI), "/petstore/v3/pet"); + Assert.assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_CONTENT_TYPE), "@startsWith(application/json)@"); + Assert.assertNull(receiveMessageAction.getEndpoint()); + Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpServer"); + + sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex); + httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + Assert.assertNotNull(httpMessageBuilder); + Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()), ""); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + Map responseHeaders = httpMessageBuilder.buildMessageHeaders(context); + Assert.assertEquals(responseHeaders.size(), 2L); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_STATUS_CODE), 201); + Assert.assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_REASON_PHRASE), "CREATED"); + Assert.assertNull(sendMessageAction.getEndpoint()); + Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); + } +} diff --git a/connectors/citrus-openapi/src/test/resources/log4j2-test.xml b/connectors/citrus-openapi/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000000..b275c86247 --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/log4j2-test.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/context/citrus-unit-context.xml b/connectors/citrus-openapi/src/test/resources/org/citrusframework/context/citrus-unit-context.xml new file mode 100644 index 0000000000..044b3ae994 --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/context/citrus-unit-context.xml @@ -0,0 +1,8 @@ + + + + diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/groovy/openapi-client.test.groovy b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/groovy/openapi-client.test.groovy new file mode 100644 index 0000000000..2c8b9c9193 --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/groovy/openapi-client.test.groovy @@ -0,0 +1,51 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.groovy + +import org.springframework.http.HttpStatus + +import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi + +name "OpenApiClientTest" +author "Christoph" +status "FINAL" +description "Sample test in Groovy" + +variables { + petId="12345" +} + +actions { + $(openapi().specification("classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml") + .client(httpClient) + .send("getPetById")) + $(openapi().specification("classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml") + .client("httpClient") + .receive("getPetById", HttpStatus.OK)) + + $(openapi().specification("classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml") + .client(httpClient) + .send("addPet")) + $(openapi().specification("classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml") + .client("httpClient") + .receive("addPet", HttpStatus.CREATED)) + + +} diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/groovy/openapi-server.test.groovy b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/groovy/openapi-server.test.groovy new file mode 100644 index 0000000000..2aeab80bf5 --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/groovy/openapi-server.test.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.groovy + +import org.springframework.http.HttpStatus + +import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi + +name "OpenApiServerTest" +author "Christoph" +status "FINAL" +description "Sample test in Groovy" + +variables { + petId="12345" +} + +actions { + $(openapi().specification("classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml") + .server(httpServer) + .receive("getPetById")) + $(openapi().specification("classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml") + .server("httpServer") + .send("getPetById", HttpStatus.OK)) + + $(openapi().specification("classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml") + .server(httpServer) + .receive("addPet") + .timeout(2000)) + $(openapi().specification("classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml") + .server("httpServer") + .send("addPet", HttpStatus.CREATED)) +} diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/pet.json b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/pet.json new file mode 100644 index 0000000000..0d4e504a8f --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/pet.json @@ -0,0 +1,16 @@ +{ + "id": ${petId}, + "name": "citrus:randomEnumValue('hasso','cutie','fluffy')", + "category": { + "id": ${petId}, + "name": "citrus:randomEnumValue('dog', 'cat', 'fish')" + }, + "photoUrls": [ "http://localhost:8080/photos/${petId}" ], + "tags": [ + { + "id": ${petId}, + "name": "generated" + } + ], + "status": "citrus:randomEnumValue('available', 'pending', 'sold')" +} diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v2.json b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v2.json new file mode 100644 index 0000000000..9b1418a1dc --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v2.json @@ -0,0 +1,287 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server.", + "version": "1.0.1", + "title": "Swagger Petstore", + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "localhost", + "basePath": "/petstore/v2", + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets" + } + ], + "schemes": [ + "https" + ], + "paths": { + "/pet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "201": { + "description": "Created" + }, + "405": { + "description": "Invalid input" + } + } + }, + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + } + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "verbose", + "in": "query", + "description": "Output details", + "required": false, + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + } + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "api_key", + "in": "header", + "required": false, + "type": "string" + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "definitions": { + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "type": "object", + "required": [ + "category", + "name", + "status" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } +} diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v3.json b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v3.json new file mode 100644 index 0000000000..618854948f --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v3.json @@ -0,0 +1,292 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Swagger Petstore", + "version": "1.0.1", + "description": "This is a sample server Petstore server.", + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "servers": [ + { + "url": "http://localhost/petstore/v3" + } + ], + "paths": { + "/pet": { + "put": { + "requestBody": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "tags": [ + "pet" + ], + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "operationId": "updatePet", + "summary": "Update an existing pet", + "description": "" + }, + "post": { + "requestBody": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "tags": [ + "pet" + ], + "responses": { + "201": { + "description": "Created" + }, + "405": { + "description": "Invalid input" + } + }, + "operationId": "addPet", + "summary": "Add a new pet to the store", + "description": "" + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "parameters": [ + { + "name": "petId", + "description": "ID of pet to return", + "schema": { + "format": "int64", + "type": "integer" + }, + "in": "path", + "required": true + }, + { + "name": "verbose", + "description": "Output details", + "schema": { + "type": "boolean" + }, + "in": "query", + "required": false + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "description": "successful operation" + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "operationId": "getPetById", + "summary": "Find pet by ID", + "description": "Returns a single pet" + }, + "delete": { + "tags": [ + "pet" + ], + "parameters": [ + { + "name": "api_key", + "schema": { + "type": "string" + }, + "in": "header", + "required": false + }, + { + "name": "petId", + "description": "Pet id to delete", + "schema": { + "format": "int64", + "type": "integer" + }, + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "operationId": "deletePet", + "summary": "Deletes a pet", + "description": "" + } + } + }, + "components": { + "schemas": { + "Category": { + "type": "object", + "properties": { + "id": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "required": [ + "category", + "name", + "status" + ], + "type": "object", + "properties": { + "id": { + "format": "int64", + "type": "integer" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "items": { + "type": "string" + }, + "xml": { + "name": "photoUrl", + "wrapped": true + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + }, + "xml": { + "name": "tag", + "wrapped": true + } + }, + "status": { + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ], + "type": "string" + } + }, + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + }, + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets" + } + ] +} diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v3.yaml b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v3.yaml new file mode 100644 index 0000000000..bf3bde73d9 --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-v3.yaml @@ -0,0 +1,197 @@ +openapi: 3.0.2 +info: + title: Swagger Petstore + version: 1.0.1 + description: This is a sample server Petstore server. + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +servers: + - url: 'http://localhost/petstore/v3/' +paths: + /pet: + put: + requestBody: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + required: true + tags: + - pet + responses: + '204': + description: No content + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + operationId: updatePet + summary: Update an existing pet + description: '' + post: + requestBody: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + required: true + tags: + - pet + responses: + '201': + description: Created + '405': + description: Invalid input + operationId: addPet + summary: Add a new pet to the store + description: '' + '/pet/{petId}': + get: + tags: + - pet + parameters: + - + name: petId + description: ID of pet to return + schema: + format: int64 + type: integer + in: path + required: true + - + name: verbose + description: Output details + schema: + type: boolean + in: query + required: false + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: successful operation + '400': + description: Invalid ID supplied + '404': + description: Pet not found + operationId: getPetById + summary: Find pet by ID + description: Returns a single pet + delete: + tags: + - pet + parameters: + - + name: api_key + schema: + type: string + in: header + required: false + - + name: petId + description: Pet id to delete + schema: + format: int64 + type: integer + in: path + required: true + responses: + '204': + description: No content + '400': + description: Invalid ID supplied + '404': + description: Pet not found + operationId: deletePet + summary: Deletes a pet + description: '' +components: + schemas: + Category: + type: object + properties: + id: + format: int64 + type: integer + name: + type: string + xml: + name: Category + Tag: + type: object + properties: + id: + format: int64 + type: integer + name: + type: string + xml: + name: Tag + Pet: + required: + - category + - name + - status + type: object + properties: + id: + format: int64 + type: integer + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + items: + type: string + xml: + name: photoUrl + wrapped: true + tags: + type: array + items: + $ref: '#/components/schemas/Tag' + xml: + name: tag + wrapped: true + status: + description: pet status in the store + enum: + - available + - pending + - sold + type: string + xml: + name: Pet + ApiResponse: + type: object + properties: + code: + format: int32 + type: integer + type: + type: string + message: + type: string +tags: + - + name: pet + description: Everything about your Pets diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/xml/openapi-client-test.xml b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/xml/openapi-client-test.xml new file mode 100644 index 0000000000..dc2ac52af3 --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/xml/openapi-client-test.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/xml/openapi-server-test.xml b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/xml/openapi-server-test.xml new file mode 100644 index 0000000000..31c637345f --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/xml/openapi-server-test.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/yaml/openapi-client-test.yaml b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/yaml/openapi-client-test.yaml new file mode 100644 index 0000000000..b674d6cf29 --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/yaml/openapi-client-test.yaml @@ -0,0 +1,32 @@ +name: "OpenApiClientTest" +author: "Christoph" +status: "FINAL" +variables: + - name: "petstoreSpec" + value: classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml + - name: "petId" + value: "12345" +actions: + - openapi: + specification: ${petstoreSpec} + client: "httpClient" + sendRequest: + operation: getPetById + - openapi: + specification: ${petstoreSpec} + client: "httpClient" + receiveResponse: + operation: getPetById + status: 200 + + - openapi: + specification: ${petstoreSpec} + client: "httpClient" + sendRequest: + operation: addPet + - openapi: + specification: ${petstoreSpec} + client: "httpClient" + receiveResponse: + operation: addPet + status: 201 diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/yaml/openapi-server-test.yaml b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/yaml/openapi-server-test.yaml new file mode 100644 index 0000000000..9a6cefe680 --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/yaml/openapi-server-test.yaml @@ -0,0 +1,33 @@ +name: "OpenApiServerTest" +author: "Christoph" +status: "FINAL" +variables: + - name: "petstoreSpec" + value: classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml + - name: "petId" + value: "12345" +actions: + - openapi: + specification: ${petstoreSpec} + server: "httpServer" + receiveRequest: + operation: getPetById + - openapi: + specification: ${petstoreSpec} + server: "httpServer" + sendResponse: + operation: getPetById + status: 200 + + - openapi: + specification: ${petstoreSpec} + server: "httpServer" + receiveRequest: + operation: addPet + timeout: 2000 + - openapi: + specification: ${petstoreSpec} + server: "httpServer" + sendResponse: + operation: addPet + status: 201 diff --git a/connectors/citrus-selenium/src/test/java/org/citrusframework/selenium/UnitTestSupport.java b/connectors/citrus-selenium/src/test/java/org/citrusframework/selenium/UnitTestSupport.java index ced9db2d78..fd97be9d29 100644 --- a/connectors/citrus-selenium/src/test/java/org/citrusframework/selenium/UnitTestSupport.java +++ b/connectors/citrus-selenium/src/test/java/org/citrusframework/selenium/UnitTestSupport.java @@ -2,8 +2,6 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.annotations.BeforeMethod; /** @@ -24,9 +22,6 @@ public void prepareTest() { } protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = TestContextFactory.newInstance(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } } diff --git a/connectors/citrus-selenium/src/test/java/org/citrusframework/selenium/actions/AlertActionTest.java b/connectors/citrus-selenium/src/test/java/org/citrusframework/selenium/actions/AlertActionTest.java index c5384ec291..b355ae82c8 100644 --- a/connectors/citrus-selenium/src/test/java/org/citrusframework/selenium/actions/AlertActionTest.java +++ b/connectors/citrus-selenium/src/test/java/org/citrusframework/selenium/actions/AlertActionTest.java @@ -19,10 +19,8 @@ import org.citrusframework.context.TestContextFactory; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.ValidationException; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.selenium.endpoint.SeleniumBrowser; import org.citrusframework.testng.AbstractTestNGUnitTest; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.mockito.Mockito; import org.openqa.selenium.Alert; import org.openqa.selenium.WebDriver; @@ -47,10 +45,7 @@ public class AlertActionTest extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } @BeforeMethod diff --git a/connectors/citrus-sql/src/test/java/org/citrusframework/UnitTestSupport.java b/connectors/citrus-sql/src/test/java/org/citrusframework/UnitTestSupport.java index 12aa053688..e008a7d1cb 100644 --- a/connectors/citrus-sql/src/test/java/org/citrusframework/UnitTestSupport.java +++ b/connectors/citrus-sql/src/test/java/org/citrusframework/UnitTestSupport.java @@ -1,9 +1,7 @@ package org.citrusframework; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.testng.AbstractTestNGUnitTest; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; /** * @author Christoph Deppisch @@ -12,9 +10,6 @@ public abstract class UnitTestSupport extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } } diff --git a/connectors/pom.xml b/connectors/pom.xml index 1d7d8a8226..0f2c263d38 100644 --- a/connectors/pom.xml +++ b/connectors/pom.xml @@ -15,6 +15,7 @@ pom + citrus-openapi citrus-docker citrus-kubernetes citrus-selenium diff --git a/core/citrus-api/src/main/java/org/citrusframework/validation/DefaultTextEqualsMessageValidator.java b/core/citrus-api/src/main/java/org/citrusframework/validation/DefaultTextEqualsMessageValidator.java index 277e0fe5c5..e32ede32bc 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/validation/DefaultTextEqualsMessageValidator.java +++ b/core/citrus-api/src/main/java/org/citrusframework/validation/DefaultTextEqualsMessageValidator.java @@ -63,10 +63,28 @@ public void validateMessage(Message receivedMessage, Message controlMessage, } if (!receivedPayload.equals(controlPayload)) { - throw new ValidationException("Validation failed - message payload not equal!"); + throw new ValidationException("Validation failed - message payload not equal " + getFirstDiff(receivedPayload, controlPayload)); } } + public String getFirstDiff(String received, String control) { + int position; + for (position = 0; position < received.length() && position < control.length(); position++) { + if (received.charAt(position) != control.charAt(position)) { + break; + } + } + + if (position < control.length() || position < received.length()) { + int controlEnd = Math.min(position + 25, control.length()); + int receivedEnd = Math.min(position + 25, received.length()); + + return String.format("at position %d expected '%s', but was '%s'", position + 1, control.substring(position, controlEnd), received.substring(position, receivedEnd)); + } + + return ""; + } + public DefaultTextEqualsMessageValidator normalizeLineEndings() { this.normalizeLineEndings = true; return this; 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 f54866a961..b41155fbfa 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 @@ -57,7 +57,6 @@ */ public class SendMessageAction extends AbstractTestAction implements Completable { - private static final String REPORT_ENABLED_ENV = "CITRUS_SUMMARY_REPORT_ENABLED"; /** Message endpoint instance */ private final Endpoint endpoint; diff --git a/core/citrus-base/src/main/java/org/citrusframework/context/TestContextFactory.java b/core/citrus-base/src/main/java/org/citrusframework/context/TestContextFactory.java index 3f134b5c0e..73d5eaa9a9 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/context/TestContextFactory.java +++ b/core/citrus-base/src/main/java/org/citrusframework/context/TestContextFactory.java @@ -26,6 +26,7 @@ import org.citrusframework.container.BeforeTest; import org.citrusframework.endpoint.DefaultEndpointFactory; import org.citrusframework.endpoint.EndpointFactory; +import org.citrusframework.functions.DefaultFunctionRegistry; import org.citrusframework.functions.FunctionRegistry; import org.citrusframework.log.DefaultLogModifier; import org.citrusframework.log.LogModifier; @@ -37,7 +38,9 @@ import org.citrusframework.spi.ReferenceResolverAware; import org.citrusframework.spi.SimpleReferenceResolver; import org.citrusframework.util.TypeConverter; +import org.citrusframework.validation.DefaultMessageValidatorRegistry; import org.citrusframework.validation.MessageValidatorRegistry; +import org.citrusframework.validation.matcher.DefaultValidationMatcherRegistry; import org.citrusframework.validation.matcher.ValidationMatcherRegistry; import org.citrusframework.variable.GlobalVariables; import org.citrusframework.variable.SegmentVariableExtractorRegistry; @@ -90,10 +93,10 @@ public class TestContextFactory implements ReferenceResolverAware { public static TestContextFactory newInstance() { TestContextFactory factory = new TestContextFactory(); - factory.setFunctionRegistry(new FunctionRegistry()); - factory.setValidationMatcherRegistry(new ValidationMatcherRegistry()); + factory.setFunctionRegistry(new DefaultFunctionRegistry()); + factory.setValidationMatcherRegistry(new DefaultValidationMatcherRegistry()); factory.setGlobalVariables(new GlobalVariables()); - factory.setMessageValidatorRegistry(new MessageValidatorRegistry()); + factory.setMessageValidatorRegistry(new DefaultMessageValidatorRegistry()); factory.setTestListeners(new TestListeners()); factory.setTestActionListeners(new TestActionListeners()); factory.setMessageListeners(new MessageListeners()); diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultMessageValidatorRegistry.java b/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultMessageValidatorRegistry.java index a2353f66e0..e528d2634f 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultMessageValidatorRegistry.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultMessageValidatorRegistry.java @@ -12,7 +12,6 @@ public class DefaultMessageValidatorRegistry extends MessageValidatorRegistry { * Default constructor adds message validator implementations from resource path lookup. */ public DefaultMessageValidatorRegistry() { - MessageValidator.lookup().forEach(this::addMessageValidator); SchemaValidator.lookup().forEach(this::addSchemaValidator); } diff --git a/core/citrus-base/src/test/java/org/citrusframework/UnitTestSupport.java b/core/citrus-base/src/test/java/org/citrusframework/UnitTestSupport.java index 57b2a79338..f98d007821 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/UnitTestSupport.java +++ b/core/citrus-base/src/test/java/org/citrusframework/UnitTestSupport.java @@ -2,8 +2,6 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.annotations.BeforeMethod; /** @@ -24,9 +22,6 @@ public void prepareTest() { } protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = TestContextFactory.newInstance(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } } diff --git a/core/citrus-base/src/test/java/org/citrusframework/validation/DefaultTextEqualsMessageValidatorTest.java b/core/citrus-base/src/test/java/org/citrusframework/validation/DefaultTextEqualsMessageValidatorTest.java index 67d705b6e6..5f3caa9377 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/validation/DefaultTextEqualsMessageValidatorTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/validation/DefaultTextEqualsMessageValidatorTest.java @@ -23,6 +23,7 @@ import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; import org.citrusframework.validation.context.DefaultValidationContext; +import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -50,6 +51,19 @@ public void testValidateError(Object received, Object control) throws Exception validator.validateMessage(receivedMessage, controlMessage, context, validationContext); } + @Test + public void testFirstDiff() { + Assert.assertEquals(validator.getFirstDiff("Hello", "Hello"), ""); + Assert.assertEquals(validator.getFirstDiff("Hello", "Hi"), "at position 2 expected 'i', but was 'ello'"); + Assert.assertEquals(validator.getFirstDiff("Hello bar", "Hello foo"), "at position 7 expected 'foo', but was 'bar'"); + Assert.assertEquals(validator.getFirstDiff("Hello foo, how are you doing!", "Hello foo, how are you doing?"), "at position 29 expected '?', but was '!'"); + Assert.assertEquals(validator.getFirstDiff("Hello foo, how are you doing!", "Hello foo, how are you doing"), "at position 29 expected '', but was '!'"); + Assert.assertEquals(validator.getFirstDiff("Hello foo, how are you doing", "Hello foo, how are you doing!"), "at position 29 expected '!', but was ''"); + Assert.assertEquals(validator.getFirstDiff("1", "2"), "at position 1 expected '2', but was '1'"); + Assert.assertEquals(validator.getFirstDiff("1234", "1243"), "at position 3 expected '43', but was '34'"); + Assert.assertEquals(validator.getFirstDiff("nospacesatall", "no spaces at all"), "at position 3 expected ' spaces at all', but was 'spacesatall'"); + } + @DataProvider private Object[][] successTests() { return new Object[][] { diff --git a/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/UnitTestSupport.java b/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/UnitTestSupport.java index e460a8a96f..07404cf0e4 100644 --- a/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/UnitTestSupport.java +++ b/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/UnitTestSupport.java @@ -2,8 +2,6 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.annotations.BeforeMethod; /** @@ -24,9 +22,6 @@ public void prepareTest() { } protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = TestContextFactory.newInstance(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } } 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 af19a1194d..08bc3cec7d 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 @@ -23,6 +23,7 @@ import org.citrusframework.http.message.HttpMessageBuilder; import org.citrusframework.http.message.HttpMessageUtils; import org.citrusframework.message.Message; +import org.citrusframework.message.MessageBuilder; import org.citrusframework.message.builder.SendMessageBuilderSupport; import org.springframework.http.HttpMethod; import org.springframework.util.MultiValueMap; @@ -34,15 +35,26 @@ public class HttpClientRequestActionBuilder extends SendMessageAction.SendMessageActionBuilder { /** Http message to send or receive */ - private final HttpMessage httpMessage = new HttpMessage(); + private final HttpMessage httpMessage; /** * Default constructor initializes http message. */ public HttpClientRequestActionBuilder() { + this.httpMessage = new HttpMessage(); message(new HttpMessageBuilder(httpMessage)); } + /** + * Subclasses may use custom message builder and Http message. + * @param messageBuilder + * @param httpMessage + */ + protected HttpClientRequestActionBuilder(MessageBuilder messageBuilder, HttpMessage httpMessage) { + this.httpMessage = httpMessage; + message(messageBuilder); + } + @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { 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 92b4536c6b..766466e4d9 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 @@ -16,14 +16,15 @@ package org.citrusframework.http.actions; -import jakarta.servlet.http.Cookie; import java.util.Optional; +import jakarta.servlet.http.Cookie; import org.citrusframework.actions.ReceiveMessageAction; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; import org.citrusframework.http.message.HttpMessageUtils; import org.citrusframework.message.Message; +import org.citrusframework.message.MessageBuilder; import org.citrusframework.message.builder.ReceiveMessageBuilderSupport; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; @@ -35,16 +36,28 @@ public class HttpClientResponseActionBuilder extends ReceiveMessageAction.ReceiveMessageActionBuilder { /** Http message to send or receive */ - private final HttpMessage httpMessage = new HttpMessage(); + private final HttpMessage httpMessage; /** * Default constructor. */ public HttpClientResponseActionBuilder() { + this.httpMessage = new HttpMessage(); message(new HttpMessageBuilder(httpMessage)) .headerNameIgnoreCase(true); } + /** + * Subclasses may use custom message builder and Http message. + * @param messageBuilder + * @param httpMessage + */ + public HttpClientResponseActionBuilder(MessageBuilder messageBuilder, HttpMessage httpMessage) { + this.httpMessage = httpMessage; + message(messageBuilder) + .headerNameIgnoreCase(true); + } + @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { 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 9723023955..75a47150eb 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 @@ -25,6 +25,7 @@ import org.citrusframework.http.message.HttpMessageUtils; import org.citrusframework.http.message.HttpQueryParamHeaderValidator; import org.citrusframework.message.Message; +import org.citrusframework.message.MessageBuilder; import org.citrusframework.message.builder.ReceiveMessageBuilderSupport; import org.springframework.http.HttpMethod; import org.springframework.util.MultiValueMap; @@ -36,17 +37,30 @@ public class HttpServerRequestActionBuilder extends ReceiveMessageAction.ReceiveMessageActionBuilder { /** Http message to send or receive */ - private final HttpMessage httpMessage = new HttpMessage(); + private final HttpMessage httpMessage; /** * Default constructor. */ public HttpServerRequestActionBuilder() { + this.httpMessage = new HttpMessage(); message(new HttpMessageBuilder(httpMessage)) .headerNameIgnoreCase(true); validator(new HttpQueryParamHeaderValidator()); } + /** + * Subclasses may use custom message builder and Http message. + * @param messageBuilder + * @param httpMessage + */ + public HttpServerRequestActionBuilder(MessageBuilder messageBuilder, HttpMessage httpMessage) { + this.httpMessage = httpMessage; + message(messageBuilder) + .headerNameIgnoreCase(true); + validator(new HttpQueryParamHeaderValidator()); + } + @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { 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 02e68e034e..a21be0be13 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 @@ -23,6 +23,7 @@ import org.citrusframework.http.message.HttpMessageBuilder; import org.citrusframework.http.message.HttpMessageUtils; import org.citrusframework.message.Message; +import org.citrusframework.message.MessageBuilder; import org.citrusframework.message.builder.SendMessageBuilderSupport; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; @@ -34,15 +35,26 @@ public class HttpServerResponseActionBuilder extends SendMessageAction.SendMessageActionBuilder { /** Http message to send or receive */ - private final HttpMessage httpMessage = new HttpMessage(); + private final HttpMessage httpMessage; /** * Default constructor. */ public HttpServerResponseActionBuilder() { + this.httpMessage = new HttpMessage(); message(new HttpMessageBuilder(httpMessage)); } + /** + * Subclasses may use custom message builder and Http message. + * @param messageBuilder + * @param httpMessage + */ + public HttpServerResponseActionBuilder(MessageBuilder messageBuilder, HttpMessage httpMessage) { + this.httpMessage = httpMessage; + message(messageBuilder); + } + @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/xml/Http.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/xml/Http.java index 7e6f99ae5a..137752e1ea 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/xml/Http.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/xml/Http.java @@ -397,7 +397,7 @@ private HttpClientActionBuilder asClientBuilder() { } /** - * Converts current builder to client builder. + * Converts current builder to server builder. * @return */ private HttpServerActionBuilder asServerBuilder() { @@ -405,7 +405,7 @@ private HttpServerActionBuilder asServerBuilder() { return (HttpServerActionBuilder) builder; } - throw new CitrusRuntimeException(String.format("Failed to convert '%s' to http client action builder", + throw new CitrusRuntimeException(String.format("Failed to convert '%s' to http server action builder", Optional.ofNullable(builder).map(Object::getClass).map(Class::getName).orElse("null"))); } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/yaml/Http.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/yaml/Http.java index a125a2aa64..0baadf9be7 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/yaml/Http.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/yaml/Http.java @@ -374,7 +374,7 @@ private HttpClientActionBuilder asClientBuilder() { } /** - * Converts current builder to client builder. + * Converts current builder to server builder. * @return */ private HttpServerActionBuilder asServerBuilder() { @@ -382,7 +382,7 @@ private HttpServerActionBuilder asServerBuilder() { return (HttpServerActionBuilder) builder; } - throw new CitrusRuntimeException(String.format("Failed to convert '%s' to http client action builder", + throw new CitrusRuntimeException(String.format("Failed to convert '%s' to http server action builder", Optional.ofNullable(builder).map(Object::getClass).map(Class::getName).orElse("null"))); } diff --git a/endpoints/citrus-http/src/test/java/org/citrusframework/http/UnitTestSupport.java b/endpoints/citrus-http/src/test/java/org/citrusframework/http/UnitTestSupport.java index 280534ee9f..fc922d4a74 100644 --- a/endpoints/citrus-http/src/test/java/org/citrusframework/http/UnitTestSupport.java +++ b/endpoints/citrus-http/src/test/java/org/citrusframework/http/UnitTestSupport.java @@ -2,8 +2,6 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.citrusframework.validation.xml.DomXmlMessageValidator; import org.testng.annotations.BeforeMethod; @@ -26,9 +24,6 @@ public void prepareTest() { protected TestContextFactory createTestContextFactory() { TestContextFactory factory = TestContextFactory.newInstance(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - factory.getMessageValidatorRegistry().addMessageValidator("xml", new DomXmlMessageValidator()); return factory; } diff --git a/endpoints/citrus-http/src/test/java/org/citrusframework/http/message/HttpQueryParamHeaderValidatorTest.java b/endpoints/citrus-http/src/test/java/org/citrusframework/http/message/HttpQueryParamHeaderValidatorTest.java index 41b0610a36..638873325f 100644 --- a/endpoints/citrus-http/src/test/java/org/citrusframework/http/message/HttpQueryParamHeaderValidatorTest.java +++ b/endpoints/citrus-http/src/test/java/org/citrusframework/http/message/HttpQueryParamHeaderValidatorTest.java @@ -20,10 +20,8 @@ import org.citrusframework.context.TestContextFactory; import org.citrusframework.exceptions.ValidationException; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.testng.AbstractTestNGUnitTest; import org.citrusframework.validation.context.HeaderValidationContext; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -40,10 +38,7 @@ public class HttpQueryParamHeaderValidatorTest extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } @Test(dataProvider = "successData") diff --git a/endpoints/citrus-jms/src/test/java/org/citrusframework/jms/UnitTestSupport.java b/endpoints/citrus-jms/src/test/java/org/citrusframework/jms/UnitTestSupport.java index 7fe5d61b42..2284ee5afd 100644 --- a/endpoints/citrus-jms/src/test/java/org/citrusframework/jms/UnitTestSupport.java +++ b/endpoints/citrus-jms/src/test/java/org/citrusframework/jms/UnitTestSupport.java @@ -2,8 +2,6 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.annotations.BeforeMethod; /** @@ -24,9 +22,6 @@ public void prepareTest() { } protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = TestContextFactory.newInstance(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } } diff --git a/endpoints/citrus-spring-integration/src/test/java/org/citrusframework/UnitTestSupport.java b/endpoints/citrus-spring-integration/src/test/java/org/citrusframework/UnitTestSupport.java index 12aa053688..e008a7d1cb 100644 --- a/endpoints/citrus-spring-integration/src/test/java/org/citrusframework/UnitTestSupport.java +++ b/endpoints/citrus-spring-integration/src/test/java/org/citrusframework/UnitTestSupport.java @@ -1,9 +1,7 @@ package org.citrusframework; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.testng.AbstractTestNGUnitTest; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; /** * @author Christoph Deppisch @@ -12,9 +10,6 @@ public abstract class UnitTestSupport extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } } diff --git a/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/UnitTestSupport.java b/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/UnitTestSupport.java index c6d77a6b89..9f738f95fc 100644 --- a/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/UnitTestSupport.java +++ b/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/UnitTestSupport.java @@ -1,10 +1,8 @@ package org.citrusframework.ws; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.testng.AbstractTestNGUnitTest; import org.citrusframework.validation.DefaultTextEqualsMessageValidator; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.citrusframework.validation.xml.DomXmlMessageValidator; /** @@ -15,9 +13,6 @@ public abstract class UnitTestSupport extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - factory.getMessageValidatorRegistry().addMessageValidator("xml", new DomXmlMessageValidator()); factory.getMessageValidatorRegistry().addMessageValidator("text", new DefaultTextEqualsMessageValidator()); return factory; diff --git a/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/groovy/SoapServerTest.java b/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/groovy/SoapServerTest.java index 2b83ba3340..addf35216e 100644 --- a/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/groovy/SoapServerTest.java +++ b/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/groovy/SoapServerTest.java @@ -89,7 +89,7 @@ public void shouldLoadSoapServerActions() throws IOException { context.getReferenceResolver().bind("soapAttachmentValidator", new SimpleSoapAttachmentValidator()); context.getReferenceResolver().bind("mySoapAttachmentValidator", new SimpleSoapAttachmentValidator()); - endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("MySoapAttachment", "text/plain", "This is an attachment!"))); + endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("MySoapAttachment", "text/plain", "This is an attachment!")).soapAction("myAction")); endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("MySoapAttachment", "application/xml", FileUtils.readToString(FileUtils.getFileResource("classpath:org/citrusframework/ws/actions/test-attachment.xml")), "UTF-8"))); endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("FirstSoapAttachment", "text/plain", "This is an attachment!"), diff --git a/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/xml/SoapServerTest.java b/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/xml/SoapServerTest.java index 256f12d4fc..81fca03c01 100644 --- a/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/xml/SoapServerTest.java +++ b/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/xml/SoapServerTest.java @@ -89,7 +89,7 @@ public void shouldLoadSoapServerActions() throws IOException { context.getReferenceResolver().bind("soapAttachmentValidator", new SimpleSoapAttachmentValidator()); context.getReferenceResolver().bind("mySoapAttachmentValidator", new SimpleSoapAttachmentValidator()); - endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("MySoapAttachment", "text/plain", "This is an attachment!"))); + endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("MySoapAttachment", "text/plain", "This is an attachment!")).soapAction("myAction")); endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("MySoapAttachment", "application/xml", FileUtils.readToString(FileUtils.getFileResource("classpath:org/citrusframework/ws/actions/test-attachment.xml")), "UTF-8"))); endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("FirstSoapAttachment", "text/plain", "This is an attachment!"), diff --git a/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/yaml/SoapServerTest.java b/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/yaml/SoapServerTest.java index f442d22486..d9036ad7ea 100644 --- a/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/yaml/SoapServerTest.java +++ b/endpoints/citrus-ws/src/test/java/org/citrusframework/ws/yaml/SoapServerTest.java @@ -89,7 +89,7 @@ public void shouldLoadSoapServerActions() throws IOException { context.getReferenceResolver().bind("soapAttachmentValidator", new SimpleSoapAttachmentValidator()); context.getReferenceResolver().bind("mySoapAttachmentValidator", new SimpleSoapAttachmentValidator()); - endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("MySoapAttachment", "text/plain", "This is an attachment!"))); + endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("MySoapAttachment", "text/plain", "This is an attachment!")).soapAction("myAction")); endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("MySoapAttachment", "application/xml", FileUtils.readToString(FileUtils.getFileResource("classpath:org/citrusframework/ws/actions/test-attachment.xml")), "UTF-8"))); endpointAdapter.handleMessage(createSoapMessage(new SoapAttachment("FirstSoapAttachment", "text/plain", "This is an attachment!"), diff --git a/endpoints/citrus-zookeeper/src/test/java/org/citrusframework/zookeeper/UnitTestSupport.java b/endpoints/citrus-zookeeper/src/test/java/org/citrusframework/zookeeper/UnitTestSupport.java index e9cb33e205..425dda3f5c 100644 --- a/endpoints/citrus-zookeeper/src/test/java/org/citrusframework/zookeeper/UnitTestSupport.java +++ b/endpoints/citrus-zookeeper/src/test/java/org/citrusframework/zookeeper/UnitTestSupport.java @@ -2,8 +2,6 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.annotations.BeforeMethod; /** @@ -24,9 +22,6 @@ public void prepareTest() { } protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = TestContextFactory.newInstance(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } } diff --git a/pom.xml b/pom.xml index 1c8e9f5d9f..316e30771a 100644 --- a/pom.xml +++ b/pom.xml @@ -171,6 +171,7 @@ 2.0.2 1.10.14 4.1.0 + 1.1.27 1.8.0 1.76 1.14.9 @@ -554,6 +555,12 @@ ${httpclient.version} + + io.apicurio + apicurio-data-models + ${apicurio.data-models.version} + + org.eclipse.jetty diff --git a/runtime/citrus-groovy/src/test/java/org/citrusframework/UnitTestSupport.java b/runtime/citrus-groovy/src/test/java/org/citrusframework/UnitTestSupport.java index 6488c60180..e47a582ce5 100644 --- a/runtime/citrus-groovy/src/test/java/org/citrusframework/UnitTestSupport.java +++ b/runtime/citrus-groovy/src/test/java/org/citrusframework/UnitTestSupport.java @@ -20,11 +20,9 @@ package org.citrusframework; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.testng.AbstractTestNGUnitTest; import org.citrusframework.validation.DefaultMessageHeaderValidator; import org.citrusframework.validation.DefaultTextEqualsMessageValidator; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; /** * @author Christoph Deppisch @@ -34,9 +32,6 @@ public abstract class UnitTestSupport extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - factory.getMessageValidatorRegistry().addMessageValidator("headerValidator", new DefaultMessageHeaderValidator()); factory.getMessageValidatorRegistry().addMessageValidator("textEqualsMessageValidator", new DefaultTextEqualsMessageValidator()); return factory; diff --git a/runtime/citrus-groovy/src/test/java/org/citrusframework/actions/ReceiveMessageActionTest.java b/runtime/citrus-groovy/src/test/java/org/citrusframework/actions/ReceiveMessageActionTest.java index aeef6559d4..c4b8dde579 100644 --- a/runtime/citrus-groovy/src/test/java/org/citrusframework/actions/ReceiveMessageActionTest.java +++ b/runtime/citrus-groovy/src/test/java/org/citrusframework/actions/ReceiveMessageActionTest.java @@ -22,7 +22,6 @@ import org.citrusframework.context.TestContextFactory; import org.citrusframework.endpoint.Endpoint; import org.citrusframework.endpoint.EndpointConfiguration; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; import org.citrusframework.message.MessageQueue; @@ -35,18 +34,13 @@ import org.citrusframework.validation.MessageValidator; import org.citrusframework.validation.builder.DefaultMessageBuilder; import org.citrusframework.validation.context.ValidationContext; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.testng.Assert; import org.testng.annotations.Test; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyLong; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; /** * @author Christoph Deppisch @@ -71,9 +65,6 @@ protected TestContextFactory createTestContextFactory() { when(validator.supportsMessageType(any(String.class), any(Message.class))).thenReturn(true); TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - factory.getMessageValidatorRegistry().addMessageValidator("validator", validator); factory.getReferenceResolver().bind("mockQueue", mockQueue); diff --git a/runtime/citrus-groovy/src/test/java/org/citrusframework/actions/SendMessageActionTest.java b/runtime/citrus-groovy/src/test/java/org/citrusframework/actions/SendMessageActionTest.java index dcb1f027e7..1c1048cb8b 100644 --- a/runtime/citrus-groovy/src/test/java/org/citrusframework/actions/SendMessageActionTest.java +++ b/runtime/citrus-groovy/src/test/java/org/citrusframework/actions/SendMessageActionTest.java @@ -20,7 +20,6 @@ import org.citrusframework.context.TestContextFactory; import org.citrusframework.endpoint.Endpoint; import org.citrusframework.endpoint.EndpointConfiguration; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; import org.citrusframework.message.builder.script.GroovyFileResourcePayloadBuilder; @@ -31,7 +30,6 @@ import org.citrusframework.validation.DefaultMessageHeaderValidator; import org.citrusframework.validation.builder.DefaultMessageBuilder; import org.citrusframework.validation.context.HeaderValidationContext; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.mockito.Mockito; import org.testng.Assert; import org.testng.annotations.Test; @@ -52,10 +50,7 @@ public class SendMessageActionTest extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } @Test diff --git a/runtime/citrus-testng/src/test/java/org/citrusframework/UnitTestSupport.java b/runtime/citrus-testng/src/test/java/org/citrusframework/UnitTestSupport.java index 3e29c2da5a..32a958ec09 100644 --- a/runtime/citrus-testng/src/test/java/org/citrusframework/UnitTestSupport.java +++ b/runtime/citrus-testng/src/test/java/org/citrusframework/UnitTestSupport.java @@ -2,9 +2,7 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.validation.DefaultTextEqualsMessageValidator; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.annotations.BeforeMethod; /** @@ -26,8 +24,6 @@ public void prepareTest() { protected TestContextFactory createTestContextFactory() { TestContextFactory factory = TestContextFactory.newInstance(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); factory.getMessageValidatorRegistry().addMessageValidator("all", new DefaultTextEqualsMessageValidator()); return factory; } diff --git a/runtime/citrus-xml/src/main/resources/org/citrusframework/schema/xml/testcase/citrus-testcase-4.1.0-SNAPSHOT.xsd b/runtime/citrus-xml/src/main/resources/org/citrusframework/schema/xml/testcase/citrus-testcase-4.1.0-SNAPSHOT.xsd index a60629928a..ae36a304f5 100644 --- a/runtime/citrus-xml/src/main/resources/org/citrusframework/schema/xml/testcase/citrus-testcase-4.1.0-SNAPSHOT.xsd +++ b/runtime/citrus-xml/src/main/resources/org/citrusframework/schema/xml/testcase/citrus-testcase-4.1.0-SNAPSHOT.xsd @@ -707,6 +707,7 @@ + @@ -1202,6 +1203,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/runtime/citrus-xml/src/main/resources/org/citrusframework/schema/xml/testcase/citrus-testcase.xsd b/runtime/citrus-xml/src/main/resources/org/citrusframework/schema/xml/testcase/citrus-testcase.xsd index a60629928a..6ec0fa3997 100644 --- a/runtime/citrus-xml/src/main/resources/org/citrusframework/schema/xml/testcase/citrus-testcase.xsd +++ b/runtime/citrus-xml/src/main/resources/org/citrusframework/schema/xml/testcase/citrus-testcase.xsd @@ -707,6 +707,7 @@ + @@ -1202,6 +1203,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/assembly/dist-antlibs.xml b/src/main/assembly/dist-antlibs.xml index 6566813a96..af6146b6ac 100644 --- a/src/main/assembly/dist-antlibs.xml +++ b/src/main/assembly/dist-antlibs.xml @@ -30,6 +30,7 @@ org.citrusframework:citrus-camel org.citrusframework:citrus-ftp org.citrusframework:citrus-http + org.citrusframework:citrus-openapi org.citrusframework:citrus-jms org.citrusframework:citrus-kafka org.citrusframework:citrus-jmx diff --git a/src/main/assembly/dist-release.xml b/src/main/assembly/dist-release.xml index 5911dad00a..e3e37ea4f6 100644 --- a/src/main/assembly/dist-release.xml +++ b/src/main/assembly/dist-release.xml @@ -26,6 +26,7 @@ org.citrusframework:citrus-camel org.citrusframework:citrus-ftp org.citrusframework:citrus-http + org.citrusframework:citrus-openapi org.citrusframework:citrus-jms org.citrusframework:citrus-kafka org.citrusframework:citrus-jmx @@ -320,6 +321,13 @@ *sources.jar + + connectors/citrus-openapi/target + src + + *sources.jar + + tools/citrus-restdocs/target diff --git a/src/main/assembly/dist-sources.xml b/src/main/assembly/dist-sources.xml index b298cb7da1..b63714d824 100644 --- a/src/main/assembly/dist-sources.xml +++ b/src/main/assembly/dist-sources.xml @@ -263,6 +263,13 @@ *sources.jar + + connectors/citrus-openapi/target + + + *sources.jar + + tools/citrus-restdocs/target diff --git a/src/manual/connector-openapi.adoc b/src/manual/connector-openapi.adoc new file mode 100644 index 0000000000..53333ff1f0 --- /dev/null +++ b/src/manual/connector-openapi.adoc @@ -0,0 +1,311 @@ +[[openapi]] +== OpenAPI support + +https://www.openapis.org/[OpenAPI] is a popular specification language to describe HTTP APIs and its exposure to clients. +Citrus is able to leverage the specification to auto generate client and server request/response message data. +The generated message data follows the rules of a given operation in the specification. +In particular, the message body is generated from the given Json schema rules in that specification. +This way users may do contract-driven testing where the client and the server ensure the conformity with the contract to obey to the same specification rules. + +NOTE: The OpenAPI support in Citrus get enabled by adding a separate Maven module as dependency to your project + +[source,xml] +---- + + org.citrusframework + citrus-openapi + ${citrus.version} + +---- + +[[openapi-specification]] +=== OpenAPI specification + +The OpenAPI test actions in Citrus uses a specification which usually is a json or yaml document shared between the components. + +Sometimes the specification gets exposed by a server application via HTTP endpoint. +You can directly load the specification from the HTTP URL. +Or you may just point the OpenAPI components to a local specification file. + +Citrus supports OpenAPI on both client and server components so the next sections will describe the usage for each of those. + +[[openapi-client]] +=== OpenAPI client + +On the client side Citrus uses the OpenAPI specification to generate a proper HTTP request that is sent to the server. +The user just gives a valid operationId from the specification every thing else is automatically generated. +The Citrus client message will use the proper request path (e.g. `/petstore/v3/pet`) and Content-Type (e.g. `applicaiton/json`) according to the specification rules. + +Of course, you can also validate the HTTP response message with auto generated validation. +The user just gives the expected HTTP status code that is also described in the specification (e.g. 200 OK). +The response data used as expected message content is then also generated from the specification. + +As an example the following OpenAPI specification defines the operation `getPetById`. + +.petstore-v3.yaml +[source,yaml] +---- +openapi: 3.0.2 +info: + title: Petstore + version: 1.0.1 +servers: + - url: 'http://localhost/petstore/v3/' +paths: + '/pet/{petId}': + get: + operationId: getPetById + parameters: + - name: petId + description: ID of pet to return + schema: + format: int64 + type: integer + in: path + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + '404': + description: Pet not found + summary: Find pet by ID + description: Returns a single pet +# ... +---- + +The operation defines the HTTP GET request on `/pet/{petId}` and the response `200` OK that delivers the `#/components/schemas/Pet` Json object to the calling client as a response. + +The Json schema for the pet defines all properties on the object. + +.Pet Json schema +[source,yaml] +---- +Pet: + required: + - category + - name + - status + type: object + properties: + id: + format: int64 + type: integer + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + items: + type: string + tags: + type: array + items: + $ref: '#/components/schemas/Tag' + status: + description: pet status in the store + enum: + - available + - pending + - sold + type: string +# ... +---- + +In a testcase Citrus is able to leverage this information in order to send a proper request and validate the response based on the OpenAPI specification. + +.Java +[source,java,indent=0,role="primary"] +---- +private final HttpClient httpClient = new HttpClientBuilder() + .requestUrl("http://localhost:%d".formatted(port)) + .build(); + +private final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( + Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); + +@CitrusTest +public void openApiClientTest() { + when(openapi(petstoreSpec) + .client(httpClient) + .send("getPetById")); + + then(openapi(petstoreSpec) + .client(httpClient) + .receive("getPetById", HttpStatus.OK)); +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + + + + + + + + + + + +---- + +.YAML +[source,yaml,indent=0,role="secondary"] +---- +name: OpenApiClientTest +variables: + - name: petstoreSpec + value: classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml +actions: + - openapi: + specification: ${petstoreSpec} + client: "httpClient" + sendRequest: + operation: getPetById + - openapi: + specification: ${petstoreSpec} + client: "httpClient" + receiveResponse: + operation: getPetById + status: 200 +---- + +.Spring XML +[source,xml,indent=0,role="secondary"] +---- + + + +---- + +In this very first example The client uses the OpenAPI specification to generate a proper GET HTTP request for the `getPetById` operation. +The request is sent to the server using the request URL path `/petstore/v3/pet/${petId}` as declared in the OpenAPI specification. + +The resulting HTTP response from the server is verified on the client by giving the operationId and the expected status `200`. +The OpenAPI client generates the expected control message from the given Json schema in the OpenAPI specification. + +The generated control message contains validation matchers and expressions as follows. + +.Generated control message body +[source,json] +---- +{ + "id": "@isNumber()@", + "name": "@notEmpty()@", + "category": { + "id": "@isNumber()@", + "name": "@notEmpty()@" + }, + "photoUrls": "@notEmpty()@", + "tags": "@ignore@", + "status": "@matches(sold|pending|available)@" +} +---- + +This control message meets the rules defined by the OpenAPI Json schema specification for the pet object. +For instance the enum field `status` is validated with a matching expression. +In case the OpenAPI specification changes the generated control message will change accordingly. + +This completes the client side OpenAPI support. +Now let's have a closer look at the server side OpenAPI support in the next section. + +[[openapi-server]] +=== OpenAPI server + +On the server side Citrus is able to verify incoming requests based on the OpenAPI specification. +The expected request message content as well as the expected resource URL path and the Content-Type are automatically validated. + +.Java +[source,java,indent=0,role="primary"] +---- +private final HttpServer httpServer = new HttpServerBuilder() + .port(port) + .timeout(5000L) + .autoStart(true) + .defaultStatus(HttpStatus.NO_CONTENT) + .build(); + +private final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( + Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); + +@CitrusTest +public void openApiClientTest() { + when(openapi(petstoreSpec) + .server(httpServer) + .receive("addPet")); + + then(openapi(petstoreSpec) + .server(httpServer) + .send("addPet", HttpStatus.CREATED)); +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + + + + + + + + + + + +---- + +.YAML +[source,yaml,indent=0,role="secondary"] +---- +name: OpenApiClientTest +variables: + - name: petstoreSpec + value: classpath:org/citrusframework/openapi/petstore/petstore-v3.yaml +actions: + - openapi: + specification: ${petstoreSpec} + server: "httpServer" + receiveRequest: + operation: addPet + - openapi: + specification: ${petstoreSpec} + server: "httpServer" + sendResponse: + operation: addPet + status: 200 +---- + +.Spring XML +[source,xml,indent=0,role="secondary"] +---- + + + +---- + +The example above uses the `addPet` operation defined in the OpenAPI specification. +The operation expects a HTTP POST request with a pet object as message payload. +The OpenAPI server generates an expected Json message body according to the specification. +This ensures that the incoming client request meets the Json schema rules for the pet object. +Also, the server will verify the HTTP request method, the Content-Type header as well as the used resource path `/petstore/v3/pet`. + +The given HTTP status code defines the response that should be sent by the server. +The server will generate a proper response according to the OpenAPI specification. +This also includes a potential response message body (e.g. pet object). diff --git a/src/manual/connectors.adoc b/src/manual/connectors.adoc new file mode 100644 index 0000000000..4d1c3b8253 --- /dev/null +++ b/src/manual/connectors.adoc @@ -0,0 +1,9 @@ +[[connectors]] += Connectors + +Connectors generally are quite similar to link:#endpoints[endpoints]. +These modules connect Citrus to a certain technology or framework rather than implementing a message transport (client and server) like endpoints usually do. + +Connectors typically provide a client side only implementation that enable Citrus to interact with a service or framework (e.g. Docker deamon, Selenium web driver, OpenAPI specification). + +include::connector-openapi.adoc[] diff --git a/src/manual/index.adoc b/src/manual/index.adoc index 4d04fc1589..9ae7fccb18 100644 --- a/src/manual/index.adoc +++ b/src/manual/index.adoc @@ -62,6 +62,8 @@ include::endpoint-restdocs.adoc[] include::endpoint-component.adoc[] include::endpoint-adapter.adoc[] +include::connectors.adoc[] + include::functions.adoc[] include::validation-matchers.adoc[] diff --git a/validation/citrus-validation-groovy/src/test/java/org/citrusframework/UnitTestSupport.java b/validation/citrus-validation-groovy/src/test/java/org/citrusframework/UnitTestSupport.java index 57b2a79338..f98d007821 100644 --- a/validation/citrus-validation-groovy/src/test/java/org/citrusframework/UnitTestSupport.java +++ b/validation/citrus-validation-groovy/src/test/java/org/citrusframework/UnitTestSupport.java @@ -2,8 +2,6 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.annotations.BeforeMethod; /** @@ -24,9 +22,6 @@ public void prepareTest() { } protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = TestContextFactory.newInstance(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } } diff --git a/validation/citrus-validation-groovy/src/test/java/org/citrusframework/validation/script/ReceiveMessageActionTest.java b/validation/citrus-validation-groovy/src/test/java/org/citrusframework/validation/script/ReceiveMessageActionTest.java index 40f592760e..720222e02f 100644 --- a/validation/citrus-validation-groovy/src/test/java/org/citrusframework/validation/script/ReceiveMessageActionTest.java +++ b/validation/citrus-validation-groovy/src/test/java/org/citrusframework/validation/script/ReceiveMessageActionTest.java @@ -26,7 +26,6 @@ import org.citrusframework.context.TestContextFactory; import org.citrusframework.endpoint.Endpoint; import org.citrusframework.endpoint.EndpointConfiguration; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; import org.citrusframework.message.MessageQueue; @@ -42,18 +41,12 @@ import org.citrusframework.validation.MessageValidatorRegistry; import org.citrusframework.validation.builder.DefaultMessageBuilder; import org.citrusframework.validation.context.ValidationContext; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.testng.annotations.Test; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyLong; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; /** * @author Christoph Deppisch @@ -77,9 +70,6 @@ protected TestContextFactory createTestContextFactory() { MockitoAnnotations.openMocks(this); TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - factory.getMessageValidatorRegistry().addMessageValidator("header", new DefaultMessageHeaderValidator()); factory.getMessageValidatorRegistry().addMessageValidator("groovyJson", new GroovyJsonMessageValidator()); factory.getMessageValidatorRegistry().addMessageValidator("groovyText", new GroovyScriptMessageValidator()); diff --git a/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/UnitTestSupport.java b/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/UnitTestSupport.java index 3e29c2da5a..32a958ec09 100644 --- a/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/UnitTestSupport.java +++ b/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/UnitTestSupport.java @@ -2,9 +2,7 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.validation.DefaultTextEqualsMessageValidator; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.annotations.BeforeMethod; /** @@ -26,8 +24,6 @@ public void prepareTest() { protected TestContextFactory createTestContextFactory() { TestContextFactory factory = TestContextFactory.newInstance(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); factory.getMessageValidatorRegistry().addMessageValidator("all", new DefaultTextEqualsMessageValidator()); return factory; } diff --git a/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/ValidationUtilsTest.java b/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/ValidationUtilsTest.java index 771b73b326..53d15ee0a2 100644 --- a/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/ValidationUtilsTest.java +++ b/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/ValidationUtilsTest.java @@ -18,9 +18,7 @@ import org.citrusframework.context.TestContextFactory; import org.citrusframework.exceptions.ValidationException; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.testng.AbstractTestNGUnitTest; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.hamcrest.Matchers; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -33,10 +31,7 @@ public class ValidationUtilsTest extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } @Test(dataProvider = "testData") diff --git a/validation/citrus-validation-json/src/test/java/org/citrusframework/UnitTestSupport.java b/validation/citrus-validation-json/src/test/java/org/citrusframework/UnitTestSupport.java index 1663a43f3c..ada9d8ba1b 100644 --- a/validation/citrus-validation-json/src/test/java/org/citrusframework/UnitTestSupport.java +++ b/validation/citrus-validation-json/src/test/java/org/citrusframework/UnitTestSupport.java @@ -5,7 +5,6 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; import org.citrusframework.exceptions.ValidationException; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.message.Message; import org.citrusframework.message.MessageType; import org.citrusframework.testng.AbstractTestNGUnitTest; @@ -14,7 +13,6 @@ import org.citrusframework.validation.context.ValidationContext; import org.citrusframework.validation.json.JsonPathMessageValidator; import org.citrusframework.validation.json.JsonTextMessageValidator; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.testng.Assert; /** @@ -25,9 +23,6 @@ public abstract class UnitTestSupport extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - factory.getMessageValidatorRegistry().addMessageValidator("header", new DefaultMessageHeaderValidator()); factory.getMessageValidatorRegistry().addMessageValidator("json", new JsonTextMessageValidator()); factory.getMessageValidatorRegistry().addMessageValidator("jsonPath", new JsonPathMessageValidator()); diff --git a/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/ReceiveMessageActionTest.java b/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/ReceiveMessageActionTest.java index 280d4a293e..dc9184efe0 100644 --- a/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/ReceiveMessageActionTest.java +++ b/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/ReceiveMessageActionTest.java @@ -26,7 +26,6 @@ import org.citrusframework.endpoint.EndpointConfiguration; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.ValidationException; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; import org.citrusframework.message.MessageQueue; @@ -36,7 +35,6 @@ import org.citrusframework.testng.AbstractTestNGUnitTest; import org.citrusframework.validation.DefaultMessageHeaderValidator; import org.citrusframework.validation.builder.DefaultMessageBuilder; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.testng.Assert; @@ -66,9 +64,6 @@ public class ReceiveMessageActionTest extends AbstractTestNGUnitTest { protected TestContextFactory createTestContextFactory() { MockitoAnnotations.openMocks(this); TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - factory.getMessageValidatorRegistry().addMessageValidator("header", new DefaultMessageHeaderValidator()); factory.getMessageValidatorRegistry().addMessageValidator("json", new JsonTextMessageValidator()); factory.getMessageValidatorRegistry().addMessageValidator("jsonPath", new JsonPathMessageValidator()); 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 63261cc72d..fb1f31449d 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 @@ -25,7 +25,6 @@ import org.citrusframework.context.TestContextFactory; import org.citrusframework.endpoint.Endpoint; import org.citrusframework.endpoint.EndpointConfiguration; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; import org.citrusframework.message.MessageType; @@ -33,10 +32,10 @@ import org.citrusframework.messaging.Producer; import org.citrusframework.testng.AbstractTestNGUnitTest; import org.citrusframework.validation.DefaultMessageHeaderValidator; +import org.citrusframework.validation.MessageValidatorRegistry; import org.citrusframework.validation.SchemaValidator; import org.citrusframework.validation.builder.DefaultMessageBuilder; import org.citrusframework.validation.context.HeaderValidationContext; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.mockito.Mockito; import org.testng.Assert; import org.testng.annotations.Test; @@ -60,9 +59,8 @@ public class SendMessageActionTest extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + factory.setMessageValidatorRegistry(new MessageValidatorRegistry()); + return factory; } @Test @@ -106,14 +104,13 @@ public void testSendJsonMessageWithValidation() { AtomicBoolean validated = new AtomicBoolean(false); - SchemaValidator schemaValidator = mock(SchemaValidator.class); + SchemaValidator schemaValidator = mock(SchemaValidator.class); when(schemaValidator.supportsMessageType(eq("JSON"), any())).thenReturn(true); doAnswer(invocation-> { - Object argument = invocation.getArgument(2); + JsonMessageValidationContext argument = invocation.getArgument(2, JsonMessageValidationContext.class); - Assert.assertTrue(argument instanceof JsonMessageValidationContext); - Assert.assertEquals(((JsonMessageValidationContext)argument).getSchema(), "fooSchema"); - Assert.assertEquals(((JsonMessageValidationContext)argument).getSchemaRepository(), "fooRepository"); + Assert.assertEquals(argument.getSchema(), "fooSchema"); + Assert.assertEquals(argument.getSchemaRepository(), "fooRepository"); validated.set(true); return null; @@ -143,8 +140,6 @@ public void testSendJsonMessageWithValidation() { Assert.assertTrue(validated.get()); } - - private void validateMessageToSend(Message toSend, Message controlMessage) { Assert.assertEquals(toSend.getPayload(String.class).trim(), controlMessage.getPayload(String.class).trim()); DefaultMessageHeaderValidator validator = new DefaultMessageHeaderValidator(); diff --git a/validation/citrus-validation-text/src/test/java/org/citrusframework/validation/text/PlainTextMessageValidatorTest.java b/validation/citrus-validation-text/src/test/java/org/citrusframework/validation/text/PlainTextMessageValidatorTest.java index 02d5f31d50..e2f29bafae 100644 --- a/validation/citrus-validation-text/src/test/java/org/citrusframework/validation/text/PlainTextMessageValidatorTest.java +++ b/validation/citrus-validation-text/src/test/java/org/citrusframework/validation/text/PlainTextMessageValidatorTest.java @@ -22,14 +22,12 @@ import org.citrusframework.context.TestContextFactory; import org.citrusframework.exceptions.ValidationException; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; import org.citrusframework.message.MessageType; import org.citrusframework.testng.AbstractTestNGUnitTest; import org.citrusframework.validation.context.DefaultValidationContext; import org.citrusframework.validation.context.ValidationContext; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.citrusframework.validation.script.ScriptValidationContext; import org.testng.Assert; import org.testng.annotations.Test; @@ -44,12 +42,8 @@ public class PlainTextMessageValidatorTest extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - validator = new PlainTextMessageValidator(); - return factory; + return TestContextFactory.newInstance(); } @Test diff --git a/validation/citrus-validation-xml/src/test/java/org/citrusframework/UnitTestSupport.java b/validation/citrus-validation-xml/src/test/java/org/citrusframework/UnitTestSupport.java index 4010fb8164..c4b79238c6 100644 --- a/validation/citrus-validation-xml/src/test/java/org/citrusframework/UnitTestSupport.java +++ b/validation/citrus-validation-xml/src/test/java/org/citrusframework/UnitTestSupport.java @@ -5,14 +5,12 @@ import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; import org.citrusframework.exceptions.ValidationException; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.message.Message; import org.citrusframework.message.MessageType; import org.citrusframework.testng.AbstractTestNGUnitTest; import org.citrusframework.validation.DefaultMessageHeaderValidator; import org.citrusframework.validation.MessageValidator; import org.citrusframework.validation.context.ValidationContext; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.citrusframework.validation.xhtml.XhtmlMessageValidator; import org.citrusframework.validation.xhtml.XhtmlXpathMessageValidator; import org.citrusframework.validation.xml.DomXmlMessageValidator; @@ -27,9 +25,6 @@ public abstract class UnitTestSupport extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - factory.getMessageValidatorRegistry().addMessageValidator("header", new DefaultMessageHeaderValidator()); factory.getMessageValidatorRegistry().addMessageValidator("xml", new DomXmlMessageValidator()); factory.getMessageValidatorRegistry().addMessageValidator("xpath", new XpathMessageValidator()); diff --git a/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/ReceiveMessageActionTest.java b/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/ReceiveMessageActionTest.java index 763d7fa1e8..a5896ed5f1 100644 --- a/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/ReceiveMessageActionTest.java +++ b/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/ReceiveMessageActionTest.java @@ -28,7 +28,6 @@ import org.citrusframework.endpoint.Endpoint; import org.citrusframework.endpoint.EndpointConfiguration; import org.citrusframework.exceptions.CitrusRuntimeException; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; import org.citrusframework.message.MessageQueue; @@ -39,7 +38,6 @@ import org.citrusframework.testng.AbstractTestNGUnitTest; import org.citrusframework.validation.DefaultMessageHeaderValidator; import org.citrusframework.validation.builder.DefaultMessageBuilder; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; import org.citrusframework.validation.xhtml.XhtmlMessageValidator; import org.citrusframework.validation.xhtml.XhtmlXpathMessageValidator; import org.citrusframework.variable.MessageHeaderVariableExtractor; @@ -72,9 +70,6 @@ public class ReceiveMessageActionTest extends AbstractTestNGUnitTest { protected TestContextFactory createTestContextFactory() { openMocks(this); TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - factory.getMessageValidatorRegistry().addMessageValidator("header", new DefaultMessageHeaderValidator()); factory.getMessageValidatorRegistry().addMessageValidator("xml", new DomXmlMessageValidator()); factory.getMessageValidatorRegistry().addMessageValidator("xpath", new XpathMessageValidator()); 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 8c49073428..a92d23a13b 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 @@ -16,12 +16,15 @@ package org.citrusframework.validation.xml; +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; import org.citrusframework.endpoint.Endpoint; import org.citrusframework.endpoint.EndpointConfiguration; -import org.citrusframework.functions.DefaultFunctionLibrary; import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; import org.citrusframework.message.builder.DefaultPayloadBuilder; @@ -31,20 +34,10 @@ import org.citrusframework.validation.SchemaValidator; import org.citrusframework.validation.builder.DefaultMessageBuilder; import org.citrusframework.validation.context.HeaderValidationContext; -import org.citrusframework.validation.matcher.DefaultValidationMatcherLibrary; 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.Mockito.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; /** * @author Christoph Deppisch @@ -57,10 +50,7 @@ public class SendMessageActionTest extends AbstractTestNGUnitTest { @Override protected TestContextFactory createTestContextFactory() { - TestContextFactory factory = super.createTestContextFactory(); - factory.getFunctionRegistry().addFunctionLibrary(new DefaultFunctionLibrary()); - factory.getValidationMatcherRegistry().addValidationMatcherLibrary(new DefaultValidationMatcherLibrary()); - return factory; + return TestContextFactory.newInstance(); } @Test