diff --git a/catalog/citrus-bom/pom.xml b/catalog/citrus-bom/pom.xml index 16c218700d..0659de9994 100644 --- a/catalog/citrus-bom/pom.xml +++ b/catalog/citrus-bom/pom.xml @@ -238,6 +238,21 @@ citrus-openapi 4.6.0-SNAPSHOT + + org.citrusframework + citrus-test-api-core + 4.6.0-SNAPSHOT + + + org.citrusframework + citrus-test-api-generator-core + 4.6.0-SNAPSHOT + + + org.citrusframework + citrus-test-api-spring + 4.6.0-SNAPSHOT + org.citrusframework citrus-jms diff --git a/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/actions/JBangActionTest.java b/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/actions/JBangActionTest.java index 1a80074d7c..fedf6ff504 100644 --- a/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/actions/JBangActionTest.java +++ b/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/actions/JBangActionTest.java @@ -21,13 +21,23 @@ import org.citrusframework.jbang.UnitTestSupport; import org.citrusframework.spi.Resource; import org.citrusframework.spi.Resources; +import org.citrusframework.util.TestUtils; import org.testng.Assert; +import org.testng.SkipException; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; public class JBangActionTest extends UnitTestSupport { private final Resource helloScript = Resources.fromClasspath("org/citrusframework/jbang/hello.java"); + @BeforeClass + public static void beforeEach() { + if (!TestUtils.isNetworkReachable()) { + throw new SkipException("Test skipped because network is not reachable. We are probably running behind a proxy and JBang download is not possible."); + } + } + @Test public void testScriptOrFile() { JBangAction jbang = new JBangAction.Builder() diff --git a/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/xml/JBangTest.java b/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/xml/JBangTest.java index 07cc310078..c4fbdc6151 100644 --- a/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/xml/JBangTest.java +++ b/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/xml/JBangTest.java @@ -19,13 +19,23 @@ import org.citrusframework.TestCase; import org.citrusframework.TestCaseMetaInfo; import org.citrusframework.jbang.actions.JBangAction; +import org.citrusframework.util.TestUtils; import org.citrusframework.xml.XmlTestLoader; import org.citrusframework.xml.actions.XmlTestActionBuilder; import org.testng.Assert; +import org.testng.SkipException; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; public class JBangTest extends AbstractXmlActionTest { + @BeforeClass + public static void beforeEach() { + if (!TestUtils.isNetworkReachable()) { + throw new SkipException("Test skipped because network is not reachable. We are probably running behind a proxy and JBang download is not possible."); + } + } + @Test public void shouldLoadJBangActions() { XmlTestLoader testLoader = createTestLoader("classpath:org/citrusframework/jbang/xml/jbang-test.xml"); diff --git a/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/yaml/JBangTest.java b/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/yaml/JBangTest.java index 596e61dc2b..03d272eee1 100644 --- a/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/yaml/JBangTest.java +++ b/connectors/citrus-jbang-connector/src/test/java/org/citrusframework/jbang/yaml/JBangTest.java @@ -19,13 +19,23 @@ import org.citrusframework.TestCase; import org.citrusframework.TestCaseMetaInfo; import org.citrusframework.jbang.actions.JBangAction; +import org.citrusframework.util.TestUtils; import org.citrusframework.yaml.YamlTestLoader; import org.citrusframework.yaml.actions.YamlTestActionBuilder; import org.testng.Assert; +import org.testng.SkipException; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; public class JBangTest extends AbstractYamlActionTest { + @BeforeClass + public static void beforeEach() { + if (!TestUtils.isNetworkReachable()) { + throw new SkipException("Test skipped because network is not reachable. We are probably running behind a proxy and JBang download is not possible."); + } + } + @Test public void shouldLoadJBangActions() { YamlTestLoader testLoader = createTestLoader("classpath:org/citrusframework/jbang/yaml/jbang-test.yaml"); diff --git a/connectors/citrus-openapi/pom.xml b/connectors/citrus-openapi/pom.xml index d8b8c6ce27..3dc018d1c3 100644 --- a/connectors/citrus-openapi/pom.xml +++ b/connectors/citrus-openapi/pom.xml @@ -41,11 +41,19 @@ ${project.version} provided - io.apicurio apicurio-data-models + + com.atlassian.oai + swagger-request-validator-core + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.17.0 + diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/AutoFillType.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/AutoFillType.java new file mode 100644 index 0000000000..a842e00cdc --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/AutoFillType.java @@ -0,0 +1,22 @@ +package org.citrusframework.openapi; + +/** + * Enum representing different types of auto-fill behavior for OpenAPI parameters/body. + * This enum defines how missing or required parameters/body should be auto-filled. + */ +public enum AutoFillType { + /** + * No auto-fill will be performed for any parameters/body. + */ + NONE, + + /** + * Auto-fill will be applied only to required parameters/body that are missing. + */ + REQUIRED, + + /** + * Auto-fill will be applied to all parameters/body, whether they are required or not. + */ + ALL +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiConstants.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiConstants.java new file mode 100644 index 0000000000..fd40e3e8f2 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiConstants.java @@ -0,0 +1,41 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +public final class OpenApiConstants { + + public static final String TYPE_ARRAY = "array"; + public static final String TYPE_BOOLEAN = "boolean"; + public static final String TYPE_INTEGER = "integer"; + public static final String TYPE_NUMBER = "number"; + public static final String TYPE_OBJECT = "object"; + public static final String TYPE_STRING = "string"; + + public static final String FORMAT_INT32 = "int32"; + public static final String FORMAT_INT64 = "int64"; + public static final String FORMAT_FLOAT = "float"; + public static final String FORMAT_DOUBLE = "double"; + public static final String FORMAT_DATE = "date"; + public static final String FORMAT_DATE_TIME = "date-time"; + public static final String FORMAT_UUID = "uuid"; + + /** + * Prevent instantiation. + */ + private OpenApiConstants() { + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageHeaders.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageHeaders.java new file mode 100644 index 0000000000..13caac11ac --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageHeaders.java @@ -0,0 +1,22 @@ +package org.citrusframework.openapi; + +import org.citrusframework.message.MessageHeaders; + +public class OpenApiMessageHeaders { + + public static final String OAS_PREFIX = MessageHeaders.PREFIX + "oas_"; + + public static final String OAS_UNIQUE_OPERATION_ID = OAS_PREFIX + "unique_operation_id"; + + public static final String OAS_SPECIFICATION_ID = OAS_PREFIX + "unique_specification_id"; + + public static final String OAS_MESSAGE_TYPE = OAS_PREFIX + "message_type"; + + public static final String RESPONSE_TYPE = OAS_PREFIX + "response"; + + public static final String REQUEST_TYPE = OAS_PREFIX + "request"; + + private OpenApiMessageHeaders() { + // Static access only + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageType.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageType.java new file mode 100644 index 0000000000..e6b747d5ae --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiMessageType.java @@ -0,0 +1,22 @@ +package org.citrusframework.openapi; + +/** + * The {@code OpenApiMessageType} enum defines the types of OpenAPI messages, + * specifically REQUEST and RESPONSE. Each type is associated with a specific + * header name, which is used to identify the type of message in the OpenAPI + * message headers. + */ +public enum OpenApiMessageType { + + REQUEST(OpenApiMessageHeaders.REQUEST_TYPE), RESPONSE(OpenApiMessageHeaders.RESPONSE_TYPE); + + private final String headerName; + + OpenApiMessageType(String headerName) { + this.headerName = headerName; + } + + public String toHeaderName() { + return headerName; + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiRepository.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiRepository.java new file mode 100644 index 0000000000..c346c479c5 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiRepository.java @@ -0,0 +1,213 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import static java.lang.String.format; +import static java.util.Collections.synchronizedList; +import static org.citrusframework.openapi.OpenApiSettings.isNeglectBasePathGlobally; +import static org.citrusframework.openapi.OpenApiSettings.isRequestValidationEnabledGlobally; +import static org.citrusframework.openapi.OpenApiSettings.isResponseValidationEnabledGlobally; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.openapi.validation.OpenApiValidationPolicy; +import org.citrusframework.repository.BaseRepository; +import org.citrusframework.spi.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * OpenApi repository holding a set of {@link OpenApiSpecification} known in the test scope. + * + * @since 4.4.0 + */ +public class OpenApiRepository extends BaseRepository { + + private static final Logger logger = LoggerFactory.getLogger(OpenApiRepository.class); + + private static final String DEFAULT_NAME = "openApiSchemaRepository"; + + /** + * List of specifications + */ + private final List openApiSpecifications = synchronizedList( + new ArrayList<>()); + + /** + * An optional context path, used for each api, without taking into account any + * {@link OpenApiSpecification} specific context path. + */ + private String rootContextPath; + + /** + * Flag to indicate whether the base path of the OpenAPI should be part of the path or not. + */ + private boolean neglectBasePath = isNeglectBasePathGlobally(); + + /** + * Flag to indicate whether OpenAPIs managed by this repository should perform request validation. + */ + private boolean requestValidationEnabled = isRequestValidationEnabledGlobally(); + + /** + * Flag to indicate whether OpenAPIs managed by this repository should perform response validation. + */ + private boolean responseValidationEnabled = isResponseValidationEnabledGlobally(); + + private OpenApiValidationPolicy validationPolicy = OpenApiSettings.getOpenApiValidationPolicy(); + + public OpenApiRepository() { + super(DEFAULT_NAME); + } + + + + public List getOpenApiSpecifications() { + return openApiSpecifications; + } + + public String getRootContextPath() { + return rootContextPath; + } + + public void setRootContextPath(String rootContextPath) { + this.rootContextPath = rootContextPath; + } + + public OpenApiRepository rootContextPath(String rootContextPath) { + setRootContextPath(rootContextPath); + return this; + } + + public boolean isNeglectBasePath() { + return neglectBasePath; + } + + public void setNeglectBasePath(boolean neglectBasePath) { + this.neglectBasePath = neglectBasePath; + } + + public OpenApiRepository neglectBasePath(boolean neglectBasePath) { + setNeglectBasePath(neglectBasePath); + return this; + } + + public boolean isRequestValidationEnabled() { + return requestValidationEnabled; + } + + public void setRequestValidationEnabled(boolean requestValidationEnabled) { + this.requestValidationEnabled = requestValidationEnabled; + } + + public OpenApiRepository requestValidationEnabled(boolean requestValidationEnabled) { + setRequestValidationEnabled(requestValidationEnabled); + return this; + } + + public boolean isResponseValidationEnabled() { + return responseValidationEnabled; + } + + public void setResponseValidationEnabled(boolean responseValidationEnabled) { + this.responseValidationEnabled = responseValidationEnabled; + } + + public OpenApiRepository responseValidationEnabled(boolean responseValidationEnabled) { + setResponseValidationEnabled(responseValidationEnabled); + return this; + } + + public OpenApiValidationPolicy getValidationPolicy() { + return validationPolicy; + } + + public void setValidationPolicy( + OpenApiValidationPolicy validationPolicy) { + this.validationPolicy = validationPolicy; + } + + public OpenApiRepository validationPolicy(OpenApiValidationPolicy openApiValidationPolicy) { + setValidationPolicy(openApiValidationPolicy); + return this; + } + + /** + * Adds an OpenAPI Specification specified by the given resource to the repository. If an alias + * is determined from the resource name, it is added to the specification. + * + * @param openApiResource the resource to add as an OpenAPI specification + */ + @Override + public void addRepository(Resource openApiResource) { + + try { + OpenApiSpecification openApiSpecification = OpenApiSpecification.from(openApiResource, + validationPolicy); + openApiSpecification.setApiRequestValidationEnabled(requestValidationEnabled); + openApiSpecification.setApiResponseValidationEnabled(responseValidationEnabled); + openApiSpecification.setRootContextPath(rootContextPath); + openApiSpecification.neglectBasePath(neglectBasePath); + addRepository(openApiSpecification); + } catch (Exception e) { + logger.error(format("Unable to read OpenApiSpecification from location: %s", openApiResource.getURI())); + throw new CitrusRuntimeException(e); + } + + } + + /** + * Adds the given OpenAPI specification to this repository and invokes all registered + * {@link OpenApiSpecificationProcessor}. + * + * @param openApiSpecification the OpenAPI specification to add to the repository + */ + public void addRepository(OpenApiSpecification openApiSpecification) { + this.openApiSpecifications.add(openApiSpecification); + + OpenApiSpecificationProcessor.lookup() + .values() + .forEach(processor -> processor.process(openApiSpecification)); + } + + public OpenApiRepository locations(List locations) { + setLocations(locations); + return this; + } + + public @Nullable OpenApiSpecification openApi(@NotNull String alias) { + + if (alias.equals(getName())) { + if (openApiSpecifications.size() == 1) { + return openApiSpecifications.get(0); + } else { + throw new IllegalArgumentException( + "The alias matches the repository name, but the repository contains multiple specifications. " + + "Matching a specification by repository name is only allowed if there is exactly one specification in the repository." + ); + } + } + + return getOpenApiSpecifications().stream() + .filter(spec -> spec.getAliases().contains(alias)) + .findFirst() + .orElse(null); + } +} 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 index e2427165b2..38f21954d4 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiResourceLoader.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiResourceLoader.java @@ -16,33 +16,46 @@ package org.citrusframework.openapi; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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.ssl.SSLContexts; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.Resource; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; import java.io.IOException; +import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLConnection; 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; +import static javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier; +import static javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory; +import static org.apache.hc.core5.http.HttpHeaders.ACCEPT; +import static org.apache.hc.core5.http.Method.GET; +import static org.citrusframework.util.FileUtils.getFileResource; +import static org.citrusframework.util.FileUtils.readToString; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; /** * Loads Open API specifications from different locations like file resource or web resource. */ public final class OpenApiResourceLoader { + private static final RawResolver RAW_RESOLVER = new RawResolver(); + + private static final OasResolver OAS_RESOLVER = new OasResolver(); + /** * Prevent instantiation of utility class. */ @@ -52,21 +65,50 @@ private OpenApiResourceLoader() { /** * 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)); + return fromFile(getFileResource(resource), OAS_RESOLVER); } /** - * Loads the specification from a file resource. Either classpath or file system resource path is supported. - * @param resource - * @return + * Loads the specification from an open api string. + */ + public static OasDocument fromString(String openApi) { + return resolve(openApi, OAS_RESOLVER); + } + + /** + * Loads the raw specification from a file resource. Either classpath or file system resource path is supported. + */ + public static String rawFromFile(String resource) { + return fromFile(getFileResource(resource), + RAW_RESOLVER); + } + + /** + * Loads the raw specification from an open api string. + */ + public static String rawFromString(String openApi) { + return resolve(openApi, RAW_RESOLVER); + } + + /** + * Loads the specification from a resource. */ public static OasDocument fromFile(Resource resource) { + return fromFile(resource, OAS_RESOLVER); + } + + /** + * Loads the raw specification from a resource. + */ + public static String rawFromFile(Resource resource) { + return fromFile(resource, RAW_RESOLVER); + } + + private static T fromFile(Resource resource, Resolver resolver) { try { - return resolve(FileUtils.readToString(resource)); + return resolve(readToString(resource), resolver); } catch (IOException e) { throw new IllegalStateException("Failed to parse Open API specification: " + resource, e); } @@ -74,82 +116,153 @@ public static OasDocument fromFile(Resource resource) { /** * Loads specification from given web URL location. - * @param url - * @return */ public static OasDocument fromWebResource(URL url) { - HttpURLConnection con = null; + return fromWebResource(url, OAS_RESOLVER); + } + + /** + * Loads raw specification from given web URL location. + */ + public static String rawFromWebResource(URL url) { + return fromWebResource(url, RAW_RESOLVER); + } + + private static T fromWebResource(URL url, Resolver resolver) { + URLConnection connection = null; try { - con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod(HttpMethod.GET.name()); - con.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + connection = url.openConnection(); - 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())); + if (connection instanceof HttpURLConnection httpURLConnection) { + httpURLConnection.setRequestMethod(GET.name()); + connection.setRequestProperty(ACCEPT, APPLICATION_JSON_VALUE); + + int status = httpURLConnection.getResponseCode(); + if (status > 299) { + throw new IllegalStateException( + "Failed to retrieve Open API specification: " + url, + new IOException(readToString(httpURLConnection.getErrorStream()))); + } + } + + try (InputStream inputStream = connection.getInputStream()) { + return resolve(readToString(inputStream), resolver); } } catch (IOException e) { - throw new IllegalStateException("Failed to retrieve Open API specification: " + url.toString(), e); + throw new IllegalStateException("Failed to retrieve Open API specification: " + url, e); } finally { - if (con != null) { - con.disconnect(); + if (connection instanceof HttpURLConnection httpURLConnection) { + httpURLConnection.disconnect(); } } } /** * Loads specification from given web URL location using secured Http connection. - * @param url - * @return */ public static OasDocument fromSecuredWebResource(URL url) { + return fromSecuredWebResource(url, OAS_RESOLVER); + } + + /** + * Loads raw specification from given web URL location using secured Http connection. + */ + public static String rawFromSecuredWebResource(URL url) { + return fromSecuredWebResource(url, RAW_RESOLVER); + } + + private static T fromSecuredWebResource(URL url, Resolver resolver) { Objects.requireNonNull(url); - HttpsURLConnection con = null; + HttpsURLConnection connection = null; try { SSLContext sslcontext = SSLContexts .custom() .loadTrustMaterial(TrustAllStrategy.INSTANCE) .build(); - HttpsURLConnection.setDefaultSSLSocketFactory(sslcontext.getSocketFactory()); - HttpsURLConnection.setDefaultHostnameVerifier(NoopHostnameVerifier.INSTANCE); + setDefaultSSLSocketFactory(sslcontext.getSocketFactory()); + setDefaultHostnameVerifier(NoopHostnameVerifier.INSTANCE); - con = (HttpsURLConnection) url.openConnection(); - con.setRequestMethod(HttpMethod.GET.name()); - con.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + connection = (HttpsURLConnection) url.openConnection(); + connection.setRequestMethod(GET.name()); + connection.setRequestProperty(ACCEPT, APPLICATION_JSON_VALUE); - int status = con.getResponseCode(); + int status = connection.getResponseCode(); if (status > 299) { - throw new IllegalStateException("Failed to retrieve Open API specification: " + url.toString(), - new IOException(FileUtils.readToString(con.getErrorStream()))); + throw new IllegalStateException("Failed to retrieve Open API specification: " + url, + new IOException(readToString(connection.getErrorStream()))); } else { - return resolve(FileUtils.readToString(con.getInputStream())); + return resolve(readToString(connection.getInputStream()), resolver); } } 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); + throw new IllegalStateException("Failed to retrieve Open API specification: " + url, e); } finally { - if (con != null) { - con.disconnect(); + if (connection != null) { + connection.disconnect(); } } } - private static OasDocument resolve(String specification) { - if (isJsonSpec(specification)) { - return (OasDocument) Library.readDocumentFromJSONString(specification); + private static T resolve(String openApi, Resolver resolver) { + if (isJsonSpec(openApi)) { + return resolver.resolveFromString(openApi); } - final JsonNode node = OpenApiSupport.json().convertValue(OpenApiSupport.yaml().load(specification), JsonNode.class); - return (OasDocument) Library.readDocument(node); + final JsonNode node = OpenApiSupport.json().convertValue(OpenApiSupport.yaml().load(openApi), JsonNode.class); + return resolver.resolveFromNode(node); } private static boolean isJsonSpec(final String specification) { return specification.trim().startsWith("{"); } + + private interface Resolver { + + T resolveFromString(String specification); + + T resolveFromNode(JsonNode node); + + } + + /** + * {@link Resolver} implementation, that resolves to {@link OasDocument}. + */ + private static class OasResolver implements Resolver { + + @Override + public OasDocument resolveFromString(String specification) { + return (OasDocument) Library.readDocumentFromJSONString(specification); + } + + @Override + public OasDocument resolveFromNode(JsonNode node) { + return (OasDocument) Library.readDocument(node); + } + } + + /** + * {@link Resolver} implementation, that resolves to {@link String}. + */ + private static class RawResolver implements Resolver { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Override + public String resolveFromString(String specification) { + return specification; + } + + @Override + public String resolveFromNode(JsonNode node) { + + try { + return mapper.writeValueAsString(node); + } catch (JsonProcessingException e) { + throw new CitrusRuntimeException("Unable to write OpenApi specification node to string!", e); + } + } + } } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSettings.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSettings.java new file mode 100644 index 0000000000..c984edd1c6 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSettings.java @@ -0,0 +1,105 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import static java.lang.Boolean.parseBoolean; + +import org.citrusframework.openapi.validation.OpenApiValidationPolicy; + +/** + * The {@code OpenApiSettings} class provides configuration settings for enabling or disabling + * OpenAPI request and response validation globally. The settings can be controlled through + * system properties or environment variables. + */ +public class OpenApiSettings { + + public static final String GENERATE_OPTIONAL_FIELDS_PROPERTY = "citrus.openapi.generate.optional.fields"; + public static final String GENERATE_OPTIONAL_FIELDS_ENV = "CITRUS_OPENAPI_GENERATE_OPTIONAL_FIELDS"; + + public static final String REQUEST_VALIDATION_ENABLED_PROPERTY = "citrus.openapi.validation.enabled.request"; + public static final String REQUEST_VALIDATION_ENABLED_ENV = "CITRUS_OPENAPI_VALIDATION_DISABLE_REQUEST"; + + public static final String RESPONSE_VALIDATION_ENABLED_PROPERTY = "citrus.openapi.validation.enabled.response"; + public static final String RESPONSE_VALIDATION_ENABLED_ENV = "CITRUS_OPENAPI_VALIDATION_DISABLE_RESPONSE"; + + public static final String NEGLECT_OPEN_API_BASE_PATH_PROPERTY = "citrus.openapi.neglect.base.path"; + public static final String NEGLECT_OPEN_API_BASE_PATH_ENV = "CITRUS_OPENAPI_NEGLECT_BASE_PATH"; + + public static final String REQUEST_AUTO_FILL_RANDOM_VALUES_PROPERTY = "citrus.openapi.request.fill.random.values"; + public static final String REQUEST_AUTO_FILL_RANDOM_VALUES_ENV = "CITRUS_OPENAPI_REQUEST_FILL_RANDOM_VALUES"; + + public static final String RESPONSE_AUTO_FILL_RANDOM_VALUES_PROPERTY = "citrus.openapi.response.fill.random.values"; + public static final String RESPONSE_AUTO_FILL_RANDOM_VALUES_ENV = "CITRUS_OPENAPI_RESPONSE_FILL_RANDOM_VALUES"; + + public static final String OPEN_API_VALIDATION_POLICY_PROPERTY = "citrus.openapi.validation.policy"; + public static final String OPEN_API_VALIDATION_POLICY_ENV = "CITRUS_OPENAPI_VALIDATION_POLICY"; + + private OpenApiSettings() { + // static access only + } + + public static boolean isGenerateOptionalFieldsGlobally() { + return parseBoolean(System.getProperty(GENERATE_OPTIONAL_FIELDS_PROPERTY, System.getenv(GENERATE_OPTIONAL_FIELDS_ENV) != null ? + System.getenv(GENERATE_OPTIONAL_FIELDS_ENV) : "true")); + } + + public static boolean isRequestValidationEnabledGlobally() { + return parseBoolean(System.getProperty( + REQUEST_VALIDATION_ENABLED_PROPERTY, System.getenv(REQUEST_VALIDATION_ENABLED_ENV) != null ? + System.getenv(REQUEST_VALIDATION_ENABLED_ENV) : "true")); + } + + public static boolean isResponseValidationEnabledGlobally() { + return parseBoolean(System.getProperty( + RESPONSE_VALIDATION_ENABLED_PROPERTY, System.getenv(RESPONSE_VALIDATION_ENABLED_ENV) != null ? + System.getenv(RESPONSE_VALIDATION_ENABLED_ENV) : "true")); + } + + public static boolean isNeglectBasePathGlobally() { + return parseBoolean(System.getProperty( + NEGLECT_OPEN_API_BASE_PATH_PROPERTY, System.getenv(NEGLECT_OPEN_API_BASE_PATH_ENV) != null ? + System.getenv(NEGLECT_OPEN_API_BASE_PATH_ENV) : "false")); + } + + /** + * The default AutoFillType for request is set to REQUIRED to support backwards compatibility. + */ + public static AutoFillType getRequestAutoFillRandomValues() { + return AutoFillType.valueOf(System.getProperty( + REQUEST_AUTO_FILL_RANDOM_VALUES_PROPERTY, System.getenv(REQUEST_AUTO_FILL_RANDOM_VALUES_ENV) != null ? + System.getenv(REQUEST_AUTO_FILL_RANDOM_VALUES_ENV) : AutoFillType.REQUIRED.name())); + } + + /** + * The default AutoFillType for response is set to REQUIRED to support backwards compatibility. + */ + public static AutoFillType getResponseAutoFillRandomValues() { + return AutoFillType.valueOf(System.getProperty( + RESPONSE_AUTO_FILL_RANDOM_VALUES_PROPERTY, System.getenv(RESPONSE_AUTO_FILL_RANDOM_VALUES_ENV) != null ? + System.getenv(RESPONSE_AUTO_FILL_RANDOM_VALUES_ENV) : AutoFillType.REQUIRED.name())); + } + + /** + * The default OpenApiValidationPolicy for OpenAPI parsed for validation purposes. + */ + public static OpenApiValidationPolicy getOpenApiValidationPolicy() { + return OpenApiValidationPolicy.valueOf(System.getProperty( + OPEN_API_VALIDATION_POLICY_PROPERTY, System.getenv(OPEN_API_VALIDATION_POLICY_ENV) != null ? + System.getenv(OPEN_API_VALIDATION_POLICY_ENV) : OpenApiValidationPolicy.REPORT.name())); + } + +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecification.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecification.java index 00c5a1c382..03a7b04880 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecification.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecification.java @@ -16,79 +16,355 @@ package org.citrusframework.openapi; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.emptySet; +import static java.util.Collections.singletonList; +import static java.util.Collections.synchronizedSet; +import static org.citrusframework.openapi.OpenApiSettings.getRequestAutoFillRandomValues; +import static org.citrusframework.openapi.OpenApiSettings.getResponseAutoFillRandomValues; +import static org.citrusframework.openapi.OpenApiSettings.isGenerateOptionalFieldsGlobally; +import static org.citrusframework.openapi.OpenApiSettings.isNeglectBasePathGlobally; +import static org.citrusframework.openapi.OpenApiSettings.isRequestValidationEnabledGlobally; +import static org.citrusframework.openapi.OpenApiSettings.isResponseValidationEnabledGlobally; +import static org.citrusframework.openapi.model.OasModelHelper.getBasePath; +import static org.citrusframework.util.StringUtils.appendSegmentToUrlPath; +import static org.citrusframework.util.StringUtils.hasText; +import static org.citrusframework.util.StringUtils.isEmpty; + +import io.apicurio.datamodels.core.models.Extension; +import io.apicurio.datamodels.core.models.common.Info; +import io.apicurio.datamodels.openapi.models.OasDocument; +import io.apicurio.datamodels.openapi.models.OasOperation; +import io.apicurio.datamodels.openapi.models.OasPathItem; +import java.io.File; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; +import java.net.URLDecoder; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; +import java.util.Map; import java.util.Optional; - -import io.apicurio.datamodels.openapi.models.OasDocument; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import org.apache.commons.codec.digest.DigestUtils; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.http.client.HttpClient; import org.citrusframework.openapi.model.OasModelHelper; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.citrusframework.openapi.util.OpenApiUtils; +import org.citrusframework.openapi.validation.OpenApiValidationContext; +import org.citrusframework.openapi.validation.OpenApiValidationContextLoader; +import org.citrusframework.openapi.validation.OpenApiValidationPolicy; import org.citrusframework.spi.Resource; import org.citrusframework.spi.Resources; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * OpenApi specification resolves URL or local file resources to a specification document. + * The OpenApiSpecification class is responsible for handling the loading and processing of OpenAPI + * specification documents from various sources, such as URLs or local files. It supports the + * extraction and usage of key information from these documents, facilitating the interaction with + * OpenAPI-compliant APIs. + *

+ * The class maintains a set of aliases derived from the OpenAPI document's information. These + * aliases typically include the title of the API and its version, providing easy reference and + * identification. For example, if the OpenAPI document's title is "Sample API" and its version is + * "1.0", the aliases set will include "Sample API" and "Sample API/1.0". + *

+ * Users are responsible for ensuring that the sources provided to this class have unique aliases, + * or at least use the correct alias. If the same API is registered with different versions, all + * versions will likely share the same title alias but can be distinguished by the version alias + * (e.g., "Sample API/1.0" and "Sample API/2.0"). This distinction is crucial to avoid conflicts and + * ensure the correct identification and reference of each OpenAPI specification. Also note, that + * aliases may be added manually or programmatically by + * {@link OpenApiSpecification#addAlias(String)}. */ public class OpenApiSpecification { - /** URL to load the OpenAPI specification */ + private static final Logger logger = LoggerFactory.getLogger(OpenApiSpecification.class); + + private static final String HTTPS = "https"; + private static final String HTTP = "http"; + + /** + * A unique identifier (UID) for this specification at runtime. The UID is generated based on + * the SHA of the OpenAPI document combined with the full context path to which the API is + * attached. + * + * @see OpenApiSpecification#determineUid for detailed information on how the UID is generated. + */ + private String uid; + + private final Set aliases = synchronizedSet(new HashSet<>()); + + /** + * Maps the identifier (id) of an operation to OperationPathAdapters. Two different keys may be + * used for each operation. Refer to + * {@link org.citrusframework.openapi.OpenApiSpecification#storeOperationPathAdapter} for more + * details. + */ + private final Map operationIdToOperationPathAdapter = new ConcurrentHashMap<>(); + + /** + * Stores the unique identifier (uniqueId) of an operation, derived from its HTTP method and + * path. This identifier can always be determined and is therefore safe to use, even for + * operations without an optional operationId defined. + */ + private final Map operationToUniqueId = new ConcurrentHashMap<>(); + + /** + * URL to load the OpenAPI specification + */ private String specUrl; - private String httpClient; private String requestUrl; + /** + * Flag indicating whether the base path should be excluded when constructing the complete + * operation path. + *

+ * If set to {@code true}, the base path will be omitted from the final URL path construction. + * This allows for more flexible path handling where the base path is not required. + *

+ * + * @see #getFullPath(OasPathItem) for the method affected by this flag + */ + private boolean neglectBasePath = isNeglectBasePathGlobally(); + + /** + * The optional root context path to which the OpenAPI is hooked. + *

+ * This path is prepended to the base path specified in the OpenAPI configuration. If no root + * context path is specified, only the base path and additional segments are used when + * constructing the complete URL path. + *

+ * + * @see #neglectBasePath for information on excluding the base path + * @see #getFullPath(OasPathItem) for how this path is used in constructing the full operation + * path + */ + private String rootContextPath; + private OasDocument openApiDoc; + private OpenApiValidationContext openApiValidationContext; + + /** + * Generate optional attributes when generating random schema objects. + */ + private boolean generateOptionalFields = isGenerateOptionalFieldsGlobally(); + + /** + * Autofill parameters and body of request with random data. + */ + private AutoFillType requestAutoFill = getRequestAutoFillRandomValues(); + + /** + * Autofill parameters and body of response with random data. + */ + private AutoFillType responseAutoFill = getResponseAutoFillRandomValues(); + + /** + * Flag to indicate, whether request validation is enabled on api level. Api level overrules + * global level and may be overruled by request level. + */ + + private boolean apiRequestValidationEnabled = isRequestValidationEnabledGlobally(); + + /** + * Flag to indicate, whether response validation is enabled on api level. Api level overrules + * global level and may be overruled by request level. + */ + private boolean apiResponseValidationEnabled = isResponseValidationEnabledGlobally(); + + /** + * The policy that determines how OpenAPI validation errors are handled. + */ + private final OpenApiValidationPolicy openApiValidationPolicy; + + public OpenApiSpecification() { + this(OpenApiSettings.getOpenApiValidationPolicy()); + } - private boolean generateOptionalFields = true; + public OpenApiSpecification(OpenApiValidationPolicy openApiValidationPolicy) { + this.openApiValidationPolicy = openApiValidationPolicy; + } - private boolean validateOptionalFields = true; + /** + * Creates an OpenAPI specification instance from the given URL applying the default validation + * policy. + * + * @param specUrl the URL pointing to the OpenAPI specification to load + * @return an OpenApiSpecification instance populated with the document and validation context + */ public static OpenApiSpecification from(String specUrl) { - OpenApiSpecification specification = new OpenApiSpecification(); + return from(specUrl, OpenApiSettings.getOpenApiValidationPolicy()); + } + + /** + * Creates an OpenAPI specification instance from the given url string. + * + * @param specUrl the URL pointing to the OpenAPI specification to load + * @param openApiValidationPolicy the validation policy to apply to the loaded OpenApi + * @return an OpenApiSpecification instance populated with the document and validation context + */ + public static OpenApiSpecification from(String specUrl, + OpenApiValidationPolicy openApiValidationPolicy) { + OpenApiSpecification specification = new OpenApiSpecification(openApiValidationPolicy); specification.setSpecUrl(specUrl); + try { + determineUrlAlias(new URL(specUrl)).ifPresent(specification::addAlias); + } catch (MalformedURLException e) { + // Ignore; + } + return specification; } + /** + * Creates an OpenAPI specification instance from the given URL applying the default validation + * policy. + * + * @param specUrl the URL pointing to the OpenAPI specification to load + * @return an OpenApiSpecification instance populated with the document and validation context + */ public static OpenApiSpecification from(URL specUrl) { - OpenApiSpecification specification = new OpenApiSpecification(); + return from(specUrl, OpenApiSettings.getOpenApiValidationPolicy()); + } + + /** + * Creates an OpenAPI specification instance from the given URL. + * + * @param specUrl the URL pointing to the OpenAPI specification to load + * @param openApiValidationPolicy the validation policy to apply to the loaded OpenApi + * @return an OpenApiSpecification instance populated with the document and validation context + */ + public static OpenApiSpecification from(URL specUrl, + OpenApiValidationPolicy openApiValidationPolicy) { + OpenApiSpecification specification = new OpenApiSpecification(openApiValidationPolicy); OasDocument openApiDoc; - if (specUrl.getProtocol().startsWith("https")) { + OpenApiValidationContext openApiValidationContext; + if (specUrl.getProtocol().startsWith(HTTPS)) { openApiDoc = OpenApiResourceLoader.fromSecuredWebResource(specUrl); } else { openApiDoc = OpenApiResourceLoader.fromWebResource(specUrl); } + openApiValidationContext = OpenApiValidationContextLoader.fromSpec( + OasModelHelper.toJson(openApiDoc), openApiValidationPolicy); + specification.setOpenApiValidationContext(openApiValidationContext); + + determineUrlAlias(specUrl).ifPresent(specification::addAlias); + specification.setSpecUrl(specUrl.toString()); + specification.initPathLookups(); specification.setOpenApiDoc(openApiDoc); - specification.setRequestUrl(String.format("%s://%s%s%s", specUrl.getProtocol(), specUrl.getHost(), specUrl.getPort() > 0 ? ":" + specUrl.getPort() : "", OasModelHelper.getBasePath(openApiDoc))); + specification.setRequestUrl( + format("%s://%s%s%s", specUrl.getProtocol(), specUrl.getHost(), + specUrl.getPort() > 0 ? ":" + specUrl.getPort() : "", + specification.rootContextPath)); return specification; } + /** + * Creates an OpenAPI specification instance from the specified resource, applying the default + * validation strategy. + * + * @param resource the file resource containing the OpenAPI specification to load + * @return an OpenApiSpecification instance populated with the document and validation context + */ public static OpenApiSpecification from(Resource resource) { - OpenApiSpecification specification = new OpenApiSpecification(); + return from(resource, OpenApiSettings.getOpenApiValidationPolicy()); + } + + /** + * Creates an OpenAPI specification instance from the specified resource. + * + * @param resource the file resource containing the OpenAPI specification to + * load + * @param openApiValidationPolicy the validation policy to apply to the loaded OpenApi + * @return an OpenApiSpecification instance populated with the document and validation context + */ + public static OpenApiSpecification from(Resource resource, + OpenApiValidationPolicy openApiValidationPolicy) { + OpenApiSpecification specification = new OpenApiSpecification(openApiValidationPolicy); + OasDocument openApiDoc = OpenApiResourceLoader.fromFile(resource); + specification.setOpenApiValidationContext( + OpenApiValidationContextLoader.fromSpec(OasModelHelper.toJson(openApiDoc), + openApiValidationPolicy)); specification.setOpenApiDoc(openApiDoc); + determineResourceAlias(resource).ifPresent(specification::addAlias); + String schemeToUse = Optional.ofNullable(OasModelHelper.getSchemes(openApiDoc)) - .orElse(Collections.singletonList("http")) - .stream() - .filter(s -> s.equals("http") || s.equals("https")) - .findFirst() - .orElse("http"); + .orElse(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))); + specification.setRequestUrl( + format("%s://%s%s", schemeToUse, OasModelHelper.getHost(openApiDoc), + getBasePath(openApiDoc))); + + return specification; + } + + /** + * Creates an OpenAPI specification instance from the provided OpenAPI specification string. + * + * @param openApi the OpenAPI specification content as a string + * @return an OpenApiSpecification instance populated with the document and validation context + */ + public static OpenApiSpecification fromString(String openApi) { + OpenApiSpecification specification = new OpenApiSpecification(); + OasDocument openApiDoc = OpenApiResourceLoader.fromString(openApi); + + specification.setOpenApiDoc(openApiDoc); + specification.setOpenApiValidationContext( + OpenApiValidationContextLoader.fromString(openApi)); + + String schemeToUse = Optional.ofNullable(OasModelHelper.getSchemes(openApiDoc)) + .orElse(singletonList(HTTP)) + .stream() + .filter(s -> s.equals(HTTP) || s.equals(HTTPS)) + .findFirst() + .orElse(HTTP); + + specification.setSpecUrl("loaded from memory"); + specification.setRequestUrl( + format("%s://%s%s", schemeToUse, OasModelHelper.getHost(openApiDoc), + specification.rootContextPath)); return specification; } - public OasDocument getOpenApiDoc(TestContext context) { + /** + * Get the UID of this specification. + */ + public String getUid() { + return uid; + } + + /** + * Get the unique id of the given operation. + */ + @SuppressWarnings("unused") + public String getUniqueId(OasOperation oasOperation) { + return operationToUniqueId.get(oasOperation); + } + + public synchronized OasDocument getOpenApiDoc(TestContext context) { if (openApiDoc != null) { return openApiDoc; } @@ -99,52 +375,155 @@ public OasDocument getOpenApiDoc(TestContext context) { 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;; + 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)); + 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); + if (resolvedSpecUrl.startsWith(HTTP)) { + URL specWebResource = toSpecUrl(resolvedSpecUrl); + if (resolvedSpecUrl.startsWith(HTTPS)) { + initApiDoc(() -> OpenApiResourceLoader.fromSecuredWebResource(specWebResource)); + } else { + initApiDoc(() -> OpenApiResourceLoader.fromWebResource(specWebResource)); + } + + if (requestUrl == null) { + setRequestUrl(format("%s://%s%s%s", specWebResource.getProtocol(), + specWebResource.getHost(), + specWebResource.getPort() > 0 ? ":" + specWebResource.getPort() : "", + getBasePath(openApiDoc))); } + } else { - openApiDoc = OpenApiResourceLoader.fromFile(Resources.create(resolvedSpecUrl)); + Resource resource = Resources.create(resolvedSpecUrl); + initApiDoc(() -> OpenApiResourceLoader.fromFile(resource)); 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))); + .orElse(singletonList(HTTP)) + .stream() + .filter(s -> s.equals(HTTP) || s.equals(HTTPS)) + .findFirst() + .orElse(HTTP); + + setRequestUrl( + format("%s://%s%s", schemeToUse, OasModelHelper.getHost(openApiDoc), + getBasePath(openApiDoc))); } } } + setOpenApiValidationContext( + OpenApiValidationContextLoader.fromSpec(OasModelHelper.toJson(openApiDoc), + openApiValidationPolicy)); + return openApiDoc; } - public void setOpenApiDoc(OasDocument openApiDoc) { - this.openApiDoc = openApiDoc; + public OpenApiValidationContext getOpenApiValidationContext() { + return openApiValidationContext; + } + + private void setOpenApiValidationContext(OpenApiValidationContext openApiValidationContext) { + this.openApiValidationContext = openApiValidationContext; + this.openApiValidationContext.setResponseValidationEnabled(apiResponseValidationEnabled); + this.openApiValidationContext.setRequestValidationEnabled(apiRequestValidationEnabled); + } + + // exposed for testing + URL toSpecUrl(String resolvedSpecUrl) { + try { + return URI.create(resolvedSpecUrl).toURL(); + } catch (MalformedURLException e) { + throw new IllegalStateException( + "Failed to retrieve Open API specification as web resource: " + resolvedSpecUrl, e); + } + } + + void setOpenApiDoc(OasDocument openApiDoc) { + initApiDoc(() -> openApiDoc); + } + + private void initApiDoc(Supplier openApiDocSupplier) { + this.openApiDoc = openApiDocSupplier.get(); + + this.aliases.addAll(collectAliases(openApiDoc)); + initPathLookups(); + } + + private void initPathLookups() { + if (this.openApiDoc == null) { + return; + } + + determineUid(); + + operationIdToOperationPathAdapter.clear(); + OasModelHelper.visitOasOperations(this.openApiDoc, (oasPathItem, oasOperation) -> { + String path = oasPathItem.getPath(); + + if (isEmpty(path)) { + logger.warn("Skipping path item without path."); + return; + } + + for (Map.Entry operationEntry : OasModelHelper.getOperationMap( + oasPathItem).entrySet()) { + storeOperationPathAdapter(operationEntry.getValue(), oasPathItem); + } + }); + } + + private void determineUid() { + if (uid != null) { + aliases.remove(uid); + } + uid = DigestUtils.sha256Hex(OasModelHelper.toJson(openApiDoc) + getFullContextPath()); + aliases.add(uid); + } + + /** + * Stores an {@link OperationPathAdapter} in + * {@link org.citrusframework.openapi.OpenApiSpecification#operationIdToOperationPathAdapter}. + * The adapter is stored using two keys: the operationId (optional) and the full path of the + * operation, including the method. The full path is always determinable and thus can always be + * safely used. + * + * @param operation The {@link OperationPathAdapter} to store. + * @param pathItem The path item of the operation, including the method. + */ + private void storeOperationPathAdapter(OasOperation operation, OasPathItem pathItem) { + + String fullContextPath = getFullContextPath(); + String fullOperationPath = getFullPath(pathItem); + String path = pathItem.getPath(); + + String uniqueOperationId = OpenApiUtils.createFullPathOperationIdentifier(operation, + fullOperationPath); + operationToUniqueId.put(operation, uniqueOperationId); + + OperationPathAdapter operationPathAdapter = new OperationPathAdapter(path, fullContextPath, + appendSegmentToUrlPath(fullContextPath, path), operation, uniqueOperationId); + + operationIdToOperationPathAdapter.put(uniqueOperationId, operationPathAdapter); + + if (hasText(operation.operationId)) { + operationIdToOperationPathAdapter.put(operation.operationId, operationPathAdapter); + } } public String getSpecUrl() { @@ -155,14 +534,14 @@ public void setSpecUrl(String specUrl) { this.specUrl = specUrl; } - public void setHttpClient(String httpClient) { - this.httpClient = httpClient; - } - public String getHttpClient() { return httpClient; } + public void setHttpClient(String httpClient) { + this.httpClient = httpClient; + } + public String getRequestUrl() { if (requestUrl == null) { return specUrl; @@ -175,6 +554,28 @@ public void setRequestUrl(String requestUrl) { this.requestUrl = requestUrl; } + public boolean isApiRequestValidationEnabled() { + return apiRequestValidationEnabled; + } + + public void setApiRequestValidationEnabled(boolean enabled) { + this.apiRequestValidationEnabled = enabled; + if (this.openApiValidationContext != null) { + this.openApiValidationContext.setRequestValidationEnabled(enabled); + } + } + + public boolean isApiResponseValidationEnabled() { + return apiResponseValidationEnabled; + } + + public void setApiResponseValidationEnabled(boolean enabled) { + this.apiResponseValidationEnabled = enabled; + if (this.openApiValidationContext != null) { + this.openApiValidationContext.setResponseValidationEnabled(enabled); + } + } + public boolean isGenerateOptionalFields() { return generateOptionalFields; } @@ -183,11 +584,259 @@ public void setGenerateOptionalFields(boolean generateOptionalFields) { this.generateOptionalFields = generateOptionalFields; } - public boolean isValidateOptionalFields() { - return validateOptionalFields; + public AutoFillType getRequestAutoFill() { + return requestAutoFill; + } + + public void setRequestAutoFill(AutoFillType requestAutoFill) { + this.requestAutoFill = requestAutoFill; + } + + public OpenApiSpecification requestAutoFill(AutoFillType requestAutoFill) { + setRequestAutoFill(requestAutoFill); + return this; + } + + public AutoFillType getResponseAutoFill() { + return responseAutoFill; + } + + public void setResponseAutoFill(AutoFillType responseAutoFill) { + this.responseAutoFill = responseAutoFill; + } + + public OpenApiSpecification responseAutoFill(AutoFillType responseAutoFill) { + setResponseAutoFill(responseAutoFill); + return this; + } + + public String getRootContextPath() { + return rootContextPath; + } + + /** + * Sets the root context path for the OpenAPI integration. + *

+ * This path will be prepended to the base path when constructing the full URL path. After + * setting the root context path, the internal path lookups are re-initialized to reflect the + * updated configuration. + *

+ *

Side Effect: Invokes {@link #initPathLookups()} to update internal path mappings + * based on the new root context path.

+ * + * @param rootContextPath the root context path to set + * @see #rootContextPath for more details on how this path is used + * @see #initPathLookups() for the re-initialization of path lookups + */ + public void setRootContextPath(String rootContextPath) { + this.rootContextPath = rootContextPath; + initPathLookups(); + } + + public OpenApiSpecification rootContextPath(String rootContextPath) { + setRootContextPath(rootContextPath); + return this; + } + + public void addAlias(String alias) { + aliases.add(alias); + } + + public Set getAliases() { + return Collections.unmodifiableSet(aliases); + } + + private Collection collectAliases(OasDocument document) { + if (document == null) { + return emptySet(); + } + + Info info = document.info; + if (info == null) { + return emptySet(); + } + + Set set = new HashSet<>(); + if (hasText(info.title)) { + set.add(info.title); + + if (hasText(info.version)) { + set.add(info.title + "/" + info.version); + } + } + + Extension xAlias = info.getExtension("x-citrus-alias"); + if (xAlias != null && xAlias.value != null) { + set.add(xAlias.value.toString()); + } + + return set; + } + + public Optional getOperation(String operationId, TestContext context) { + if (operationId == null) { + return Optional.empty(); + } + + // This is ugly, but we need not make sure that the openApiDoc is initialized, which might + // happen, when instance is created with org.citrusframework.openapi.OpenApiSpecification.from(java.lang.String) + initOpenApiDoc(context); + + return Optional.ofNullable(operationIdToOperationPathAdapter.get(operationId)); + } + + public void initOpenApiDoc(TestContext context) { + if (openApiDoc == null) { + getOpenApiDoc(context); + } + } + + /** + * Get the full path for the given {@link OasPathItem}. + *

+ * The full path is constructed by concatenating the root context path, the base path (if + * applicable), and the path of the given {@code oasPathItem}. The resulting format is: + *

+ *
+     * /rootContextPath/basePath/pathItemPath
+     * 
+ * If the base path is to be neglected, it is excluded from the final constructed path. + * + * @param oasPathItem the OpenAPI path item whose full path is to be constructed + * @return the full URL path, consisting of the root context path, base path, and the given path + * item + */ + public String getFullPath(OasPathItem oasPathItem) { + return appendSegmentToUrlPath(rootContextPath, + getFullBasePath(oasPathItem)); } - public void setValidateOptionalFields(boolean validateOptionalFields) { - this.validateOptionalFields = validateOptionalFields; + /** + * Get the full base-path for the given {@link OasPathItem}. + *

+ * The full base-path is constructed by concatenating the base path (if applicable), and the + * path of the given {@code oasPathItem}. The resulting format is: + *

+ *
+     * /basePath/pathItemPath
+     * 
+ * If the base path is to be neglected, it is excluded from the final constructed path. + * + * @param oasPathItem the OpenAPI path item whose full base-path is to be constructed + * @return the full base URL path, consisting of the base path, and the given path item + */ + public String getFullBasePath(OasPathItem oasPathItem) { + return appendSegmentToUrlPath( + getApplicableBasePath(), oasPathItem.getPath()); } + + + /** + * Constructs the full context path for the given {@link OasPathItem}. + *

+ * The full context path is constructed by appending the root context path to the base path + * specified in the OpenAPI document. If the base path should be neglected (as indicated by + * {@link #neglectBasePath}), only the root context path will be used. + *

+ * + * @return the full context path, consisting of the root context path and optionally the base + * path + * @see #neglectBasePath to understand when the base path is omitted + * @see #rootContextPath for the field used as the root context path + */ + public String getFullContextPath() { + return appendSegmentToUrlPath(rootContextPath, + getApplicableBasePath()); + } + + /** + * Sets whether the base path should be excluded when constructing the full operation path. + * + *

Side Effect: Invokes {@link #initPathLookups()} to update internal path mappings + * based on the new root context path.

+ * + * @param neglectBasePath {@code true} to exclude the base path, {@code false} to include it + * @see #neglectBasePath for the field description + * @see #getFullPath(OasPathItem) for the method affected by this flag + * @see #initPathLookups() for the re-initialization of path lookups + */ + public void setNeglectBasePath(boolean neglectBasePath) { + this.neglectBasePath = neglectBasePath; + initPathLookups(); + } + + public OpenApiSpecification neglectBasePath(boolean neglectBasePath) { + setNeglectBasePath(neglectBasePath); + return this; + } + + /** + * Gets the base path if basePath should be applied. + */ + private String getApplicableBasePath() { + return neglectBasePath ? "" : getBasePath(openApiDoc); + } + + + /** + * Add another alias for this specification. + */ + public OpenApiSpecification alias(String alias) { + addAlias(alias); + return this; + } + + /** + * @param openApiResource the OpenAPI resource from which to determine the alias + * @return an {@code Optional} containing the resource alias if it can be resolved, otherwise an + * empty {@code Optional} + */ + // Package protection for testing + static Optional determineResourceAlias(Resource openApiResource) { + String resourceAlias = null; + + try { + File file = openApiResource.getFile(); + if (file != null) { + resourceAlias = file.getName(); + int index = resourceAlias.lastIndexOf("."); + if (index != -1 && index != resourceAlias.length() - 1) { + resourceAlias = resourceAlias.substring(0, index); + } + return Optional.of(resourceAlias); + } + } catch (Exception e) { + // Ignore and try with url + } + + try { + URL url = openApiResource.getURL(); + return determineUrlAlias(url); + } catch (MalformedURLException e) { + logger.error("Unable to determine resource alias from resource!", e); + } + + return Optional.ofNullable(resourceAlias); + } + + static Optional determineUrlAlias(URL url) { + String resourceAlias = null; + + if (url != null) { + String urlString = URLDecoder.decode(url.getPath(), UTF_8).replace("\\", "/"); + int index = urlString.lastIndexOf("/"); + resourceAlias = urlString; + if (index != -1 && index != urlString.length() - 1) { + resourceAlias = resourceAlias.substring(index + 1); + } + index = resourceAlias.lastIndexOf("."); + if (index != -1 && index != resourceAlias.length() - 1) { + resourceAlias = resourceAlias.substring(0, index); + } + + } + + return Optional.ofNullable(resourceAlias); + } + } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecificationProcessor.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecificationProcessor.java new file mode 100644 index 0000000000..540ad87d9e --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSpecificationProcessor.java @@ -0,0 +1,64 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import org.citrusframework.spi.ResourcePathTypeResolver; +import org.citrusframework.spi.TypeResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +/** + * Interface for processing OpenAPI specifications. + *

+ * This interface is designed to be implemented by custom processors that handle OpenAPI specifications. + * Implementations of this interface are discovered by the standard citrus SPI mechanism. + */ +public interface OpenApiSpecificationProcessor { + + /** + * Logger + */ + Logger logger = LoggerFactory.getLogger(OpenApiSpecificationProcessor.class); + + /** + * OpenAPI processors resource lookup path + */ + String RESOURCE_PATH = "META-INF/citrus/openapi/processor"; + + /** + * Type resolver to find OpenAPI processors on classpath via resource path lookup + */ + TypeResolver TYPE_RESOLVER = new ResourcePathTypeResolver(RESOURCE_PATH); + + /** + * Resolves all available processors from resource path lookup. Scans classpath for processors meta information + * and instantiates those processors. + */ + static Map lookup() { + Map processors = TYPE_RESOLVER.resolveAll("", TypeResolver.DEFAULT_TYPE_PROPERTY, "name"); + + if (logger.isDebugEnabled()) { + processors.forEach((k, v) -> logger.debug("Found openapi specification processor '{}' as {}", k, v.getClass())); + } + + return processors; + } + + void process(OpenApiSpecification openApiSpecification); +} 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 index e966960fdf..9020d555e3 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSupport.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiSupport.java @@ -16,9 +16,6 @@ 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; @@ -33,6 +30,9 @@ import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.representer.Representer; +import java.util.Collection; +import java.util.Map; + public class OpenApiSupport { private static final ObjectMapper OBJECT_MAPPER; diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java index c5aadd01e2..2f6db9e17d 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/OpenApiTestDataGenerator.java @@ -16,299 +16,45 @@ 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; +import org.citrusframework.openapi.random.RandomContext; /** - * Generates proper payloads and validation expressions based on Open API specification rules. Creates outbound payloads - * with generated random test data according to specification and creates inbound payloads with proper validation expressions to - * enforce the specification rules. - * + * Generates proper payloads and validation expressions based on Open API specification rules. */ -public class OpenApiTestDataGenerator { - - /** - * 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; - } +public final class OpenApiTestDataGenerator { - return createRandomValueExpression(schema, definitions, quotes, specification); + private OpenApiTestDataGenerator() { + // Static access only } /** - * 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('yyyy-MM-dd')"); - } 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 + * Creates payload from schema for outbound message. */ - private static boolean isRequired(OasSchema schema, String field) { - if (schema.required == null) { - return true; - } - - return schema.required.contains(field); + public static String createOutboundPayload(OasSchema schema, OpenApiSpecification specification) { + RandomContext randomContext = new RandomContext(specification, true); + randomContext.generate(schema); + return randomContext.getRandomModelBuilder().write(); } /** - * 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 + * Use test variable with given name if present or create value from schema with random values */ - public static String createValidationExpression(String name, OasSchema schema, Map definitions, - boolean quotes, OpenApiSpecification specification, - TestContext context) { + public static String createRandomValueExpression(String name, OasSchema schema, OpenApiSpecification specification, TestContext context) { if (context.getVariables().containsKey(name)) { return CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX; } - return createValidationExpression(schema, definitions, quotes, specification); + RandomContext randomContext = new RandomContext(specification, false); + randomContext.generate(schema); + return randomContext.getRandomModelBuilder().write(); } /** - * 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 + * Use test variable with given name (if present) or create random value expression using + * functions according to schema type and format. */ public static String createRandomValueExpression(String name, OasSchema schema, TestContext context) { if (context.getVariables().containsKey(name)) { @@ -318,34 +64,9 @@ public static String createRandomValueExpression(String name, OasSchema schema, 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 ""; - } + RandomContext randomContext = new RandomContext(); + randomContext.generate(schema); + return randomContext.getRandomModelBuilder().write(); } } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiActionBuilder.java index c17613738a..9f7c3136d3 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiActionBuilder.java @@ -16,6 +16,9 @@ package org.citrusframework.openapi.actions; +import static org.citrusframework.openapi.OpenApiSettings.getOpenApiValidationPolicy; + +import java.net.URL; import org.citrusframework.TestAction; import org.citrusframework.endpoint.Endpoint; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -25,8 +28,6 @@ import org.citrusframework.spi.ReferenceResolver; import org.citrusframework.util.ObjectHelper; -import java.net.URL; - /** * Action executes client and server operations using given OpenApi specification. * Action creates proper request and response data from given specification rules. @@ -35,117 +36,126 @@ */ public class OpenApiActionBuilder extends AbstractReferenceResolverAwareTestActionBuilder { - 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(); - } + private OpenApiSpecificationSource openApiSpecificationSource; + + public OpenApiActionBuilder() { + } + + public OpenApiActionBuilder(OpenApiSpecification specification) { + this.openApiSpecificationSource = new OpenApiSpecificationSource(specification); + } + + public OpenApiActionBuilder(String openApiAlias) { + this.openApiSpecificationSource = new OpenApiSpecificationSource(openApiAlias); + } + + /** + * Static entrance method for the OpenApi fluent action builder. + */ + public static OpenApiActionBuilder openapi() { + return new OpenApiActionBuilder(); + } + + public static OpenApiActionBuilder openapi(OpenApiSpecification specification) { + return new OpenApiActionBuilder(specification); + } + + public static OpenApiActionBuilder openapi(String openApiAlias) { + return new OpenApiActionBuilder(openApiAlias); + } + + public OpenApiActionBuilder specification(OpenApiSpecification specification) { + this.openApiSpecificationSource = new OpenApiSpecificationSource(specification); + return this; + } + + public OpenApiActionBuilder specification(URL specUrl) { + return specification(OpenApiSpecification.from(specUrl, getOpenApiValidationPolicy())); + } + + public OpenApiActionBuilder specification(String specUrl) { + return specification(OpenApiSpecification.from(specUrl, getOpenApiValidationPolicy())); + } + + public OpenApiClientActionBuilder client() { + assertSpecification(); + OpenApiClientActionBuilder clientActionBuilder = new OpenApiClientActionBuilder(openApiSpecificationSource) + .withReferenceResolver(referenceResolver); + this.delegate = clientActionBuilder; + return clientActionBuilder; + } + + /** + * Initiate http client action. + */ + public OpenApiClientActionBuilder client(HttpClient httpClient) { + assertSpecification(); + + if (httpClient.getEndpointConfiguration().getRequestUrl() != null) { + openApiSpecificationSource.setHttpClient(httpClient.getEndpointConfiguration().getRequestUrl()); + } + + OpenApiClientActionBuilder clientActionBuilder = new OpenApiClientActionBuilder(httpClient, openApiSpecificationSource) + .withReferenceResolver(referenceResolver); + this.delegate = clientActionBuilder; + return clientActionBuilder; + } + + /** + * Initiate http client action. + */ + public OpenApiClientActionBuilder client(String httpClient) { + assertSpecification(); + + openApiSpecificationSource.setHttpClient(httpClient); + + OpenApiClientActionBuilder clientActionBuilder = new OpenApiClientActionBuilder(httpClient, openApiSpecificationSource) + .withReferenceResolver(referenceResolver); + this.delegate = clientActionBuilder; + return clientActionBuilder; + } + + /** + * Initiate http server action. + */ + public OpenApiServerActionBuilder server(Endpoint endpoint) { + assertSpecification(); + + OpenApiServerActionBuilder serverActionBuilder = new OpenApiServerActionBuilder(endpoint, openApiSpecificationSource) + .withReferenceResolver(referenceResolver); + this.delegate = serverActionBuilder; + return serverActionBuilder; + } + + private void assertSpecification() { + if (openApiSpecificationSource == null) { + throw new CitrusRuntimeException("Invalid OpenApiSpecificationSource - please set specification first"); + } + } + + /** + * Initiate http server action. + */ + public OpenApiServerActionBuilder server(String httpServer) { + assertSpecification(); + + OpenApiServerActionBuilder serverActionBuilder = new OpenApiServerActionBuilder(httpServer, openApiSpecificationSource) + .withReferenceResolver(referenceResolver); + this.delegate = serverActionBuilder; + return serverActionBuilder; + } + + /** + * Sets the bean reference resolver. + */ + 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(); + } } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilder.java index a80cd953ba..5ff12aa381 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilder.java @@ -18,12 +18,13 @@ import org.citrusframework.TestAction; import org.citrusframework.endpoint.Endpoint; -import org.citrusframework.openapi.OpenApiSpecification; import org.citrusframework.spi.AbstractReferenceResolverAwareTestActionBuilder; import org.citrusframework.spi.ReferenceResolver; import org.citrusframework.util.ObjectHelper; import org.springframework.http.HttpStatus; +import static org.springframework.http.HttpStatus.OK; + /** * Action executes http client operations such as sending requests and receiving responses. * @@ -31,33 +32,39 @@ */ public class OpenApiClientActionBuilder extends AbstractReferenceResolverAwareTestActionBuilder { - private final OpenApiSpecification specification; + private final OpenApiSpecificationSource openApiSpecificationSource; - /** Target http client instance */ + /** + * Target http client instance + */ private Endpoint httpClient; private String httpClientUri; /** * Default constructor. */ - public OpenApiClientActionBuilder(Endpoint httpClient, OpenApiSpecification specification) { + public OpenApiClientActionBuilder(Endpoint httpClient, OpenApiSpecificationSource openApiSpecificationSource) { this.httpClient = httpClient; - this.specification = specification; + this.openApiSpecificationSource = openApiSpecificationSource; } /** * Default constructor. */ - public OpenApiClientActionBuilder(String httpClientUri, OpenApiSpecification specification) { + public OpenApiClientActionBuilder(String httpClientUri, OpenApiSpecificationSource openApiSpecificationSource) { this.httpClientUri = httpClientUri; - this.specification = specification; + this.openApiSpecificationSource = openApiSpecificationSource; + } + + public OpenApiClientActionBuilder(OpenApiSpecificationSource openApiSpecificationSource) { + this.openApiSpecificationSource = openApiSpecificationSource; } /** * Sends Http requests as client. */ public OpenApiClientRequestActionBuilder send(String operationId) { - OpenApiClientRequestActionBuilder builder = new OpenApiClientRequestActionBuilder(specification, operationId); + OpenApiClientRequestActionBuilder builder = new OpenApiClientRequestActionBuilder(openApiSpecificationSource, operationId); if (httpClient != null) { builder.endpoint(httpClient); } else { @@ -76,7 +83,7 @@ public OpenApiClientRequestActionBuilder send(String operationId) { * Uses default Http status 200 OK. */ public OpenApiClientResponseActionBuilder receive(String operationId) { - return receive(operationId, HttpStatus.OK); + return receive(operationId, OK); } /** @@ -90,7 +97,7 @@ public OpenApiClientResponseActionBuilder receive(String operationId, HttpStatus * Receives Http response messages as client. */ public OpenApiClientResponseActionBuilder receive(String operationId, String statusCode) { - OpenApiClientResponseActionBuilder builder = new OpenApiClientResponseActionBuilder(specification, operationId, statusCode); + OpenApiClientResponseActionBuilder builder = new OpenApiClientResponseActionBuilder(openApiSpecificationSource, operationId, statusCode); if (httpClient != null) { builder.endpoint(httpClient); } else { @@ -105,6 +112,7 @@ public OpenApiClientResponseActionBuilder receive(String operationId, String sta /** * Sets the bean reference resolver. + * * @param referenceResolver */ public OpenApiClientActionBuilder withReferenceResolver(ReferenceResolver 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 index a9574f5310..d9f2994b29 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientRequestActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientRequestActionBuilder.java @@ -16,27 +16,31 @@ 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 static org.citrusframework.openapi.OpenApiMessageType.REQUEST; +import static org.citrusframework.openapi.OpenApiTestDataGenerator.createOutboundPayload; +import static org.citrusframework.openapi.OpenApiTestDataGenerator.createRandomValueExpression; +import static org.citrusframework.util.StringUtils.isNotEmpty; -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 java.util.List; +import java.util.Locale; +import java.util.Optional; import org.citrusframework.CitrusSettings; +import org.citrusframework.actions.SendMessageAction; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.http.actions.HttpClientRequestActionBuilder; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; import org.citrusframework.message.Message; +import org.citrusframework.openapi.AutoFillType; import org.citrusframework.openapi.OpenApiSpecification; -import org.citrusframework.openapi.OpenApiTestDataGenerator; import org.citrusframework.openapi.model.OasModelHelper; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.citrusframework.openapi.validation.OpenApiOperationToMessageHeadersProcessor; +import org.citrusframework.openapi.validation.OpenApiValidationContext; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -45,115 +49,260 @@ */ public class OpenApiClientRequestActionBuilder extends HttpClientRequestActionBuilder { + private final OpenApiSpecificationSource openApiSpecificationSource; + private final String operationId; + private OpenApiOperationToMessageHeadersProcessor openApiOperationToMessageHeadersProcessor; + private boolean schemaValidation = true; + + /** * Default constructor initializes http request message builder. */ - public OpenApiClientRequestActionBuilder(OpenApiSpecification openApiSpec, String operationId) { + public OpenApiClientRequestActionBuilder(OpenApiSpecificationSource openApiSpec, + String operationId) { this(new HttpMessage(), openApiSpec, operationId); } - public OpenApiClientRequestActionBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, - String operationId) { - super(new OpenApiClientRequestMessageBuilder(httpMessage, openApiSpec, operationId), httpMessage); + public OpenApiClientRequestActionBuilder(HttpMessage httpMessage, + OpenApiSpecificationSource openApiSpec, + String operationId) { + this(openApiSpec, + new OpenApiClientRequestMessageBuilder(httpMessage, openApiSpec, operationId), + httpMessage, operationId); + } + + public OpenApiClientRequestActionBuilder(OpenApiSpecificationSource openApiSpec, + OpenApiClientRequestMessageBuilder messageBuilder, + HttpMessage message, + String operationId) { + super(messageBuilder, message); + this.openApiSpecificationSource = openApiSpec; + this.operationId = operationId; + } + + public OpenApiClientRequestActionBuilder autoFill(AutoFillType autoFill) { + ((OpenApiClientRequestMessageBuilder) this.messageBuilderSupport.getMessageBuilder()).autoFill( + autoFill); + return this; + } + + @Override + public SendMessageAction doBuild() { + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve( + referenceResolver); + + // Honor default enablement of schema validation + OpenApiValidationContext openApiValidationContext = openApiSpecification.getOpenApiValidationContext(); + if (openApiValidationContext != null && schemaValidation) { + schemaValidation = openApiValidationContext.isRequestValidationEnabled(); + } + + if (schemaValidation && !messageProcessors.contains( + openApiOperationToMessageHeadersProcessor)) { + openApiOperationToMessageHeadersProcessor = new OpenApiOperationToMessageHeadersProcessor( + openApiSpecification, operationId, REQUEST); + process(openApiOperationToMessageHeadersProcessor); + } + + return super.doBuild(); + } + + /** + * By default, enable schema validation as the OpenAPI is always available. + */ + @Override + protected HttpMessageBuilderSupport createHttpMessageBuilderSupport() { + HttpMessageBuilderSupport httpMessageBuilderSupport = super.createHttpMessageBuilderSupport(); + httpMessageBuilderSupport.schemaValidation(true); + return httpMessageBuilderSupport; + } + + public OpenApiClientRequestActionBuilder schemaValidation(boolean schemaValidation) { + this.schemaValidation = schemaValidation; + return this; } - private static class OpenApiClientRequestMessageBuilder extends HttpMessageBuilder { + public static class OpenApiClientRequestMessageBuilder extends HttpMessageBuilder { + + private final OpenApiSpecificationSource openApiSpecificationSource; - private final OpenApiSpecification openApiSpec; private final String operationId; - private final HttpMessage httpMessage; + private AutoFillType autoFill ; - public OpenApiClientRequestMessageBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, - String operationId) { + public OpenApiClientRequestMessageBuilder(HttpMessage httpMessage, + OpenApiSpecificationSource openApiSpec, + String operationId) { super(httpMessage); - this.openApiSpec = openApiSpec; + this.openApiSpecificationSource = openApiSpec; this.operationId = operationId; - this.httpMessage = httpMessage; + } + + public OpenApiClientRequestMessageBuilder autoFill(AutoFillType autoFill) { + this.autoFill = autoFill; + return this; } @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; - } - } + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve( + context.getReferenceResolver()); - if (operation == null) { - throw new CitrusRuntimeException("Unable to locate operation with id '%s' in OpenAPI specification %s".formatted(operationId, openApiSpec.getSpecUrl())); + if (autoFill == null) { + autoFill = openApiSpecification.getRequestAutoFill(); } - if (operation.parameters != null) { - List configuredHeaders = getHeaderBuilders() - .stream() - .flatMap(b -> b.builderHeaders(context).keySet().stream()) - .toList(); - operation.parameters.stream() - .filter(param -> "header".equals(param.in)) - .filter(param -> (param.required != null && param.required) || context.getVariables().containsKey(param.getName())) - .forEach(param -> { - if(httpMessage.getHeader(param.getName()) == null && !configuredHeaders.contains(param.getName())) { - httpMessage.setHeader(param.getName(), - OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), (OasSchema) param.schema, - OasModelHelper.getSchemaDefinitions(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 -> { - if(!httpMessage.getQueryParams().containsKey(param.getName())) { - httpMessage.queryParam(param.getName(), - OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), (OasSchema) param.schema, context)); - } - }); + openApiSpecification.initOpenApiDoc(context); + openApiSpecification.getOperation(operationId, context) + .ifPresentOrElse(operationPathAdapter -> + buildMessageFromOperation(openApiSpecification, operationPathAdapter, + context), + () -> { + throw new CitrusRuntimeException( + "Unable to locate operation with id '%s' in OpenAPI specification %s".formatted( + operationId, openApiSpecification.getSpecUrl())); + }); + context.setVariable(openApiSpecification.getUid(), openApiSpecification); + + return super.build(context, messageType); + } + + @Override + public Object buildMessagePayload(TestContext context, String messageType) { + if (getPayloadBuilder() == null) { + this.setPayloadBuilder(new OpenApiPayloadBuilder(getMessage().getPayload())); } + return super.buildMessagePayload(context, messageType); + } - if(httpMessage.getPayload() == null || (httpMessage.getPayload() instanceof String p && p.isEmpty())) { - Optional body = OasModelHelper.getRequestBodySchema(oasDocument, operation); - body.ifPresent(oasSchema -> httpMessage.setPayload(OpenApiTestDataGenerator.createOutboundPayload(oasSchema, - OasModelHelper.getSchemaDefinitions(oasDocument), openApiSpec))); + private void buildMessageFromOperation(OpenApiSpecification openApiSpecification, + OperationPathAdapter operationPathAdapter, + TestContext context) { + OasOperation operation = operationPathAdapter.operation(); + String path = operationPathAdapter.fullPath(); + HttpMethod method = HttpMethod.valueOf( + operationPathAdapter.operation().getMethod().toUpperCase(Locale.US)); + + if (operation.parameters != null) { + setMissingHeadersToRandomValues(openApiSpecification, context, operation); + setMissingQueryParametersToRandomValues(openApiSpecification, context, operation); } - String randomizedPath = pathItem.getPath(); + setMissingBodyToRandomValue(openApiSpecification, context, operation); + String randomizedPath = getMessage().getPath() != null ? getMessage().getPath() : path; if (operation.parameters != null) { List pathParams = operation.parameters.stream() - .filter(p -> "path".equals(p.in)).toList(); + .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; + String pathParameterValue = getDefinedPathParameter(context, + parameter.getName()); + if (isNotEmpty(pathParameterValue)) { + parameterValue = "\\" + pathParameterValue; } else { - parameterValue = OpenApiTestDataGenerator.createRandomValueExpression((OasSchema) parameter.schema); + parameterValue = createRandomValueExpression( + (OasSchema) parameter.schema); } - randomizedPath = Pattern.compile("\\{" + parameter.getName() + "}") - .matcher(randomizedPath) - .replaceAll(parameterValue); + + randomizedPath = randomizedPath.replaceAll("\\{" + parameter.getName() + "}", + parameterValue); } } OasModelHelper.getRequestContentType(operation) - .ifPresent(contentType -> httpMessage.setHeader(HttpHeaders.CONTENT_TYPE, contentType)); + .ifPresent( + contentType -> getMessage().setHeader(HttpHeaders.CONTENT_TYPE, contentType)); - httpMessage.path(randomizedPath); - httpMessage.method(method); + getMessage().path(randomizedPath); + getMessage().method(method); + } - return super.build(context, messageType); + protected String getDefinedPathParameter(TestContext context, String name) { + if (context.getVariables().containsKey(name)) { + return CitrusSettings.VARIABLE_PREFIX + name + CitrusSettings.VARIABLE_SUFFIX; + } + return null; + } + + private void setMissingBodyToRandomValue(OpenApiSpecification openApiSpecification, + TestContext context, OasOperation operation) { + if (getMessage().getPayload() == null || ( + getMessage().getPayload() instanceof String payloadString + && payloadString.isEmpty())) { + Optional body = OasModelHelper.getRequestBodySchema( + openApiSpecification.getOpenApiDoc(context), operation); + + body.ifPresent(oasSchema -> { + + if (autoFill == AutoFillType.ALL || autoFill == AutoFillType.REQUIRED) { + getMessage().setPayload( + createOutboundPayload(oasSchema, openApiSpecification)); + } + }); + } + } + + /** + * Creates all required query parameters, if they have not already been specified. + */ + private void setMissingQueryParametersToRandomValues( + OpenApiSpecification openApiSpecification, TestContext context, + OasOperation operation) { + operation.parameters.stream() + .filter(param -> "query".equals(param.in)) + // Not configured manually + .filter(param -> !getMessage().getQueryParams().containsKey(param.getName())) + // Only targeted parameters + .filter(param -> autoFill == AutoFillType.ALL || (autoFill == AutoFillType.REQUIRED + && Boolean.TRUE.equals(param.required))) + .forEach(param -> { + Object queryParameterValue = context.getVariables() + .get(param.getName()); + if (queryParameterValue == null) { + queryParameterValue = createRandomValueExpression(param.getName(), + (OasSchema) param.schema, openApiSpecification, + context); + } + try { + getMessage().queryParam(param.getName(), queryParameterValue.toString()); + } catch (Exception e) { + // Note that exploded object query parameter representation for example, cannot properly + // be randomized. + logger.warn( + "Unable to set missing required query parameter to random value: {}", + param); + } + }); + } + + /** + * Creates all required headers, if they have not already been specified. + */ + private void setMissingHeadersToRandomValues(OpenApiSpecification openApiSpecification, + TestContext context, OasOperation operation) { + List configuredHeaders = getHeaderBuilders() + .stream() + .flatMap(b -> b.builderHeaders(context).keySet().stream()) + .toList(); + operation.parameters.stream() + .filter(param -> "header".equals(param.in)) + // Not configured manually + .filter(param -> getMessage().getHeader(param.getName()) == null + && !configuredHeaders.contains(param.getName())) + // Only targeted parameters + .filter(param -> autoFill == AutoFillType.ALL || (autoFill == AutoFillType.REQUIRED + && Boolean.TRUE.equals(param.required))) + .forEach(param -> { + Object headerValue = context.getVariables() + .get(param.getName()); + if (headerValue == null) { + headerValue = createRandomValueExpression(param.getName(), + (OasSchema) param.schema, + openApiSpecification, context); + } + getMessage().setHeader(param.getName(), headerValue); + }); } } } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientResponseActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientResponseActionBuilder.java index 49b8a21c3a..b57061a912 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientResponseActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiClientResponseActionBuilder.java @@ -16,16 +16,23 @@ package org.citrusframework.openapi.actions; -import java.util.Map; -import java.util.Optional; -import java.util.regex.Pattern; +import static org.citrusframework.message.MessageType.JSON; +import static org.citrusframework.openapi.OpenApiMessageType.RESPONSE; +import static org.citrusframework.openapi.model.OasModelHelper.resolveSchema; +import static org.citrusframework.openapi.validation.OpenApiMessageValidationContext.Builder.openApi; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -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 jakarta.annotation.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; import org.citrusframework.CitrusSettings; +import org.citrusframework.actions.ReceiveMessageAction; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.http.actions.HttpClientResponseActionBuilder; @@ -33,9 +40,11 @@ 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.citrusframework.openapi.model.OperationPathAdapter; +import org.citrusframework.openapi.validation.OpenApiMessageValidationContext; +import org.citrusframework.openapi.validation.OpenApiOperationToMessageHeadersProcessor; +import org.citrusframework.openapi.validation.OpenApiValidationContext; import org.springframework.http.HttpStatus; /** @@ -43,88 +52,158 @@ */ public class OpenApiClientResponseActionBuilder extends HttpClientResponseActionBuilder { + private final OpenApiSpecificationSource openApiSpecificationSource; + private final String operationId; + private OpenApiOperationToMessageHeadersProcessor openApiOperationToMessageHeadersProcessor; + private boolean schemaValidation = true; + /** * Default constructor initializes http response message builder. */ - public OpenApiClientResponseActionBuilder(OpenApiSpecification openApiSpec, String operationId, String statusCode) { + public OpenApiClientResponseActionBuilder(OpenApiSpecificationSource 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); + public OpenApiClientResponseActionBuilder(HttpMessage httpMessage, + OpenApiSpecificationSource openApiSpecificationSource, + String operationId, + String statusCode) { + this(openApiSpecificationSource, new OpenApiClientResponseMessageBuilder(httpMessage, openApiSpecificationSource, operationId, statusCode), httpMessage, operationId); } - private static class OpenApiClientResponseMessageBuilder extends HttpMessageBuilder { + public OpenApiClientResponseActionBuilder(OpenApiSpecificationSource openApiSpec, + OpenApiClientResponseMessageBuilder messageBuilder, + HttpMessage message, + String operationId) { + super(messageBuilder, message); + this.openApiSpecificationSource = openApiSpec; + this.operationId = operationId; - private final OpenApiSpecification openApiSpec; - private final String operationId; - private final String statusCode; + // Set json as default instead of xml. This is most common for rest. + this.getMessageBuilderSupport().type(JSON); + } + + public static void fillMessageTypeFromResponse(OpenApiSpecification openApiSpecification, + HttpMessage httpMessage, + @Nullable OasOperation operation, + @Nullable OasResponse response) { + if (operation == null || response == null) { + return; + } + + Optional responseSchema = OasModelHelper.getSchema(response); + responseSchema.ifPresent(oasSchema -> { + OasSchema resolvedSchema = resolveSchema(openApiSpecification.getOpenApiDoc(null), oasSchema); + if (OasModelHelper.isObjectType(resolvedSchema) || OasModelHelper.isObjectArrayType(resolvedSchema)) { + Collection responseTypes = OasModelHelper.getResponseTypes(operation,response); + if (responseTypes.contains(APPLICATION_JSON_VALUE)) { + httpMessage.setType(JSON); + } + } + } + ); + } + + /** + * Overridden to change the default message type to JSON, as Json is more common in OpenAPI context. + */ + @Override + protected HttpMessageBuilderSupport createHttpMessageBuilderSupport() { + HttpMessageBuilderSupport support = super.createHttpMessageBuilderSupport(); + support.type(CitrusSettings.getPropertyEnvOrDefault( + CitrusSettings.DEFAULT_MESSAGE_TYPE_PROPERTY, + CitrusSettings.DEFAULT_MESSAGE_TYPE_ENV, + JSON.toString())); + return support; + } + + @Override + public ReceiveMessageAction doBuild() { + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve(referenceResolver); + + // Honor default enablement of schema validation + OpenApiValidationContext openApiValidationContext = openApiSpecification.getOpenApiValidationContext(); + if (openApiValidationContext != null && schemaValidation) { + schemaValidation = openApiValidationContext.isResponseValidationEnabled(); + } + + if (schemaValidation && !messageProcessors.contains(openApiOperationToMessageHeadersProcessor)) { + openApiOperationToMessageHeadersProcessor = new OpenApiOperationToMessageHeadersProcessor(openApiSpecification, operationId, RESPONSE); + process(openApiOperationToMessageHeadersProcessor); + } + if (schemaValidation && getValidationContexts().stream().noneMatch(OpenApiMessageValidationContext.class::isInstance)) { + validate(openApi(openApiSpecification) + .schemaValidation(schemaValidation) + .build()); + } + + return super.doBuild(); + } + + public OpenApiClientResponseActionBuilder schemaValidation(boolean enabled) { + schemaValidation = enabled; + return this; + } + + public static class OpenApiClientResponseMessageBuilder extends HttpMessageBuilder { + + private final OpenApiSpecificationSource openApiSpecificationSource; + private final String operationId; private final HttpMessage httpMessage; + private String statusCode; - public OpenApiClientResponseMessageBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, - String operationId, String statusCode) { + public OpenApiClientResponseMessageBuilder(HttpMessage httpMessage, + OpenApiSpecificationSource openApiSpecificationSource, + String operationId, + String statusCode) { super(httpMessage); - this.openApiSpec = openApiSpec; + this.openApiSpecificationSource = openApiSpecificationSource; this.operationId = operationId; this.statusCode = statusCode; this.httpMessage = httpMessage; } + public OpenApiClientResponseMessageBuilder statusCode(String statusCode) { + this.statusCode = statusCode; + return this; + } + @Override public Message build(TestContext context, String messageType) { - OasOperation operation = null; - OasDocument oasDocument = openApiSpec.getOpenApiDoc(context); + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve(context.getReferenceResolver()); - for (OasPathItem path : OasModelHelper.getPathItems(oasDocument.paths)) { - Optional> operationEntry = OasModelHelper.getOperationMap(path).entrySet().stream() - .filter(op -> operationId.equals(op.getValue().operationId)) - .findFirst(); + openApiSpecification.getOperation(operationId, context) + .ifPresentOrElse(operationPathAdapter -> + buildMessageFromOperation(openApiSpecification, operationPathAdapter, context), () -> { + throw new CitrusRuntimeException("Unable to locate operation with id '%s' in OpenAPI specification %s".formatted(operationId, openApiSpecification.getSpecUrl())); + }); - if (operationEntry.isPresent()) { - operation = operationEntry.get().getValue(); - break; - } - } + return super.build(context, messageType); + } - if (operation == null) { - throw new CitrusRuntimeException("Unable to locate operation with id '%s' in OpenAPI specification %s".formatted(operationId, openApiSpec.getSpecUrl())); - } + private void buildMessageFromOperation(OpenApiSpecification openApiSpecification, OperationPathAdapter operationPathAdapter, TestContext context) { + OasOperation operation = operationPathAdapter.operation(); - 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)); - } + // Headers already present in httpMessage should not be overwritten by open api. + // E.g. if a reasonPhrase was explicitly set in the builder, it must not be overwritten. + Map currentHeaders = new HashMap<>(httpMessage.getHeaders()); - 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); - } - } + if (operation.responses != null) { + Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration( + openApiSpecification.getOpenApiDoc(context), operation, statusCode, null); - Optional responseSchema = OasModelHelper.getSchema(response); - responseSchema.ifPresent(oasSchema -> httpMessage.setPayload(OpenApiTestDataGenerator.createInboundPayload(oasSchema, OasModelHelper.getSchemaDefinitions(oasDocument), openApiSpec))); - } + responseForRandomGeneration.ifPresent( + oasResponse -> fillMessageTypeFromResponse(openApiSpecification, httpMessage, operation, oasResponse)); } - OasModelHelper.getResponseContentType(oasDocument, operation) - .ifPresent(contentType -> httpMessage.setHeader(HttpHeaders.CONTENT_TYPE, contentType)); - - if (Pattern.compile("[0-9]+").matcher(statusCode).matches()) { + if (Pattern.compile("\\d+").matcher(statusCode).matches()) { httpMessage.status(HttpStatus.valueOf(Integer.parseInt(statusCode))); } else { httpMessage.status(HttpStatus.OK); } - return super.build(context, messageType); + httpMessage.getHeaders().putAll(currentHeaders); } } } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiPayloadBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiPayloadBuilder.java new file mode 100644 index 0000000000..0460344ca2 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiPayloadBuilder.java @@ -0,0 +1,54 @@ +package org.citrusframework.openapi.actions; + +import org.citrusframework.context.TestContext; +import org.citrusframework.message.builder.DefaultPayloadBuilder; +import org.springframework.util.MultiValueMap; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class OpenApiPayloadBuilder extends DefaultPayloadBuilder { + + public OpenApiPayloadBuilder(Object payload) { + super(payload); + } + + private static void replaceDynamicContentInMultiValueMap(TestContext context, MultiValueMap multiValueMap) { + Set cache = new HashSet<>(multiValueMap.entrySet()); + multiValueMap.clear(); + for (Object value : cache) { + if (value instanceof Map.Entry entry) { + replaceDynamicContentInEntry(context, multiValueMap, entry); + } + } + } + + private static void replaceDynamicContentInEntry(TestContext context, MultiValueMap multiValueMap, Entry entry) { + Object key = entry.getKey(); + + List list = (List) entry.getValue(); + if (list != null) { + for (int i = 0; i < list.size(); i++) { + Object listEntry = list.get(i); + if (listEntry instanceof String text) { + list.set(i, context.replaceDynamicContentInString(text)); + } + } + } + + Object newKey = key instanceof String text ? context.replaceDynamicContentInString(text) : key; + multiValueMap.put(newKey, list); + } + + @Override + public Object buildPayload(TestContext context) { + if (getPayload() instanceof MultiValueMap multiValueMap) { + replaceDynamicContentInMultiValueMap(context, (MultiValueMap) multiValueMap); + } + + return super.buildPayload(context); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilder.java index b1feee6faf..9afc29ca6b 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilder.java @@ -18,7 +18,6 @@ import org.citrusframework.TestAction; import org.citrusframework.endpoint.Endpoint; -import org.citrusframework.openapi.OpenApiSpecification; import org.citrusframework.spi.AbstractReferenceResolverAwareTestActionBuilder; import org.citrusframework.spi.ReferenceResolver; import org.citrusframework.util.ObjectHelper; @@ -31,33 +30,35 @@ */ public class OpenApiServerActionBuilder extends AbstractReferenceResolverAwareTestActionBuilder { - private final OpenApiSpecification specification; + private final OpenApiSpecificationSource openApiSpecificationSource; - /** Target http client instance */ + /** + * Target http client instance + */ private Endpoint httpServer; private String httpServerUri; /** * Default constructor. */ - public OpenApiServerActionBuilder(Endpoint httpServer, OpenApiSpecification specification) { + public OpenApiServerActionBuilder(Endpoint httpServer, OpenApiSpecificationSource specification) { this.httpServer = httpServer; - this.specification = specification; + this.openApiSpecificationSource = specification; } /** * Default constructor. */ - public OpenApiServerActionBuilder(String httpServerUri, OpenApiSpecification specification) { + public OpenApiServerActionBuilder(String httpServerUri, OpenApiSpecificationSource specification) { this.httpServerUri = httpServerUri; - this.specification = specification; + this.openApiSpecificationSource = specification; } /** * Receive Http requests as server. */ public OpenApiServerRequestActionBuilder receive(String operationId) { - OpenApiServerRequestActionBuilder builder = new OpenApiServerRequestActionBuilder(specification, operationId); + OpenApiServerRequestActionBuilder builder = new OpenApiServerRequestActionBuilder(openApiSpecificationSource, operationId); if (httpServer != null) { builder.endpoint(httpServer); } else { @@ -86,11 +87,25 @@ public OpenApiServerResponseActionBuilder send(String operationId, HttpStatus st return send(operationId, String.valueOf(status.value())); } + /** + * Send Http response messages as server to client. + */ + public OpenApiServerResponseActionBuilder send(String operationId, HttpStatus status, String accept) { + return send(operationId, String.valueOf(status.value()), accept); + } + /** * Send Http response messages as server to client. */ public OpenApiServerResponseActionBuilder send(String operationId, String statusCode) { - OpenApiServerResponseActionBuilder builder = new OpenApiServerResponseActionBuilder(specification, operationId, statusCode); + return send(operationId, statusCode, null); + } + + /** + * Send Http response messages as server to client. + */ + public OpenApiServerResponseActionBuilder send(String operationId, String statusCode, String accept) { + OpenApiServerResponseActionBuilder builder = new OpenApiServerResponseActionBuilder(openApiSpecificationSource, operationId, statusCode, accept); if (httpServer != null) { builder.endpoint(httpServer); } else { @@ -106,6 +121,7 @@ public OpenApiServerResponseActionBuilder send(String operationId, String status /** * Sets the Spring bean application context. + * * @param referenceResolver */ public OpenApiServerActionBuilder withReferenceResolver(ReferenceResolver 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 index 30c7a0b17a..fef2b28f64 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerRequestActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerRequestActionBuilder.java @@ -16,132 +16,95 @@ 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 static org.citrusframework.openapi.OpenApiMessageType.REQUEST; +import static org.citrusframework.openapi.validation.OpenApiMessageValidationContext.Builder.openApi; + +import org.citrusframework.actions.ReceiveMessageAction; 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; +import org.citrusframework.openapi.validation.OpenApiMessageValidationContext; +import org.citrusframework.openapi.validation.OpenApiOperationToMessageHeadersProcessor; +import org.citrusframework.openapi.validation.OpenApiValidationContext; /** * @since 4.1 */ public class OpenApiServerRequestActionBuilder extends HttpServerRequestActionBuilder { + private final OpenApiSpecificationSource openApiSpecificationSource; + private final String operationId; + private OpenApiOperationToMessageHeadersProcessor openApiOperationToMessageHeadersProcessor; + + /** + * Schema validation is enabled by default. + */ + private boolean schemaValidation = true; + /** * Default constructor initializes http request message builder. */ - public OpenApiServerRequestActionBuilder(OpenApiSpecification openApiSpec, String operationId) { - this(new HttpMessage(), openApiSpec, operationId); + public OpenApiServerRequestActionBuilder(OpenApiSpecificationSource openApiSpecificationSource, String operationId) { + this(new HttpMessage(), openApiSpecificationSource, operationId); } - public OpenApiServerRequestActionBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, + public OpenApiServerRequestActionBuilder(HttpMessage httpMessage, + OpenApiSpecificationSource openApiSpecificationSource, String operationId) { - super(new OpenApiServerRequestMessageBuilder(httpMessage, openApiSpec, operationId), httpMessage); + super(new OpenApiServerRequestMessageBuilder(httpMessage, openApiSpecificationSource), httpMessage); + this.openApiSpecificationSource = openApiSpecificationSource; + this.operationId = operationId; } - private static class OpenApiServerRequestMessageBuilder extends HttpMessageBuilder { + @Override + public ReceiveMessageAction doBuild() { + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve(referenceResolver); + + // Honor default enablement of schema validation + OpenApiValidationContext openApiValidationContext = openApiSpecification.getOpenApiValidationContext(); + if (openApiValidationContext != null && schemaValidation) { + schemaValidation = openApiValidationContext.isRequestValidationEnabled(); + } + + if (schemaValidation && !messageProcessors.contains(openApiOperationToMessageHeadersProcessor)) { + openApiOperationToMessageHeadersProcessor = new OpenApiOperationToMessageHeadersProcessor(openApiSpecification, operationId, REQUEST); + process(openApiOperationToMessageHeadersProcessor); + } - private final OpenApiSpecification openApiSpec; - private final String operationId; + if (schemaValidation && getValidationContexts().stream() + .noneMatch(OpenApiMessageValidationContext.class::isInstance)) { + validate(openApi(openApiSpecification) + .schemaValidation(schemaValidation) + .build()); + } - private final HttpMessage httpMessage; + return super.doBuild(); + } + + public OpenApiServerRequestActionBuilder schemaValidation(boolean schemaValidation) { + this.schemaValidation = schemaValidation; + return this; + } - public OpenApiServerRequestMessageBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, - String operationId) { + private static class OpenApiServerRequestMessageBuilder extends HttpMessageBuilder { + + private final OpenApiSpecificationSource openApiSpecificationSource; + + public OpenApiServerRequestMessageBuilder(HttpMessage httpMessage, + OpenApiSpecificationSource openApiSpecificationSource) { super(httpMessage); - this.openApiSpec = openApiSpec; - this.operationId = operationId; - this.httpMessage = httpMessage; + + this.openApiSpecificationSource = openApiSpecificationSource; } @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); + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve(context.getReferenceResolver()); + context.setVariable(openApiSpecification.getUid(), openApiSpecification); 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 index 8a77e21e73..d7e509a254 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerResponseActionBuilder.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiServerResponseActionBuilder.java @@ -16,26 +16,48 @@ package org.citrusframework.openapi.actions; -import java.util.Map; -import java.util.Optional; -import java.util.regex.Pattern; +import static java.lang.Integer.parseInt; +import static java.util.Collections.singletonMap; +import static org.citrusframework.openapi.AutoFillType.NONE; +import static org.citrusframework.openapi.AutoFillType.REQUIRED; +import static org.citrusframework.openapi.OpenApiMessageType.RESPONSE; +import static org.citrusframework.openapi.OpenApiTestDataGenerator.createOutboundPayload; +import static org.citrusframework.openapi.OpenApiTestDataGenerator.createRandomValueExpression; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; -import io.apicurio.datamodels.openapi.models.OasDocument; import io.apicurio.datamodels.openapi.models.OasOperation; -import io.apicurio.datamodels.openapi.models.OasPathItem; import io.apicurio.datamodels.openapi.models.OasResponse; import io.apicurio.datamodels.openapi.models.OasSchema; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; import org.citrusframework.CitrusSettings; +import org.citrusframework.actions.SendMessageAction; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.http.actions.HttpServerResponseActionBuilder; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.http.message.HttpMessageHeaders; import org.citrusframework.message.Message; +import org.citrusframework.message.MessageHeaderBuilder; +import org.citrusframework.message.builder.DefaultHeaderBuilder; +import org.citrusframework.openapi.AutoFillType; +import org.citrusframework.openapi.OpenApiSettings; import org.citrusframework.openapi.OpenApiSpecification; -import org.citrusframework.openapi.OpenApiTestDataGenerator; +import org.citrusframework.openapi.model.OasAdapter; import org.citrusframework.openapi.model.OasModelHelper; -import org.springframework.http.HttpHeaders; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.citrusframework.openapi.validation.OpenApiOperationToMessageHeadersProcessor; +import org.citrusframework.openapi.validation.OpenApiValidationContext; import org.springframework.http.HttpStatus; /** @@ -43,91 +65,252 @@ */ public class OpenApiServerResponseActionBuilder extends HttpServerResponseActionBuilder { + private final OpenApiSpecificationSource openApiSpecificationSource; + private final String operationId; + private OpenApiOperationToMessageHeadersProcessor openApiOperationToMessageHeadersProcessor; + private boolean schemaValidation = true; + /** * Default constructor initializes http response message builder. */ - public OpenApiServerResponseActionBuilder(OpenApiSpecification openApiSpec, String operationId, String statusCode) { - this(new HttpMessage(), openApiSpec, operationId, statusCode); + public OpenApiServerResponseActionBuilder(OpenApiSpecificationSource openApiSpecificationSource, + String operationId, + String statusCode, + String accept) { + this(new HttpMessage(), openApiSpecificationSource, operationId, statusCode, accept); + } + + public OpenApiServerResponseActionBuilder(HttpMessage httpMessage, + OpenApiSpecificationSource openApiSpecificationSource, + String operationId, + String statusCode, + String accept) { + super(new OpenApiServerResponseMessageBuilder(httpMessage, openApiSpecificationSource, + operationId, statusCode, accept), httpMessage); + this.openApiSpecificationSource = openApiSpecificationSource; + this.operationId = operationId; + } + + public OpenApiServerResponseActionBuilder autoFill(AutoFillType autoFill) { + ((OpenApiServerResponseMessageBuilder)this.messageBuilderSupport.getMessageBuilder()).autoFill(autoFill); + return this; } - public OpenApiServerResponseActionBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, - String operationId, String statusCode) { - super(new OpenApiServerResponseMessageBuilder(httpMessage, openApiSpec, operationId, statusCode), httpMessage); + @Override + public SendMessageAction doBuild() { + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve( + referenceResolver); + + // Honor default enablement of schema validation + OpenApiValidationContext openApiValidationContext = openApiSpecification.getOpenApiValidationContext(); + if (openApiValidationContext != null && schemaValidation) { + schemaValidation = openApiValidationContext.isResponseValidationEnabled(); + } + + if (schemaValidation && !messageProcessors.contains( + openApiOperationToMessageHeadersProcessor)) { + openApiOperationToMessageHeadersProcessor = new OpenApiOperationToMessageHeadersProcessor( + openApiSpecification, operationId, RESPONSE); + process(openApiOperationToMessageHeadersProcessor); + } + + return super.doBuild(); + } + + public OpenApiServerResponseActionBuilder schemaValidation(boolean schemaValidation) { + this.schemaValidation = schemaValidation; + return this; + } + + /** + * By default, enable schema validation as the OpenAPI is always available. + */ + @Override + protected HttpMessageBuilderSupport createMessageBuilderSupport() { + HttpMessageBuilderSupport messageBuilderSupport = super.createMessageBuilderSupport(); + messageBuilderSupport.schemaValidation(true); + return messageBuilderSupport; + } + + public OpenApiServerResponseActionBuilder enableRandomGeneration(AutoFillType autoFillType) { + ((OpenApiServerResponseMessageBuilder) getMessageBuilderSupport().getMessageBuilder()).autoFill( + autoFillType); + return this; } private static class OpenApiServerResponseMessageBuilder extends HttpMessageBuilder { - private final OpenApiSpecification openApiSpec; + private static final Pattern STATUS_CODE_PATTERN = Pattern.compile("\\d+"); + + private final OpenApiSpecificationSource openApiSpecificationSource; private final String operationId; private final String statusCode; + private final String accept; - private final HttpMessage httpMessage; + private AutoFillType autoFill; - public OpenApiServerResponseMessageBuilder(HttpMessage httpMessage, OpenApiSpecification openApiSpec, - String operationId, String statusCode) { + public OpenApiServerResponseMessageBuilder(HttpMessage httpMessage, + OpenApiSpecificationSource openApiSpecificationSource, + String operationId, + String statusCode, + String accept) { super(httpMessage); - this.openApiSpec = openApiSpec; + this.openApiSpecificationSource = openApiSpecificationSource; this.operationId = operationId; this.statusCode = statusCode; - this.httpMessage = httpMessage; + this.accept = accept; + } + + public OpenApiServerResponseMessageBuilder autoFill(AutoFillType autoFillType) { + this.autoFill = autoFillType; + return this; } @Override public Message build(TestContext context, String messageType) { - OasOperation operation = null; - OasDocument oasDocument = openApiSpec.getOpenApiDoc(context); + OpenApiSpecification openApiSpecification = openApiSpecificationSource.resolve( + context.getReferenceResolver()); - for (OasPathItem path : OasModelHelper.getPathItems(oasDocument.paths)) { - Optional> operationEntry = OasModelHelper.getOperationMap(path).entrySet().stream() - .filter(op -> operationId.equals(op.getValue().operationId)) - .findFirst(); + if (autoFill == null) { + autoFill = OpenApiSettings.getResponseAutoFillRandomValues(); + } - if (operationEntry.isPresent()) { - operation = operationEntry.get().getValue(); - break; - } + if (STATUS_CODE_PATTERN.matcher(statusCode).matches()) { + getMessage().status(HttpStatus.valueOf(parseInt(statusCode))); + } else { + getMessage().status(OK); } - if (operation == null) { - throw new CitrusRuntimeException(("Unable to locate operation with id '%s' " + - "in OpenAPI specification %s").formatted(operationId, openApiSpec.getSpecUrl())); + List initialHeaderBuilders = new ArrayList<>(getHeaderBuilders()); + getHeaderBuilders().clear(); + + openApiSpecification.getOperation(operationId, context) + .ifPresentOrElse(operationPathAdapter -> + fillRandomData(openApiSpecification, operationPathAdapter, context), () -> { + throw new CitrusRuntimeException( + "Unable to locate operation with id '%s' in OpenAPI specification %s".formatted( + operationId, openApiSpecification.getSpecUrl())); + }); + + // Initial header builder need to be prepended, so that they can overwrite randomly generated headers. + getHeaderBuilders().addAll(initialHeaderBuilders); + + return super.build(context, messageType); + } + + private void fillRandomData(OpenApiSpecification openApiSpecification, + OperationPathAdapter operationPathAdapter, TestContext context) { + + if (operationPathAdapter.operation().responses != null) { + buildResponse(context, openApiSpecification, operationPathAdapter.operation()); } + } + + private void buildResponse(TestContext context, OpenApiSpecification openApiSpecification, + OasOperation operation) { + Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration( + openApiSpecification.getOpenApiDoc(context), operation, statusCode, null); - 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))); + if (responseForRandomGeneration.isPresent()) { + OasResponse oasResponse = responseForRandomGeneration.get(); + + if (autoFill != NONE) { + buildRandomHeaders(context, openApiSpecification, oasResponse); + buildRandomPayload(openApiSpecification, operation, oasResponse); } + + // Always override existing headers by context variables. This way we can also override random values. + Map headers = OasModelHelper.getHeaders(oasResponse); + headers.entrySet().stream() + .filter(entry -> context.getVariables().containsKey(entry.getKey())) + .forEach((entry -> addHeaderBuilder( + new DefaultHeaderBuilder(singletonMap(entry.getKey(), + CitrusSettings.VARIABLE_PREFIX + entry.getKey() + + CitrusSettings.VARIABLE_SUFFIX))))); + + } + } + + private void buildRandomHeaders(TestContext context, + OpenApiSpecification openApiSpecification, OasResponse response) { + if (autoFill == NONE) { + return; } - OasModelHelper.getResponseContentType(oasDocument, operation) - .ifPresent(contentType -> httpMessage.setHeader(HttpHeaders.CONTENT_TYPE, contentType)); + Set filteredHeaders = new HashSet<>(getMessage().getHeaders().keySet()); + Predicate> filteredHeadersPredicate = entry -> !filteredHeaders.contains( + entry.getKey()); - if (Pattern.compile("[0-9]+").matcher(statusCode).matches()) { - httpMessage.status(HttpStatus.valueOf(Integer.parseInt(statusCode))); + Map headersToFill; + if (autoFill == REQUIRED) { + headersToFill = OasModelHelper.getRequiredHeaders(response); } else { - httpMessage.status(HttpStatus.OK); + headersToFill = OasModelHelper.getHeaders(response); } - return super.build(context, messageType); + headersToFill.entrySet().stream() + .filter(filteredHeadersPredicate) + .forEach(entry -> addHeaderBuilder(new DefaultHeaderBuilder( + singletonMap(entry.getKey(), createRandomValueExpression(entry.getKey(), + entry.getValue(), + openApiSpecification, + context)))) + ); + + } + + private void buildRandomPayload(OpenApiSpecification openApiSpecification, + OasOperation operation, OasResponse response) { + Optional> schemaForMediaTypeOptional; + if (statusCode.startsWith("2")) { + // if status code is good, and we have an accept, try to get the media type. Note that only json and plain text can be generated randomly. + schemaForMediaTypeOptional = OasModelHelper.getRandomizableSchema(operation, response, accept); + } else { + // In the bad case, we cannot expect, that the accept type is the type which we must generate. + // We request the type supported by the response and the random generator (json and plain text). + schemaForMediaTypeOptional = OasModelHelper.getRandomizableSchema(operation, response, null); + } + + if (schemaForMediaTypeOptional.isPresent()) { + OasAdapter schemaForMediaType = schemaForMediaTypeOptional.get(); + if (getMessage().getPayload() == null || ( + getMessage().getPayload() instanceof String string && string.isEmpty())) { + createRandomPayload(getMessage(), openApiSpecification, schemaForMediaType); + } + + // If we have a schema and a media type and the content type has not yet been set, do it. + // If schema is null, we do not set the content type, as there is no content. + if (!getMessage().getHeaders().containsKey(HttpMessageHeaders.HTTP_CONTENT_TYPE) + && schemaForMediaType.adapted() != null && schemaForMediaType.node() != null) { + addHeaderBuilder(new DefaultHeaderBuilder( + singletonMap(HttpMessageHeaders.HTTP_CONTENT_TYPE, + schemaForMediaType.adapted()))); + } + } + } + + private void createRandomPayload(HttpMessage message, + OpenApiSpecification openApiSpecification, + OasAdapter schemaForMediaType) { + if (schemaForMediaType.node() == null) { + // No schema means no payload, no type + message.setPayload(null); + } else { + String mediaTypeName = schemaForMediaType.adapted(); + + // Support any json for now. Especially: application/json, application/json;charset=UTF-8 + if (mediaTypeName.toUpperCase().contains("JSON")) { + // Json Schema + message.setPayload( + createOutboundPayload(schemaForMediaType.node(), openApiSpecification)); + message.setHeader(HttpMessageHeaders.HTTP_CONTENT_TYPE, APPLICATION_JSON_VALUE); + } else if (TEXT_PLAIN_VALUE.equals(schemaForMediaType.adapted())) { + // Schema but plain text + message.setPayload( + createOutboundPayload(schemaForMediaType.node(), openApiSpecification)); + message.setHeader(HttpMessageHeaders.HTTP_CONTENT_TYPE, TEXT_PLAIN_VALUE); + } + } } } } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiSpecificationSource.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiSpecificationSource.java new file mode 100644 index 0000000000..bf86e0ce54 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/actions/OpenApiSpecificationSource.java @@ -0,0 +1,66 @@ +package org.citrusframework.openapi.actions; + +import static org.citrusframework.util.StringUtils.isEmpty; + +import java.util.Objects; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.openapi.OpenApiRepository; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.util.OpenApiUtils; +import org.citrusframework.spi.ReferenceResolver; + +/** + * The {@code OpenApiSpecificationSource} class is responsible for managing and resolving an + * {@link OpenApiSpecification} instance. It can either directly contain an + * {@link OpenApiSpecification} or reference one by an alias. + */ +public class OpenApiSpecificationSource { + + private OpenApiSpecification openApiSpecification; + + private String openApiAlias; + + private String httpClient; + + public OpenApiSpecificationSource(OpenApiSpecification openApiSpecification) { + this.openApiSpecification = openApiSpecification; + } + + public OpenApiSpecificationSource(String openApiAlias) { + this.openApiAlias = openApiAlias; + } + + public OpenApiSpecification resolve(ReferenceResolver resolver) { + if (openApiSpecification == null) { + + if (!isEmpty(openApiAlias)) { + openApiSpecification = resolver.resolveAll(OpenApiRepository.class).values() + .stream() + .map(openApiRepository -> openApiRepository.openApi(openApiAlias)). + filter(Objects::nonNull). + findFirst() + .orElseGet(() -> resolver.resolveAll(OpenApiSpecification.class).values().stream() + .filter(specification -> specification.getAliases().contains(openApiAlias)) + .findFirst() + .orElseThrow(() -> + new CitrusRuntimeException( + "Unable to resolve OpenApiSpecification from alias '%s'. Known aliases for open api specs are '%s'".formatted( + openApiAlias, OpenApiUtils.getKnownOpenApiAliases(resolver))) + )); + } else { + throw new CitrusRuntimeException( + "Unable to resolve OpenApiSpecification. Neither OpenAPI spec, nor OpenAPI alias are specified."); + } + } + + if (httpClient != null) { + openApiSpecification.setHttpClient(httpClient); + } + + return openApiSpecification; + } + + public void setHttpClient(String httpClient) { + this.httpClient = httpClient; + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasAdapter.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasAdapter.java new file mode 100644 index 0000000000..9933ecebc8 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasAdapter.java @@ -0,0 +1,7 @@ +package org.citrusframework.openapi.model; + +import io.apicurio.datamodels.core.models.Node; + +public record OasAdapter(S node, T adapted) { + +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java index 4f4bd6b8bf..5262bb7caf 100644 --- a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasModelHelper.java @@ -16,60 +16,125 @@ 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 static java.lang.String.format; +import static java.util.Collections.emptyList; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_ARRAY; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_OBJECT; +import static org.citrusframework.util.StringUtils.hasText; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; + +import io.apicurio.datamodels.combined.visitors.CombinedVisitorAdapter; +import io.apicurio.datamodels.openapi.io.OasDataModelWriter; 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.OasPaths; import io.apicurio.datamodels.openapi.models.OasResponse; +import io.apicurio.datamodels.openapi.models.OasResponses; import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v2.io.Oas20DataModelWriter; import io.apicurio.datamodels.openapi.v2.models.Oas20Document; import io.apicurio.datamodels.openapi.v2.models.Oas20Operation; +import io.apicurio.datamodels.openapi.v2.models.Oas20Parameter; import io.apicurio.datamodels.openapi.v2.models.Oas20Response; +import io.apicurio.datamodels.openapi.v2.models.Oas20Schema; +import io.apicurio.datamodels.openapi.v2.visitors.IOas20Visitor; +import io.apicurio.datamodels.openapi.v2.visitors.Oas20Traverser; +import io.apicurio.datamodels.openapi.v3.io.Oas30DataModelWriter; import io.apicurio.datamodels.openapi.v3.models.Oas30Document; import io.apicurio.datamodels.openapi.v3.models.Oas30Operation; +import io.apicurio.datamodels.openapi.v3.models.Oas30Parameter; import io.apicurio.datamodels.openapi.v3.models.Oas30Response; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import io.apicurio.datamodels.openapi.v3.visitors.IOas30Visitor; +import io.apicurio.datamodels.openapi.v3.visitors.Oas30Traverser; +import io.apicurio.datamodels.openapi.visitors.OasTraverser; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; import org.citrusframework.openapi.model.v2.Oas20ModelHelper; import org.citrusframework.openapi.model.v3.Oas30ModelHelper; +import org.citrusframework.util.StringUtils; public final class OasModelHelper { + public static final String DEFAULT = "default_"; + + /** + * List of preferred media types in the order of priority, + * used when no specific 'Accept' header is provided to determine the default response type. + */ + public static final List DEFAULT_ACCEPTED_MEDIA_TYPES = List.of(APPLICATION_JSON_VALUE, TEXT_PLAIN_VALUE); + 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); + public static boolean isObjectType(@Nullable OasSchema schema) { + return schema != null && TYPE_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); + public static boolean isArrayType(@Nullable OasSchema schema) { + return schema != null && TYPE_ARRAY.equals(schema.type); + } + + /** + * Determines if given schema is of type object array . + * + * @param schema to check + * @return true if given schema is an object array. + */ + public static boolean isObjectArrayType(@Nullable OasSchema schema) { + if (schema == null || !TYPE_ARRAY.equals(schema.type)) { + return false; + } + + Object items = schema.items; + if (items instanceof OasSchema oasSchema) { + return isObjectType(oasSchema); + } else if (items instanceof List list) { + return list.stream().allMatch(item -> item instanceof OasSchema oasSchema && isObjectType(oasSchema)); + } + + return false; } /** * 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 boolean isReferenceType(@Nullable OasSchema schema) { + return schema != null && schema.$ref != null; + } + + public static boolean isCompositeSchema(OasSchema schema) { + return delegate(schema, Oas20ModelHelper::isCompositeSchema, Oas30ModelHelper::isCompositeSchema); } public static String getHost(OasDocument openApiDoc) { @@ -80,6 +145,14 @@ public static List getSchemes(OasDocument openApiDoc) { return delegate(openApiDoc, Oas20ModelHelper::getSchemes, Oas30ModelHelper::getSchemes); } + public static OasSchema resolveSchema(OasDocument oasDocument, OasSchema schema) { + if (isReferenceType(schema)) { + return getSchemaDefinitions(oasDocument).get(getReferenceName(schema.$ref)); + } + + return schema; + } + public static String getBasePath(OasDocument openApiDoc) { return delegate(openApiDoc, Oas20ModelHelper::getBasePath, Oas30ModelHelper::getBasePath); } @@ -90,12 +163,13 @@ public static Map getSchemaDefinitions(OasDocument openApiDoc /** * 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 emptyList(); } return paths.getItems(); @@ -104,6 +178,7 @@ public static List getPathItems(OasPaths paths) { /** * 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. */ @@ -145,6 +220,7 @@ public static Map getOperationMap(OasPathItem pathItem) { * 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. */ @@ -160,6 +236,31 @@ public static Optional getSchema(OasResponse response) { return delegate(response, Oas20ModelHelper::getSchema, Oas30ModelHelper::getSchema); } + public static Optional> getSchema(OasOperation oasOperation, OasResponse response, List acceptedMediaTypes) { + if (oasOperation instanceof Oas20Operation oas20Operation && response instanceof Oas20Response oas20Response) { + return Oas20ModelHelper.getSchema(oas20Operation, oas20Response, acceptedMediaTypes); + } else if (oasOperation instanceof Oas30Operation oas30Operation && response instanceof Oas30Response oas30Response) { + return Oas30ModelHelper.getSchema(oas30Operation, oas30Response, acceptedMediaTypes); + } + throw new IllegalArgumentException(format("Unsupported operation response type: %s", response.getClass())); + } + + public static Optional> getRandomizableSchema(OasOperation oasOperation, OasResponse response, String acceptedMediaTypes) { + List acceptedRandomizableMediaTypes = OasModelHelper.toAcceptedRandomizableMediaTypes( + acceptedMediaTypes); + + if (oasOperation instanceof Oas20Operation oas20Operation && response instanceof Oas20Response oas20Response) { + return Oas20ModelHelper.getSchema(oas20Operation, oas20Response, acceptedRandomizableMediaTypes); + } else if (oasOperation instanceof Oas30Operation oas30Operation && response instanceof Oas30Response oas30Response) { + return Oas30ModelHelper.getSchema(oas30Operation, oas30Response, acceptedRandomizableMediaTypes); + } + throw new IllegalArgumentException(format("Unsupported operation response type: %s", response.getClass())); + } + + public static Optional getParameterSchema(OasParameter parameter) { + return delegate(parameter, Oas20ModelHelper::getParameterSchema, Oas30ModelHelper::getParameterSchema); + } + public static Map getRequiredHeaders(OasResponse response) { return delegate(response, Oas20ModelHelper::getHeaders, Oas30ModelHelper::getRequiredHeaders); } @@ -176,16 +277,88 @@ public static Optional getRequestBodySchema(OasDocument openApiDoc, O return delegate(openApiDoc, operation, Oas20ModelHelper::getRequestBodySchema, Oas30ModelHelper::getRequestBodySchema); } - public static Optional getResponseContentType(OasDocument openApiDoc, OasOperation operation) { - return delegate(openApiDoc, operation, Oas20ModelHelper::getResponseContentType, Oas30ModelHelper::getResponseContentType); + public static Collection getResponseTypes(OasOperation operation, OasResponse response) { + return delegate(operation, response, Oas20ModelHelper::getResponseTypes, Oas30ModelHelper::getResponseTypes); + } + + /** + * Determines the appropriate random response from an OpenAPI Specification operation based on the given status code. + * If a status code is specified, return the response for the specified status code. May be empty. + *

+ * If no exact match is found: + *

    + *
  • Fallback 1: Returns the 'default_' response if it exists.
  • + *
  • Fallback 2: Returns the first response object related to a 2xx status code that contains an acceptable schema for random message generation.
  • + *
  • Fallback 3: Returns the first response object related to a 2xx status code even without a schema. This is for operations that simply do not return anything else than a status code.
  • + *
  • Fallback 4: Returns the first response in the list of responses, no matter which schema.
  • + *
+ *

+ * Note that for Fallback 3 and 4, it is very likely, that there is no schema specified. It is expected, that an empty response is a viable response in these cases. + * + * @param openApiDoc The OpenAPI document containing the API specifications. + * @param operation The OAS operation for which to determine the response. + * @param statusCode The specific status code to match against responses, or {@code null} to search for any acceptable response. + * @param accept The mediatype accepted by the request + * @return An {@link Optional} containing the resolved {@link OasResponse} if found, or {@link Optional#empty()} otherwise. + */ + public static Optional getResponseForRandomGeneration(OasDocument openApiDoc, OasOperation operation, @Nullable String statusCode, @Nullable String accept) { + if (operation.responses == null || operation.responses.getResponses().isEmpty()) { + return Optional.empty(); + } + + // Resolve all references + Map responseMap = OasModelHelper.resolveResponses(openApiDoc, + operation.responses); + + // For a given status code, do not fall back + if (StringUtils.isNotEmpty(statusCode)) { + return Optional.ofNullable(responseMap.get(statusCode)); + } + + // Fallback 1: Pick the default if it exists + Optional response = Optional.ofNullable(responseMap.get(DEFAULT)); + + if (response.isEmpty()) { + // Fallback 2: Pick the response object related to the first 2xx, providing an accepted schema + + // Only accept responses that provide a schema for which we can actually provide a random message. + // That is json and plain/text. We prefer json. + List acceptedMediaTypesForRandomGeneration = toAcceptedRandomizableMediaTypes(accept); + + Predicate acceptableResponseWithRandomizableSchema = resp -> getSchema(operation, resp, acceptedMediaTypesForRandomGeneration).isPresent(); + response = responseMap.values().stream() + .filter(r -> r.getStatusCode() != null && r.getStatusCode().startsWith("2")) + .map(OasResponse.class::cast) + .filter(acceptableResponseWithRandomizableSchema) + .findFirst(); + } + + if (response.isEmpty()) { + // Fallback 3: Pick the response object related to the first 2xx (even without schema) + response = responseMap.values().stream() + .filter(r -> r.getStatusCode() != null && r.getStatusCode().startsWith("2")) + .map(OasResponse.class::cast) + .findFirst(); + } + + if (response.isEmpty()) { + // Fallback 4: Pick the first response no matter which schema + response = operation.responses.getResponses().stream() + .map(resp -> responseMap.get(resp.getStatusCode())) + .filter(Objects::nonNull) + .findFirst(); + } + + return response; } /** * Delegate method to version specific model helpers for Open API v2 or v3. - * @param openApiDoc the open api document either 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 + * @param generic return value * @return */ private static T delegate(OasDocument openApiDoc, Function oas20Function, Function oas30Function) { @@ -195,51 +368,112 @@ private static T delegate(OasDocument openApiDoc, Function return oas30Function.apply((Oas30Document) openApiDoc); } - throw new IllegalArgumentException(String.format("Unsupported Open API document type: %s", openApiDoc.getClass())); + throw new IllegalArgumentException(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 + * @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); + if (response instanceof Oas20Response oas20Response) { + return oas20Function.apply(oas20Response); + } else if (response instanceof Oas30Response oas30Response) { + return oas30Function.apply(oas30Response); } - throw new IllegalArgumentException(String.format("Unsupported operation response type: %s", response.getClass())); + throw new IllegalArgumentException(format("Unsupported operation response type: %s", response.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(OasOperation operation, OasResponse response, BiFunction oas20Function, BiFunction oas30Function) { + if (operation instanceof Oas20Operation oas20Operation && response instanceof Oas20Response oas20Response) { + return oas20Function.apply(oas20Operation, oas20Response); + } else if (operation instanceof Oas30Operation oas30Operation && response instanceof Oas30Response oas30Response) { + return oas30Function.apply(oas30Operation, oas30Response); + } + + throw new IllegalArgumentException(format("Unsupported operation response type: %s", response.getClass())); + } + + /** + * Delegate method to version specific model helpers for Open API v2 or v3. + * + * @param parameter + * @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(OasParameter parameter, Function oas20Function, Function oas30Function) { + if (parameter instanceof Oas20Parameter oas20Parameter) { + return oas20Function.apply(oas20Parameter); + } else if (parameter instanceof Oas30Parameter oas30Parameter) { + return oas30Function.apply(oas30Parameter); + } + + throw new IllegalArgumentException(format("Unsupported operation parameter type: %s", parameter.getClass())); + } + + /** + * Delegate method to version specific model helpers for Open API v2 or v3. + * + * @param schema + * @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(OasSchema schema, Function oas20Function, Function oas30Function) { + if (schema instanceof Oas20Schema oas20Schema) { + return oas20Function.apply(oas20Schema); + } else if (schema instanceof Oas30Schema oas30Schema) { + return oas30Function.apply(oas30Schema); + } + + throw new IllegalArgumentException(format("Unsupported operation parameter type: %s", schema.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 + * @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); + if (operation instanceof Oas20Operation oas20Operation) { + return oas20Function.apply(oas20Operation); + } else if (operation instanceof Oas30Operation oas30Operation) { + return oas30Function.apply(oas30Operation); } - throw new IllegalArgumentException(String.format("Unsupported operation type: %s", operation.getClass())); + throw new IllegalArgumentException(format("Unsupported operation type: %s", operation.getClass())); } /** * Delegate method to version specific model helpers for Open API v2 or v3. + * + * @param openApiDoc * @param operation * @param oas20Function function to apply in case of v2 * @param oas30Function function to apply in case of v3 - * @param generic return value + * @param generic return value * @return */ private static T delegate(OasDocument openApiDoc, OasOperation operation, BiFunction oas20Function, BiFunction oas30Function) { @@ -249,7 +483,7 @@ private static T delegate(OasDocument openApiDoc, OasOperation operation, Bi return oas30Function.apply((Oas30Document) openApiDoc, (Oas30Operation) operation); } - throw new IllegalArgumentException(String.format("Unsupported Open API document type: %s", openApiDoc.getClass())); + throw new IllegalArgumentException(format("Unsupported Open API document type: %s", openApiDoc.getClass())); } private static boolean isOas30(OasDocument openApiDoc) { @@ -259,4 +493,156 @@ private static boolean isOas30(OasDocument openApiDoc) { private static boolean isOas20(OasDocument openApiDoc) { return OpenApiVersion.fromDocumentType(openApiDoc).equals(OpenApiVersion.V2); } + + /** + * Resolves all responses in the given {@link OasResponses} instance. + * + *

+ * This method iterates over the responses contained in the {@link OasResponses} object. If a response has a reference + * (indicated by a non-null {@code $ref} field), it resolves the reference and adds the resolved response to the result list. + * Non-referenced responses are added to the result list as-is. The resulting map includes the default response under + * the key {@link OasModelHelper#DEFAULT}, if it exists. + *

+ * + * @param responses the {@link OasResponses} instance containing the responses to be resolved. + * @return a {@link List} of {@link OasResponse} instances, where all references have been resolved. + */ + private static Map resolveResponses(OasDocument openApiDoc, OasResponses responses) { + Function responseResolver = getResponseResolver(openApiDoc); + + Map responseMap = new HashMap<>(); + for (OasResponse response : responses.getResponses()) { + if (response.$ref != null) { + OasResponse resolved = responseResolver.apply(getReferenceName(response.$ref)); + if (resolved != null) { + // Note that we need to get the statusCode from the ref, as the referenced does not know about it. + responseMap.put(response.getStatusCode(), resolved); + } + } else { + responseMap.put(response.getStatusCode(), response); + } + } + + if (responses.default_ != null) { + if (responses.default_.$ref != null) { + OasResponse resolved = responseResolver.apply(responses.default_.$ref); + if (resolved != null) { + responseMap.put(DEFAULT, resolved); + } + } else { + responseMap.put(DEFAULT, responses.default_); + } + } + + return responseMap; + } + + private static Function getResponseResolver(OasDocument openApiDoc) { + return delegate(openApiDoc, + doc -> (responseRef -> doc.responses.getResponse(OasModelHelper.getReferenceName(responseRef))), + doc -> (responseRef -> doc.components.responses.get(OasModelHelper.getReferenceName(responseRef)))); + } + + /** + * Traverses the OAS document and applies the given visitor to each OAS operation found. + * This method uses the provided {@link OasOperationVisitor} to process each operation within the paths of the OAS document. + * + * @param oasDocument the OAS document to traverse + * @param visitor the visitor to apply to each OAS operation + */ + public static void visitOasOperations(OasDocument oasDocument, OasOperationVisitor visitor) { + if (oasDocument == null || visitor == null || oasDocument.paths == null) { + return; + } + + oasDocument.paths.accept(new CombinedVisitorAdapter() { + + @Override + public void visitPaths(OasPaths oasPaths) { + oasPaths.getPathItems().forEach(oasPathItem -> oasPathItem.accept(this)); + } + + @Override + public void visitPathItem(OasPathItem oasPathItem) { + String path = oasPathItem.getPath(); + + if (StringUtils.isEmpty(path)) { + return; + } + + getOperationMap(oasPathItem).values() + .forEach(oasOperation -> visitor.visit(oasPathItem, oasOperation)); + + } + }); + } + + /** + * Resolves and normalizes a list of accepted media types. If the input list is null, + * returns null. Otherwise, splits each media type string by comma, trims whitespace, + * and collects them into a list of normalized types. + * + * @param acceptedMediaTypes List of accepted media types, may be null. + * @return Normalized list of media types, or null if input is null. + */ + public static List resolveAllTypes(@Nullable List acceptedMediaTypes) { + if (acceptedMediaTypes == null) { + return emptyList(); + } + + return acceptedMediaTypes.stream() + .flatMap(types -> Arrays.stream(types.split(","))).map(String::trim).toList(); + } + + /** + * Converts an OpenAPI document into a JSON string representation. + *

+ * + * @param openApiDoc the OpenAPI document to be converted to JSON. It must be of type {@link Oas20Document} + * or {@link Oas30Document}. + * @return the JSON string representation of the OpenAPI document. + * @throws IllegalArgumentException if the provided OpenAPI document is neither an instance of {@link Oas20Document} + * nor {@link Oas30Document}. + */ + public static String toJson(OasDocument openApiDoc) { + OasDataModelWriter writer; + OasTraverser oasTraverser; + if (openApiDoc instanceof Oas20Document) { + writer = new Oas20DataModelWriter(); + oasTraverser = new Oas20Traverser((IOas20Visitor) writer); + } else if (openApiDoc instanceof Oas30Document) { + writer = new Oas30DataModelWriter(); + oasTraverser = new Oas30Traverser((IOas30Visitor) writer); + } else { + throw new IllegalArgumentException(format("Unsupported Open API document type: %s", openApiDoc.getClass())); + } + + oasTraverser.traverse(openApiDoc); + return writer.getResult().toString(); + } + + /** + * Converts the given accept header value into a list of accepted media types + * that can be used for randomizable content generation. + * + *

If the accept header is non-empty, the method resolves and filters + * the media types to include only those that contain "json" or "text/plain", as these are the + * only ones we can generate randomly. If the accept header is empty or null, it defaults to a + * predefined list of accepted media types. + * + * @param accept the value of the accept header, specifying preferred media types + * @return a list of media types suitable for randomizable content generation + */ + public static List toAcceptedRandomizableMediaTypes(String accept) { + List acceptedMediaTypesForRandomGeneration = new ArrayList<>(); + if (hasText(accept)) { + List acceptedMediaTypes = OasModelHelper.resolveAllTypes(List.of(accept)); + acceptedMediaTypes.stream().filter(mediaType -> hasText(mediaType) && mediaType.contains("json")).forEach(acceptedMediaTypesForRandomGeneration::add); + acceptedMediaTypes.stream().filter(mediaType -> hasText(mediaType) && mediaType.contains("text/plain")).forEach(acceptedMediaTypesForRandomGeneration::add); + } else { + acceptedMediaTypesForRandomGeneration.addAll(DEFAULT_ACCEPTED_MEDIA_TYPES); + } + return acceptedMediaTypesForRandomGeneration; + } + } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasOperationVisitor.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasOperationVisitor.java new file mode 100644 index 0000000000..85e4cfbb35 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OasOperationVisitor.java @@ -0,0 +1,28 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.model; + +import io.apicurio.datamodels.openapi.models.OasOperation; +import io.apicurio.datamodels.openapi.models.OasPathItem; + +/** + * The {@code OasOperationVisitor} interface defines a visitor pattern for operations on OAS (OpenAPI Specification) path items and operations. + */ +public interface OasOperationVisitor { + + void visit(OasPathItem oasPathItem, OasOperation oasOperation); +} 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 index 13e4a74008..dbfcee3270 100644 --- 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 @@ -16,12 +16,12 @@ 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; +import java.util.Arrays; + /** * List of supported OpenAPI specification versions and their corresponding model document types. */ @@ -37,8 +37,8 @@ public enum OpenApiVersion { 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")); + .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/OperationPathAdapter.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OperationPathAdapter.java new file mode 100644 index 0000000000..3a04d57a84 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/model/OperationPathAdapter.java @@ -0,0 +1,39 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.model; + +import io.apicurio.datamodels.openapi.models.OasOperation; + +import static java.lang.String.format; +import static org.citrusframework.openapi.util.OpenApiUtils.getMethodPath; + +/** + * Adapts the different paths associated with an OpenAPI operation to the {@link OasOperation}. + * This record holds the API path, context path, full path, and the associated {@link OasOperation} object. + * + * @param apiPath The API path for the operation. + * @param contextPath The context path in which the API is rooted. + * @param fullPath The full path combining context path and API path. + * @param operation The {@link OasOperation} object representing the operation details. + */ +public record OperationPathAdapter(String apiPath, String contextPath, String fullPath, OasOperation operation, String uniqueOperationId) { + + @Override + public String toString() { + return format("%s (%s)", getMethodPath(operation.getMethod(), apiPath), operation.operationId); + } +} 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 index a1a5055146..9a36de2e6a 100644 --- 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 @@ -16,21 +16,32 @@ 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.Oas20Items; import io.apicurio.datamodels.openapi.v2.models.Oas20Operation; +import io.apicurio.datamodels.openapi.v2.models.Oas20Parameter; import io.apicurio.datamodels.openapi.v2.models.Oas20Response; import io.apicurio.datamodels.openapi.v2.models.Oas20Schema; +import io.apicurio.datamodels.openapi.v2.models.Oas20Schema.Oas20AllOfSchema; import io.apicurio.datamodels.openapi.v2.models.Oas20SchemaDefinition; +import jakarta.annotation.Nullable; +import org.citrusframework.openapi.model.OasAdapter; +import org.citrusframework.openapi.model.OasModelHelper; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; public final class Oas20ModelHelper { @@ -64,7 +75,28 @@ public static Optional getSchema(Oas20Response response) { return Optional.ofNullable(response.schema); } - public static Optional getRequestBodySchema(Oas20Document openApiDoc, Oas20Operation operation) { + public static Optional> getSchema(Oas20Operation oas20Operation, Oas20Response response, List acceptedMediaTypes) { + acceptedMediaTypes = OasModelHelper.resolveAllTypes(acceptedMediaTypes); + acceptedMediaTypes = acceptedMediaTypes.isEmpty() ? OasModelHelper.DEFAULT_ACCEPTED_MEDIA_TYPES : acceptedMediaTypes; + + OasSchema selectedSchema = response.schema; + String selectedMediaType = null; + if (oas20Operation.produces != null && !oas20Operation.produces.isEmpty()) { + selectedMediaType = acceptedMediaTypes.stream() + .filter(type -> !isFormDataMediaType(type)) + .filter(type -> oas20Operation.produces.contains(type)).findFirst() + .orElse(null); + } + + return selectedSchema == null && selectedMediaType == null ? Optional.empty() : Optional.of(new OasAdapter<>(selectedSchema, selectedMediaType)); + } + + public static boolean isCompositeSchema(Oas20Schema schema) { + // Note that oneOf and anyOf is not supported by Oas20. + return schema instanceof Oas20AllOfSchema; + } + + public static Optional getRequestBodySchema(@Nullable Oas20Document ignoredOpenApiDoc, Oas20Operation operation) { if (operation.parameters == null) { return Optional.empty(); } @@ -86,12 +118,11 @@ public static Optional getRequestContentType(Oas20Operation operation) { return Optional.empty(); } - public static Optional getResponseContentType(Oas20Document openApiDoc, Oas20Operation operation) { - if (operation.produces != null) { - return Optional.of(operation.produces.get(0)); + public static Collection getResponseTypes(Oas20Operation operation, @Nullable Oas20Response ignoredResponse) { + if (operation == null) { + return Collections.emptyList(); } - - return Optional.empty(); + return operation.produces; } public static Map getHeaders(Oas20Response response) { @@ -103,30 +134,63 @@ public static Map getHeaders(Oas20Response response) { .collect(Collectors.toMap(OasHeader::getName, Oas20ModelHelper::getHeaderSchema)); } + private static boolean isFormDataMediaType(String type) { + return Arrays.asList(APPLICATION_FORM_URLENCODED_VALUE, MULTIPART_FORM_DATA_VALUE).contains(type); + } + + /** + * If the header already contains a schema (and it is an instance of {@link Oas20Header}), this schema is returned. + * Otherwise, a new {@link Oas20Header} is created based on the properties of the parameter and returned. + * + * @param header the {@link Oas20Header} from which to extract or create the schema + * @return an {@link Optional} containing the extracted or newly created {@link OasSchema} + */ private static OasSchema getHeaderSchema(Oas20Header header) { + return createOas20Schema(header.getName(), header.type, header.format, header.items, header.multipleOf, header.default_, header.enum_, header.pattern, header.description, header.uniqueItems, header.maximum, header.maxItems, header.maxLength, header.exclusiveMaximum, header.minimum, header.minItems, header.minLength, header.exclusiveMinimum); + } + + /** + * If the parameter already contains a schema (and it is an instance of {@link Oas20Schema}), this schema is returned. + * Otherwise, a new {@link Oas20Schema} is created based on the properties of the parameter and returned. + * + * @param parameter the {@link Oas20Parameter} from which to extract or create the schema + * @return an {@link Optional} containing the extracted or newly created {@link OasSchema} + */ + public static Optional getParameterSchema(Oas20Parameter parameter) { + if (parameter.schema instanceof Oas20Schema oasSchema) { + return Optional.of(oasSchema); + } + + Oas20Schema schema = createOas20Schema(parameter.getName(), parameter.type, parameter.format, parameter.items, parameter.multipleOf, parameter.default_, parameter.enum_, parameter.pattern, parameter.description, parameter.uniqueItems, parameter.maximum, parameter.maxItems, parameter.maxLength, parameter.exclusiveMaximum, parameter.minimum, parameter.minItems, parameter.minLength, parameter.exclusiveMinimum); + return Optional.of(schema); + } + + private static Oas20Schema createOas20Schema(String name, String type, String format, Oas20Items items, Number multipleOf, Object aDefault, List anEnum, String pattern, String description, Boolean uniqueItems, Number maximum, Number maxItems, Number maxLength, Boolean exclusiveMaximum, Number minimum, Number minItems, Number minLength, Boolean exclusiveMinimum) { 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; + + schema.title = name; + schema.type = type; + schema.format = format; + schema.items = items; + schema.multipleOf = multipleOf; + + schema.default_ = aDefault; + schema.enum_ = anEnum; + + schema.pattern = pattern; + schema.description = description; + schema.uniqueItems = uniqueItems; + + schema.maximum = maximum; + schema.maxItems = maxItems; + schema.maxLength = maxLength; + schema.exclusiveMaximum = exclusiveMaximum; + + schema.minimum = minimum; + schema.minItems = minItems; + schema.minLength = minLength; + schema.exclusiveMinimum = 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 index fec97b4c3b..ca1973d971 100644 --- 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 @@ -16,34 +16,42 @@ 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 static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static java.util.stream.Collectors.toMap; +import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; 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.Oas30Parameter; import io.apicurio.datamodels.openapi.v3.models.Oas30RequestBody; import io.apicurio.datamodels.openapi.v3.models.Oas30Response; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import org.citrusframework.openapi.model.OasAdapter; import org.citrusframework.openapi.model.OasModelHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public final class Oas30ModelHelper { - /** Logger */ - private static final Logger LOG = LoggerFactory.getLogger(Oas30ModelHelper.class); + private static final Logger logger = LoggerFactory.getLogger(Oas30ModelHelper.class); + + public static final String NO_URL_ERROR_MESSAGE = "Unable to determine base path from server URL: %s"; private Oas30ModelHelper() { // utility class @@ -56,11 +64,7 @@ public static String getHost(Oas30Document openApiDoc) { 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 URI.create(serverUrl).getHost(); } return "localhost"; @@ -68,21 +72,25 @@ public static String getHost(Oas30Document openApiDoc) { public static List getSchemes(Oas30Document openApiDoc) { if (openApiDoc.servers == null || openApiDoc.servers.isEmpty()) { - return Collections.emptyList(); + return 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 URI.create(serverUrl).toURL().getProtocol(); + } catch (Exception e) { + logger.warn(format(NO_URL_ERROR_MESSAGE, serverUrl), e); return null; } }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .filter(Objects::nonNull) + .toList(); + } + + public static boolean isCompositeSchema(Oas30Schema schema) { + return schema.anyOf != null || schema.oneOf != null || schema.allOf != null; } public static String getBasePath(Oas30Document openApiDoc) { @@ -95,11 +103,7 @@ public static String getBasePath(Oas30Document openApiDoc) { 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)); - } + basePath = URI.create(serverUrl).getPath(); } else { basePath = serverUrl; } @@ -109,12 +113,12 @@ public static String getBasePath(Oas30Document openApiDoc) { public static Map getSchemaDefinitions(Oas30Document openApiDoc) { if (openApiDoc.components == null || openApiDoc.components.schemas == null) { - return Collections.emptyMap(); + return emptyMap(); } return openApiDoc.components.schemas.entrySet() .stream() - .collect(Collectors.toMap(Map.Entry::getKey, entry -> (OasSchema) entry.getValue())); + .collect(toMap(Map.Entry::getKey, Entry::getValue)); } public static Optional getSchema(Oas30Response response) { @@ -131,6 +135,31 @@ public static Optional getSchema(Oas30Response response) { .findFirst(); } + public static Optional> getSchema(Oas30Operation ignoredOas30Operation, Oas30Response response, List acceptedMediaTypes) { + acceptedMediaTypes = OasModelHelper.resolveAllTypes(acceptedMediaTypes); + acceptedMediaTypes = acceptedMediaTypes.isEmpty() ? OasModelHelper.DEFAULT_ACCEPTED_MEDIA_TYPES : acceptedMediaTypes; + + Map content = response.content; + if (content == null) { + return Optional.empty(); + } + + String selectedMediaType = null; + Oas30Schema selectedSchema = null; + for (String type : acceptedMediaTypes) { + if (!isFormDataMediaType(type)) { + Oas30MediaType oas30MediaType = content.get(type); + if (oas30MediaType != null) { + selectedMediaType = type; + selectedSchema = oas30MediaType.schema; + break; + } + } + } + + return selectedSchema == null && selectedMediaType == null ? Optional.empty() : Optional.of(new OasAdapter<>(selectedSchema, selectedMediaType)); + } + public static Optional getRequestBodySchema(Oas30Document openApiDoc, Oas30Operation operation) { if (operation.requestBody == null) { return Optional.empty(); @@ -169,81 +198,50 @@ public static Optional getRequestContentType(Oas30Operation operation) { .findFirst(); } - public static Optional getResponseContentType(Oas30Document openApiDoc, Oas30Operation operation) { - if (operation.responses == null) { - return Optional.empty(); + public static Collection getResponseTypes(Oas30Operation operation, Oas30Response response) { + if (operation == null) { + return emptySet(); } - 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()); - + return response.content != null ? response.content.keySet() : emptyList(); } public static Map getRequiredHeaders(Oas30Response response) { if (response.headers == null) { - return Collections.emptyMap(); + return emptyMap(); } return response.headers.entrySet() .stream() .filter(entry -> Boolean.TRUE.equals(entry.getValue().required)) - .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().schema)); + .collect(toMap(Map.Entry::getKey, entry -> entry.getValue().schema)); } public static Map getHeaders(Oas30Response response) { if (response.headers == null) { - return Collections.emptyMap(); + return emptyMap(); } return response.headers.entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().schema)); + .stream() + .collect(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); + return Arrays.asList(APPLICATION_FORM_URLENCODED_VALUE, MULTIPART_FORM_DATA_VALUE).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()) { + for (Map.Entry variable : server.variables.entrySet()) { String defaultValue = Optional.ofNullable(variable.getValue().default_).orElse(""); url = url.replaceAll(String.format("\\{%s\\}", variable.getKey()), defaultValue); } @@ -251,4 +249,8 @@ private static String resolveUrl(Server server) { return url; } + + public static Optional getParameterSchema(Oas30Parameter parameter) { + return Optional.ofNullable((OasSchema) parameter.schema); + } } diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomArrayGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomArrayGenerator.java new file mode 100644 index 0000000000..0f78f9c7d8 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomArrayGenerator.java @@ -0,0 +1,50 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import org.citrusframework.openapi.model.OasModelHelper; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * A generator for producing random arrays based on an OpenAPI schema. This class extends the + * {@link RandomGenerator} and provides a specific implementation for generating random arrays + * with constraints defined in the schema. + * + *

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

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

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

+ */ +public class RandomCompositeGenerator extends RandomGenerator { + + private static void createOneOf(RandomContext randomContext, List schemas) { + int schemaIndex = ThreadLocalRandom.current().nextInt(schemas.size()); + randomContext.getRandomModelBuilder().object(() -> randomContext.generate(schemas.get(schemaIndex))); + } + + private static void createAnyOf(RandomContext randomContext, Oas30Schema schema) { + randomContext.getRandomModelBuilder().object(() -> { + boolean anyAdded = false; + for (OasSchema oneSchema : schema.anyOf) { + if (ThreadLocalRandom.current().nextBoolean()) { + randomContext.generate(oneSchema); + anyAdded = true; + } + } + + // Add at least one + if (!anyAdded) { + createOneOf(randomContext, schema.anyOf); + } + }); + } + + private static Map createAllOf(RandomContext randomContext, OasSchema schema) { + Map allOf = new HashMap<>(); + + randomContext.getRandomModelBuilder().object(() -> { + for (OasSchema oneSchema : schema.allOf) { + randomContext.generate(oneSchema); + } + }); + + return allOf; + } + + @Override + public boolean handles(OasSchema other) { + return OasModelHelper.isCompositeSchema(other); + } + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + if (!isEmpty(schema.allOf)) { + createAllOf(randomContext, schema); + } else if (schema instanceof Oas30Schema oas30Schema && !isEmpty(oas30Schema.anyOf)) { + createAnyOf(randomContext, oas30Schema); + } else if (schema instanceof Oas30Schema oas30Schema && !isEmpty(oas30Schema.oneOf)) { + createOneOf(randomContext, oas30Schema.oneOf); + } + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomConfiguration.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomConfiguration.java new file mode 100644 index 0000000000..c7d1a64346 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomConfiguration.java @@ -0,0 +1,64 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_DATE; +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_DATE_TIME; +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_UUID; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_BOOLEAN; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_STRING; +import static org.citrusframework.openapi.random.RandomGenerator.ANY; +import static org.citrusframework.openapi.random.RandomGenerator.NOOP_RANDOM_GENERATOR; +import static org.citrusframework.openapi.random.RandomGeneratorBuilder.randomGeneratorBuilder; + +/** + * Configuration class that initializes and manages a list of random generators + * for producing random data based on an OpenAPI schema. This class is a singleton + * and provides a static instance {@code RANDOM_CONFIGURATION} for global access. + */ +public class RandomConfiguration { + + public static final RandomConfiguration RANDOM_CONFIGURATION = new RandomConfiguration(); + + // Patterns for random generation (potentially simplified) + private static final String EMAIL_PATTERN = "[a-z]{5,15}\\.?[a-z]{5,15}\\@[a-z]{5,15}\\.[a-z]{2}"; + private static final String URI_PATTERN = "((http|https)://[a-zA-Z0-9-]+(\\.[a-zA-Z]{2,})+(/[a-zA-Z0-9-]+){1,6})|(file:///[a-zA-Z0-9-]+(/[a-zA-Z0-9-]+){1,6})"; + private static final String HOSTNAME_PATTERN = "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])"; + private static final String IPV4_PATTERN = "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"; + private static final String IPV6_PATTERN = "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"; + private final List randomGenerators; + + private RandomConfiguration() { + List generators = new ArrayList<>(); + + // Note that the order of generators in the list is relevant, as the list is traversed from start to end, to find the first matching generator for a schema, and some generators match for less significant schemas. + generators.add(new RandomEnumGenerator()); + generators.add(randomGeneratorBuilder(TYPE_STRING, FORMAT_DATE).build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:currentDate('yyyy-MM-dd')"))); + generators.add(randomGeneratorBuilder(TYPE_STRING, FORMAT_DATE_TIME).build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:currentDate('yyyy-MM-dd'T'hh:mm:ssZ')"))); + generators.add(randomGeneratorBuilder(TYPE_STRING, FORMAT_UUID).build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomUUID()"))); + generators.add(randomGeneratorBuilder(TYPE_STRING, "email").build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('" + EMAIL_PATTERN + "')"))); + generators.add(randomGeneratorBuilder(TYPE_STRING, "uri").build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('" + URI_PATTERN + "')"))); + generators.add(randomGeneratorBuilder(TYPE_STRING, "hostname").build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('" + HOSTNAME_PATTERN + "')"))); + generators.add(randomGeneratorBuilder(TYPE_STRING, "ipv4").build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('" + IPV4_PATTERN + "')"))); + generators.add(randomGeneratorBuilder(TYPE_STRING, "ipv6").build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('" + IPV6_PATTERN + "')"))); + generators.add(randomGeneratorBuilder().withType(TYPE_STRING).withPattern(ANY).build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomPattern('" + schema.pattern + "')"))); + generators.add(randomGeneratorBuilder().withType(TYPE_BOOLEAN).build((randomContext, schema) -> randomContext.getRandomModelBuilder().appendSimple("citrus:randomEnumValue('true', 'false')"))); + generators.add(new RandomStringGenerator()); + generators.add(new RandomCompositeGenerator()); + generators.add(new RandomNumberGenerator()); + generators.add(new RandomObjectGenerator()); + generators.add(new RandomArrayGenerator()); + + randomGenerators = Collections.unmodifiableList(generators); + } + + public RandomGenerator getGenerator(OasSchema oasSchema) { + return randomGenerators.stream().filter(generator -> generator.handles(oasSchema)) + .findFirst() + .orElse(NOOP_RANDOM_GENERATOR); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomContext.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomContext.java new file mode 100644 index 0000000000..7262f57aff --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomContext.java @@ -0,0 +1,119 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import jakarta.annotation.Nullable; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.model.OasModelHelper; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static org.citrusframework.openapi.random.RandomConfiguration.RANDOM_CONFIGURATION; + +/** + * Context class for generating random values based on an OpenAPI specification. + * This class manages the state and configuration needed to generate random values + * for various schemas defined in the OpenAPI specification. + */ +public class RandomContext { + + private final OpenApiSpecification specification; + private final RandomModelBuilder randomModelBuilder; + + /** + * Cache for storing variable during random value generation. + */ + private final Map contextVariables = new HashMap<>(); + private Map schemaDefinitions; + + /** + * Constructs a default RandomContext backed by no specification. Note, that this context can not + * resolve referenced schemas, as no specification is available. + */ + public RandomContext() { + this.randomModelBuilder = new RandomModelBuilder(false); + this.specification = null; + } + + /** + * Constructs a new RandomContext with the specified OpenAPI specification and quote option. + * + * @param specification the OpenAPI specification + * @param quote whether to quote the generated random values + */ + public RandomContext(OpenApiSpecification specification, boolean quote) { + this.specification = specification; + this.randomModelBuilder = new RandomModelBuilder(quote); + } + + /** + * Returns the OpenAPI specification associated with this context. + * + * @return the OpenAPI specification + */ + public OpenApiSpecification getSpecification() { + return specification; + } + + /** + * Returns the RandomModelBuilder associated with this context. + * + * @return the RandomModelBuilder + */ + public RandomModelBuilder getRandomModelBuilder() { + return randomModelBuilder; + } + + /** + * Generates random values based on the specified schema. + * + * @param schema the schema to generate random values for + */ + public void generate(OasSchema schema) { + doGenerate(resolveSchema(schema)); + } + + void doGenerate(OasSchema resolvedSchema) { + RANDOM_CONFIGURATION.getGenerator(resolvedSchema).generate(this, resolvedSchema); + } + + /** + * Resolves a schema, handling reference schemas by fetching the referenced schema definition. + * + * @param schema the schema to resolve + * @return the resolved schema + */ + @Nullable OasSchema resolveSchema(OasSchema schema) { + if (OasModelHelper.isReferenceType(schema)) { + if (schemaDefinitions == null) { + schemaDefinitions = getSchemaDefinitions(); + } + schema = schemaDefinitions.get(OasModelHelper.getReferenceName(schema.$ref)); + } + return schema; + } + + /** + * Returns the schema definitions from the specified OpenAPI document. + * + * @return a map of schema definitions + */ + Map getSchemaDefinitions() { + return specification != null ? OasModelHelper.getSchemaDefinitions(specification.getOpenApiDoc(null)) : Collections.emptyMap(); + } + + /** + * Retrieves a context variable by key, computing its value if necessary using the provided mapping function. + * + * @param the type of the context variable + * @param key the key of the context variable + * @param mappingFunction the function to compute the value if it is not present + * @return the context variable value + */ + public T get(String key, Function mappingFunction) { + //noinspection unchecked + return (T) contextVariables.computeIfAbsent(key, mappingFunction); + } +} \ No newline at end of file diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomElement.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomElement.java new file mode 100644 index 0000000000..d928d4ab47 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomElement.java @@ -0,0 +1,118 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.random; + +import java.util.ArrayList; +import java.util.LinkedHashMap; + +/** + * Interface representing a random element in a JSON structure. This interface provides default + * methods to push values into the element, which can be overridden by implementing classes. + */ +public interface RandomElement { + + default void push(Object value) { + throw new UnsupportedOperationException(); + } + + default void push(String key, Object value) { + throw new UnsupportedOperationException(); + } + + /** + * A random element representing an array. Array elements can be of type String (native + * attribute) or {@link RandomElement}. + */ + class RandomList extends ArrayList implements RandomElement { + + @Override + public void push(Object value) { + add(value); + } + + @Override + public void push(String key, Object value) { + if (!isEmpty()) { + Object lastElement = get(size() - 1); + if (lastElement instanceof RandomElement randomElement) { + randomElement.push(key, value); + } + } + } + } + + /** + * A random object representing a JSON object, with attributes stored as key-value pairs. Values + * are of type String (simple attributes) or {@link RandomElement}. + */ + class RandomObject extends LinkedHashMap implements RandomElement { + + @Override + public void push(String key, Object value) { + put(key, value); + } + + @Override + public void push(Object value) { + if (value instanceof RandomObject randomObject) { + this.putAll(randomObject); + return; + + } + + throw new IllegalArgumentException("Value must be an instance of RandomObject"); + } + } + + /** + * A random value that either holds a String (simple property) or a random element. + */ + class RandomValue implements RandomElement { + + private Object value; + + public RandomValue() { + } + + public RandomValue(Object value) { + this.value = value; + } + + public Object getValue() { + return value; + } + + @Override + public void push(Object pushedValue) { + if (value instanceof RandomElement randomElement) { + randomElement.push(pushedValue); + } else { + this.value = pushedValue; + } + } + + @Override + public void push(String key, Object pushedValue) { + if (value instanceof RandomElement randomElement) { + randomElement.push(key, pushedValue); + } else { + throw new IllegalStateException("Cannot push key/value to value: " + value); + } + } + + } +} \ No newline at end of file diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomEnumGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomEnumGenerator.java new file mode 100644 index 0000000000..994b78227e --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomEnumGenerator.java @@ -0,0 +1,26 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; + +import java.util.List; + +import static java.util.stream.Collectors.joining; + +public class RandomEnumGenerator extends RandomGenerator { + + @Override + public boolean handles(OasSchema other) { + return other.enum_ != null; + } + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + List anEnum = schema.enum_; + if (anEnum != null) { + String enumValues = schema.enum_.stream() + .map(value -> "'" + value + "'") + .collect(joining(",")); + randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomEnumValue(%s)".formatted(enumValues)); + } + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGenerator.java new file mode 100644 index 0000000000..8a2bfa0bce --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGenerator.java @@ -0,0 +1,60 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; + +import java.util.Objects; + +/** + * Abstract base class for generators that produce random data based on an OpenAPI schema. + * Subclasses must implement the {@link #generate} method to provide specific random data generation logic. + * + *

The class provides methods for determining if a generator can handle a given schema, + * based on the schema type, format, pattern, and enum constraints. + */ +public abstract class RandomGenerator { + + public static final String ANY = "$ANY$"; + public static final RandomGenerator NOOP_RANDOM_GENERATOR = new RandomGenerator() { + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + // Do nothing + } + }; + private final OasSchema schema; + + protected RandomGenerator() { + this.schema = null; + } + + protected RandomGenerator(OasSchema schema) { + this.schema = schema; + } + + public boolean handles(OasSchema other) { + if (other == null || schema == null) { + return false; + } + + if (ANY.equals(schema.type) || Objects.equals(schema.type, other.type)) { + if (schema.format != null) { + return (ANY.equals(schema.format) && other.format != null) || Objects.equals(schema.format, other.format); + } + + if (schema.pattern != null) { + return (ANY.equals(schema.pattern) && other.pattern != null) || Objects.equals(schema.pattern, other.pattern); + } + + if (schema.enum_ != null && other.enum_ != null) { + return true; + } + + return true; + } + + return false; + } + + abstract void generate(RandomContext randomContext, OasSchema schema); + +} \ No newline at end of file diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGeneratorBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGeneratorBuilder.java new file mode 100644 index 0000000000..7cf024b672 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomGeneratorBuilder.java @@ -0,0 +1,62 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; + +import java.util.Collections; +import java.util.function.BiConsumer; + +/** + * A simple builder for building {@link java.util.random.RandomGenerator}s. + */ +public class RandomGeneratorBuilder { + + private final OasSchema schema = new Oas30Schema(); + + private RandomGeneratorBuilder() { + } + + static RandomGeneratorBuilder randomGeneratorBuilder() { + return new RandomGeneratorBuilder(); + } + + static RandomGeneratorBuilder randomGeneratorBuilder(String type, String format) { + return new RandomGeneratorBuilder().with(type, format); + } + + RandomGeneratorBuilder with(String type, String format) { + schema.type = type; + schema.format = format; + return this; + } + + RandomGeneratorBuilder withType(String type) { + schema.type = type; + return this; + } + + RandomGeneratorBuilder withFormat(String format) { + schema.format = format; + return this; + } + + RandomGeneratorBuilder withPattern(String pattern) { + schema.pattern = pattern; + return this; + } + + RandomGeneratorBuilder withEnum() { + schema.enum_ = Collections.emptyList(); + return this; + } + + RandomGenerator build(BiConsumer consumer) { + return new RandomGenerator(schema) { + @Override + void generate(RandomContext randomContext, OasSchema schema) { + consumer.accept(randomContext, schema); + } + }; + } + +} \ No newline at end of file diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelBuilder.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelBuilder.java new file mode 100644 index 0000000000..790e943ec0 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelBuilder.java @@ -0,0 +1,133 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.random; + +import org.citrusframework.openapi.random.RandomElement.RandomList; +import org.citrusframework.openapi.random.RandomElement.RandomObject; +import org.citrusframework.openapi.random.RandomElement.RandomValue; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * RandomModelBuilder is a class for building random JSON models. It supports adding simple values, + * objects, properties, and arrays to the JSON structure. The final model can be converted to a JSON + * string using the `writeToJson` method. + *

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

+ * Example usage: + *

+ * RandomModelBuilder builder = new RandomModelBuilder();
+ * builder.object(() -> {
+ *     builder.property("key1", () -> builder.appendSimple("value1"));
+ *     builder.property("key2", () -> builder.array(() -> {
+ *         builder.appendSimple("value2");
+ *         builder.appendSimple("value3");
+ *     }));
+ * });
+ * String json = builder.writeToJson();
+ * 
+ */ +public class RandomModelBuilder { + + final Deque deque = new ArrayDeque<>(); + + private final boolean quote; + + /** + * Creates a {@link RandomModelBuilder} in respective quoting mode. + * Quoting should be activated in case an object is created by the builder. In this case, + * all properties added by respective "quoted" methods, will be quoted. + * + * @param quote whether to run the builder in quoting mode or not. + */ + public RandomModelBuilder(boolean quote) { + deque.push(new RandomValue()); + this.quote = quote; + } + + public String write() { + return RandomModelWriter.toString(this); + } + + /** + * Append the simpleValue as is, no quoting + */ + public void appendSimple(String simpleValue) { + if (deque.isEmpty()) { + deque.push(new RandomValue(simpleValue)); + } else { + deque.peek().push(simpleValue); + } + } + + /** + * If the builder is in quoting mode, the native value will be quoted, otherwise it will be + * added as ist. + * s + * + * @param simpleValue + */ + public void appendSimpleQuoted(String simpleValue) { + appendSimple(quote(simpleValue)); + } + + public void object(Runnable objectBuilder) { + assertItemsInDequeOrThrow(); + + RandomObject randomObject = new RandomObject(); + deque.peek().push(randomObject); + objectBuilder.run(); + } + + public void property(String key, Runnable valueBuilder) { + assertItemsInDequeOrThrow(); + + RandomValue randomValue = new RandomValue(); + deque.peek().push(key, randomValue); + + deque.push(randomValue); + valueBuilder.run(); + deque.pop(); + } + + public void array(Runnable arrayBuilder) { + assertItemsInDequeOrThrow(); + + RandomList randomList = new RandomList(); + deque.peek().push(randomList); + + // For a list, we need to push the list to the queue. This is because when the builder adds elements + // to the list, and we are dealing with nested lists, we can otherwise not distinguish whether to put + // an element into the list or into the nested list. + deque.push(randomList); + arrayBuilder.run(); + deque.pop(); + } + + private String quote(String text) { + return quote ? String.format("\"%s\"", text) : text; + } + + private void assertItemsInDequeOrThrow() { + if (deque.isEmpty()) { + throw new IllegalStateException("Encountered empty stack!"); + } + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelWriter.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelWriter.java new file mode 100644 index 0000000000..c48dbbaa27 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomModelWriter.java @@ -0,0 +1,112 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.random; + +import org.citrusframework.openapi.random.RandomElement.RandomValue; + +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static org.citrusframework.util.StringUtils.trimTrailingComma; + +/** + * Utility class for converting a {@link RandomModelBuilder} to its string representation. + * This class provides static methods to serialize the model built by {@link RandomModelBuilder}. + */ +final class RandomModelWriter { + + private RandomModelWriter() { + // static access only + } + + static String toString(RandomModelBuilder randomModelBuilder) { + StringBuilder builder = new StringBuilder(); + appendObject(builder, randomModelBuilder.deque); + return builder.toString(); + } + + private static void appendObject(StringBuilder builder, Object object) { + if (object instanceof Deque deque) { + while (!deque.isEmpty()) { + appendObject(builder, deque.pop()); + } + } else if (object instanceof Map map) { + // noinspection unchecked + appendMap(builder, (Map) map); + } else if (object instanceof List list) { + appendArray(builder, list); + } else if (object instanceof String string) { + builder.append(string); + } else if (object instanceof RandomValue randomValue) { + appendObject(builder, randomValue.getValue()); + } + } + + private static void appendArray(StringBuilder builder, List list) { + builder.append("["); + list.forEach(listValue -> { + appendObject(builder, listValue); + builder.append(","); + }); + trimTrailingComma(builder); + builder.append("]"); + } + + private static void appendMap(StringBuilder builder, Map map) { + if (map.size() == 1) { + Entry entry = map.entrySet().iterator().next(); + String key = entry.getKey(); + Object value = entry.getValue(); + + if ("ARRAY".equals(key)) { + appendObject(builder, value); + } else if ("NATIVE".equals(key)) { + builder.append(value); + } else { + appendJsonObject(builder, map); + } + } else { + appendJsonObject(builder, map); + } + } + + private static void appendJsonObject(StringBuilder builder, Map map) { + builder.append("{"); + for (Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + builder.append("\""); + builder.append(key); + builder.append("\": "); + + if (value instanceof String) { + builder.append(value); + } else if (value instanceof Map) { + appendObject(builder, value); + } else if (value instanceof RandomValue randomValue) { + appendObject(builder, randomValue.getValue()); + } + + builder.append(","); + } + trimTrailingComma(builder); + + builder.append("}"); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomNumberGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomNumberGenerator.java new file mode 100644 index 0000000000..131f3c949e --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomNumberGenerator.java @@ -0,0 +1,152 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import org.citrusframework.openapi.util.OpenApiUtils; + +import java.math.BigDecimal; + +import static java.lang.Boolean.TRUE; +import static java.lang.String.format; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_INTEGER; + +/** + * A generator for producing random numbers based on an OpenAPI schema. This class extends the + * {@link RandomGenerator} and provides a specific implementation for generating random numbers with + * constraints defined in the schema. + *

+ * Supported constraints: + *

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

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

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

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

+ */ +public class RandomObjectGenerator extends RandomGenerator { + + private static final String OBJECT_STACK = "OBJECT_STACK"; + + private static final OasSchema OBJECT_SCHEMA = new Oas30Schema(); + + static { + OBJECT_SCHEMA.type = TYPE_OBJECT; + } + + public RandomObjectGenerator() { + super(OBJECT_SCHEMA); + } + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + Deque objectStack = randomContext.get(OBJECT_STACK, k -> new ArrayDeque<>()); + + if (objectStack.contains(schema)) { + // If we have already created this schema, we are very likely in a recursion and need to stop. + return; + } + + objectStack.push(schema); + randomContext.getRandomModelBuilder().object(() -> { + if (schema.properties != null) { + for (Map.Entry entry : schema.properties.entrySet()) { + if (randomContext.getSpecification().isGenerateOptionalFields() + || OpenApiUtils.isRequired(schema, entry.getKey())) { + randomContext.getRandomModelBuilder() + .property(entry.getKey(), () -> randomContext.generate(entry.getValue())); + } + } + } + }); + + objectStack.pop(); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomStringGenerator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomStringGenerator.java new file mode 100644 index 0000000000..3a592bb3e4 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/random/RandomStringGenerator.java @@ -0,0 +1,40 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; + +import static org.citrusframework.openapi.OpenApiConstants.TYPE_STRING; + +/** + * A generator for producing random strings based on an OpenAPI schema. + * This class extends the {@link RandomGenerator} and provides a specific implementation + * for generating random strings with constraints defined in the schema. + */ +public class RandomStringGenerator extends RandomGenerator { + + private static final OasSchema STRING_SCHEMA = new Oas30Schema(); + + static { + STRING_SCHEMA.type = TYPE_STRING; + } + + public RandomStringGenerator() { + super(STRING_SCHEMA); + } + + @Override + void generate(RandomContext randomContext, OasSchema schema) { + int min = 1; + int max = 10; + + if (schema.minLength != null && schema.minLength.intValue() > 0) { + min = schema.minLength.intValue(); + } + + if (schema.maxLength != null && schema.maxLength.intValue() > 0) { + max = schema.maxLength.intValue(); + } + + randomContext.getRandomModelBuilder().appendSimpleQuoted("citrus:randomString(%s,MIXED,true,%s)".formatted(max, min)); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java new file mode 100644 index 0000000000..f2fad4dec0 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/util/OpenApiUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.util; + +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; +import static org.citrusframework.util.StringUtils.hasText; + +import io.apicurio.datamodels.openapi.models.OasOperation; +import io.apicurio.datamodels.openapi.models.OasSchema; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.citrusframework.openapi.OpenApiConstants; +import org.citrusframework.openapi.OpenApiRepository; +import org.citrusframework.spi.ReferenceResolver; + +public final class OpenApiUtils { + + private OpenApiUtils() { + // Static access only + } + + public static String getMethodPath(@Nonnull String method, @Nonnull String path) { + if (hasText(path) && path.startsWith("/")) { + path = path.substring(1); + } + return format("%s_/%s", method.toUpperCase(), path); + } + + /** + * @return a unique scenario id for the {@link OasOperation} + */ + public static String createFullPathOperationIdentifier(OasOperation oasOperation, String path) { + return createFullPathOperationIdentifier(oasOperation.getMethod().toUpperCase(), path); + } + + /** + * @return a unique scenario id for the {@link OasOperation} + */ + public static String createFullPathOperationIdentifier(String method, String path) { + return format("%s_%s", method.toUpperCase(), path); + } + + public static boolean isAnyNumberScheme(@Nullable OasSchema schema) { + return schema != null + && (OpenApiConstants.TYPE_INTEGER.equalsIgnoreCase(schema.type) + || OpenApiConstants.TYPE_NUMBER.equalsIgnoreCase(schema.type)); + } + + /** + * Checks if given field name is in list of required fields for this schema. + */ + public static boolean isRequired(OasSchema schema, String field) { + if (schema.required == null) { + return true; + } + + return schema.required.contains(field); + } + + /** + * Retrieves all known OpenAPI aliases from {@link org.citrusframework.openapi.OpenApiSpecification}s + * registered in {@link OpenApiRepository}s. + * + * @param resolver the {@code ReferenceResolver} to use for resolving {@code OpenApiRepository} instances. + * @return a comma-separated string of all known OpenAPI aliases. + */ + public static String getKnownOpenApiAliases(ReferenceResolver resolver) { + return resolver.resolveAll(OpenApiRepository.class).values() + .stream() + .flatMap(openApiRepository -> openApiRepository.getOpenApiSpecifications().stream()) + .flatMap(spec -> spec.getAliases().stream()) + .collect(joining(", ")); + } + +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiMessageValidationContext.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiMessageValidationContext.java new file mode 100644 index 0000000000..2436ccd687 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiMessageValidationContext.java @@ -0,0 +1,124 @@ +package org.citrusframework.openapi.validation; + +import org.citrusframework.openapi.OpenApiSettings; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.validation.context.DefaultValidationContext; +import org.citrusframework.validation.context.SchemaValidationContext; +import org.citrusframework.validation.context.ValidationContext; + +/** + * Validation context holding OpenAPI specific validation information. + * + * @since 4.3 + */ +public class OpenApiMessageValidationContext extends DefaultValidationContext implements SchemaValidationContext { + + /** + * Should message be validated with its schema definition. This is enabled with respect to + * global settings, which are true by default. as only messages processed by open api actions + * will be processed and validation information will be derived from open api spec. + * + *

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

Note that a message validation context is explicitly created only for send messages, + * whereas default request validation enabled is chosen as default value. + */ + private Boolean schemaValidation = OpenApiSettings.isRequestValidationEnabledGlobally(); + + public static OpenApiMessageValidationContext.Builder openApi(OpenApiSpecification openApiSpecification) { + Builder builder = new Builder(); + builder.openApiSpecification = openApiSpecification; + return builder; + } + + public static OpenApiMessageValidationContext.Builder openApi() { + return new Builder(); + } + + /** + * Sets schema validation enabled/disabled for this message. + */ + public OpenApiMessageValidationContext.Builder schemaValidation(final boolean enabled) { + this.schemaValidation = enabled; + return this; + } + + /** + * Not used for open api validation. Schema is automatically derived from associated openApiSpecification. + */ + @Override + public OpenApiMessageValidationContext.Builder schema(final String schemaName) { + this.operationId = schemaName; + return this; + } + + /** + * Not used for open api validation. Schema is automatically derived from associated openApiSpecification. + */ + @Override + public OpenApiMessageValidationContext.Builder schemaRepository(final String schemaRepository) { + openApiSpecificationId = schemaRepository; + return this; + } + + @Override + public OpenApiMessageValidationContext build() { + return new OpenApiMessageValidationContext(this); + } + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiOperationToMessageHeadersProcessor.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiOperationToMessageHeadersProcessor.java new file mode 100644 index 0000000000..69cfdfa145 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiOperationToMessageHeadersProcessor.java @@ -0,0 +1,60 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.validation; + +import org.citrusframework.context.TestContext; +import org.citrusframework.message.Message; +import org.citrusframework.message.MessageProcessor; +import org.citrusframework.openapi.OpenApiMessageType; +import org.citrusframework.openapi.OpenApiSpecification; + +import static org.citrusframework.openapi.OpenApiMessageHeaders.OAS_MESSAGE_TYPE; +import static org.citrusframework.openapi.OpenApiMessageHeaders.OAS_SPECIFICATION_ID; +import static org.citrusframework.openapi.OpenApiMessageHeaders.OAS_UNIQUE_OPERATION_ID; + +/** + * {@code MessageProcessor} that prepares the message for OpenAPI validation by setting respective + * message headers. + */ +public class OpenApiOperationToMessageHeadersProcessor implements MessageProcessor { + + private final OpenApiSpecification openApiSpecification; + + private final String operationId; + + private final OpenApiMessageType type; + + public OpenApiOperationToMessageHeadersProcessor(OpenApiSpecification openApiSpecification, + String operationId, + OpenApiMessageType type) { + this.operationId = operationId; + this.openApiSpecification = openApiSpecification; + this.type = type; + } + + @Override + public void process(Message message, TestContext context) { + openApiSpecification + .getOperation(operationId, context) + .ifPresent(operationPathAdapter -> { + message.setHeader(OAS_SPECIFICATION_ID, openApiSpecification.getUid()); + // Store the uniqueId of the operation, rather than the operationId, to avoid clashes. + message.setHeader(OAS_UNIQUE_OPERATION_ID, operationPathAdapter.uniqueOperationId()); + message.setHeader(OAS_MESSAGE_TYPE, type.toHeaderName()); + }); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiRequestValidator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiRequestValidator.java new file mode 100644 index 0000000000..a924470547 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiRequestValidator.java @@ -0,0 +1,144 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.validation; + +import com.atlassian.oai.validator.model.Request; +import com.atlassian.oai.validator.model.SimpleRequest; +import com.atlassian.oai.validator.report.ValidationReport; +import org.citrusframework.exceptions.ValidationException; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.http.message.HttpMessageUtils; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.springframework.util.MultiValueMap; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; + +import static org.citrusframework.util.FileUtils.getDefaultCharset; + +/** + * Specific validator that uses Atlassian's Swagger Request Validator + * underneath. It is responsible for validating HTTP requests against + * an OpenAPI specification using the + * provided {@code OpenApiInteractionValidator}. + */ +public class OpenApiRequestValidator extends OpenApiValidator { + + public OpenApiRequestValidator(OpenApiSpecification openApiSpecification) { + super(openApiSpecification); + } + + @Override + protected String getType() { + return "request"; + } + + public void validateRequest(OperationPathAdapter operationPathAdapter, HttpMessage requestMessage) { + if (openApiInteractionValidator != null) { + ValidationReport validationReport = openApiInteractionValidator.validateRequest( + createRequestFromMessage(operationPathAdapter, requestMessage)); + if (validationReport.hasErrors()) { + throw new ValidationException( + constructErrorMessage(operationPathAdapter, validationReport)); + } + } + } + + public ValidationReport validateRequestToReport(OperationPathAdapter operationPathAdapter, + HttpMessage requestMessage) { + if (openApiInteractionValidator != null) { + return openApiInteractionValidator.validateRequest( + createRequestFromMessage(operationPathAdapter, requestMessage)); + } + + return ValidationReport.empty(); + } + + Request createRequestFromMessage(OperationPathAdapter operationPathAdapter, + HttpMessage httpMessage) { + var payload = httpMessage.getPayload(); + + String contextPath = operationPathAdapter.contextPath(); + String requestUri = (String) httpMessage.getHeader(HttpMessageHeaders.HTTP_REQUEST_URI); + if (contextPath != null && requestUri.startsWith(contextPath)) { + requestUri = requestUri.substring(contextPath.length()); + } + + SimpleRequest.Builder requestBuilder = new SimpleRequest.Builder( + httpMessage.getRequestMethod().asHttpMethod().name(), requestUri + ); + + if (payload != null) { + requestBuilder = requestBuilder.withBody(convertPayload(payload)); + } + + SimpleRequest.Builder finalRequestBuilder = requestBuilder; + finalRequestBuilder.withAccept(httpMessage.getAccept()); + + HttpMessageUtils.getQueryParameterMap(httpMessage) + .forEach((key, value) -> finalRequestBuilder.withQueryParam(key, new ArrayList<>(value))); + + httpMessage.getHeaders().forEach((key, value) -> { + if (value instanceof Collection collection) { + collection.forEach(v -> finalRequestBuilder.withHeader(key, v != null ? v.toString() : null)); + } else { + finalRequestBuilder.withHeader(key, value != null ? value.toString() : null); + } + }); + + httpMessage.getCookies().forEach(cookie -> finalRequestBuilder.withHeader("Cookie", URLDecoder.decode(cookie.getName() + "=" + cookie.getValue(), + getDefaultCharset()))); + + return requestBuilder.build(); + } + + private String convertPayload(Object payload) { + if (payload instanceof MultiValueMap multiValueMap) { + return serializeForm(multiValueMap, StandardCharsets.UTF_8); + } + + return payload != null ? payload.toString() : null; + } + + /** + * We cannot validate a MultiValueMap. The map will later on be converted to a string representation + * by Spring. For validation, we need to mimic this transformation here. + * + * @see org.springframework.http.converter.FormHttpMessageConverter + */ + private String serializeForm(MultiValueMap formData, Charset charset) { + StringBuilder builder = new StringBuilder(); + formData.forEach((name, values) -> values.forEach(value -> { + if (!builder.isEmpty()) { + builder.append('&'); + } + builder.append(URLEncoder.encode(name.toString(), charset)); + if (value != null) { + builder.append('='); + builder.append(URLEncoder.encode(String.valueOf(value), charset)); + } + })); + + return builder.toString(); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiResponseValidator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiResponseValidator.java new file mode 100644 index 0000000000..9d7a64bc37 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiResponseValidator.java @@ -0,0 +1,87 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.validation; + +import com.atlassian.oai.validator.model.Request.Method; +import com.atlassian.oai.validator.model.Response; +import com.atlassian.oai.validator.model.SimpleResponse; +import com.atlassian.oai.validator.report.ValidationReport; +import org.citrusframework.exceptions.ValidationException; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.springframework.http.HttpStatusCode; + +/** + * Specific validator, that facilitates the use of Atlassian's Swagger Request Validator, + * and delegates validation of OpenApi requests to instances of {@link OpenApiRequestValidator}. + */ +public class OpenApiResponseValidator extends OpenApiValidator { + + public OpenApiResponseValidator(OpenApiSpecification openApiSpecification) { + super(openApiSpecification); + } + + @Override + protected String getType() { + return "response"; + } + + public void validateResponse(OperationPathAdapter operationPathAdapter, HttpMessage httpMessage) { + if (openApiInteractionValidator != null) { + HttpStatusCode statusCode = httpMessage.getStatusCode(); + Response response = createResponseFromMessage(httpMessage, statusCode != null ? statusCode.value() : null); + + ValidationReport validationReport = openApiInteractionValidator.validateResponse( + operationPathAdapter.apiPath(), + Method.valueOf(operationPathAdapter.operation().getMethod().toUpperCase()), + response); + if (validationReport.hasErrors()) { + throw new ValidationException(constructErrorMessage(operationPathAdapter, validationReport)); + } + } + } + + public ValidationReport validateResponseToReport(OperationPathAdapter operationPathAdapter, HttpMessage httpMessage) { + if (openApiInteractionValidator != null) { + HttpStatusCode statusCode = httpMessage.getStatusCode(); + Response response = createResponseFromMessage(httpMessage, statusCode != null ? statusCode.value() : null); + + return openApiInteractionValidator.validateResponse( + operationPathAdapter.apiPath(), + Method.valueOf(operationPathAdapter.operation().getMethod().toUpperCase()), + response); + } + + return ValidationReport.empty(); + } + + Response createResponseFromMessage(HttpMessage message, Integer statusCode) { + var payload = message.getPayload(); + SimpleResponse.Builder responseBuilder = new SimpleResponse.Builder(statusCode); + + if (payload != null) { + responseBuilder = responseBuilder.withBody(payload.toString()); + } + + SimpleResponse.Builder finalResponseBuilder = responseBuilder; + message.getHeaders().forEach((key, value) -> finalResponseBuilder.withHeader(key, + value != null ? value.toString() : null)); + + return responseBuilder.build(); + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiSchemaValidation.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiSchemaValidation.java new file mode 100644 index 0000000000..1118117faa --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiSchemaValidation.java @@ -0,0 +1,220 @@ +package org.citrusframework.openapi.validation; + +import static java.util.Collections.emptyList; +import static org.citrusframework.openapi.OpenApiMessageHeaders.OAS_SPECIFICATION_ID; +import static org.citrusframework.openapi.OpenApiMessageHeaders.OAS_UNIQUE_OPERATION_ID; +import static org.citrusframework.util.StringUtils.isNotEmpty; + +import com.atlassian.oai.validator.report.ValidationReport; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.exceptions.ValidationException; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.message.Message; +import org.citrusframework.openapi.OpenApiMessageHeaders; +import org.citrusframework.openapi.OpenApiRepository; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.citrusframework.openapi.util.OpenApiUtils; +import org.citrusframework.openapi.validation.OpenApiMessageValidationContext.Builder; +import org.citrusframework.util.IsJsonPredicate; +import org.citrusframework.validation.AbstractMessageValidator; +import org.citrusframework.validation.SchemaValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class OpenApiSchemaValidation extends + AbstractMessageValidator implements + SchemaValidator { + + private static final Logger logger = LoggerFactory.getLogger(OpenApiSchemaValidation.class); + + @Override + protected Class getRequiredValidationContextType() { + return OpenApiMessageValidationContext.class; + } + + @Override + public void validateMessage(Message receivedMessage, + Message controlMessage, + TestContext context, + OpenApiMessageValidationContext validationContext) { + // No control message validation, only schema validation + validate(receivedMessage, context, validationContext); + } + + @Override + public void validate(Message message, TestContext context, + OpenApiMessageValidationContext validationContext) { + logger.debug("Starting OpenApi schema validation ..."); + + // In case we have a redirect header, we cannot validate the message, as we have to expect, that the message has no valid response. + if (!(message instanceof HttpMessage httpMessage) || httpMessage.getHeader("Location") != null) { + return; + } + + ValidationReportData validationReportData = validate(context, + httpMessage, + findSchemaRepositories(context), + validationContext); + + if (validationReportData != null + && validationReportData.report != null + && validationReportData.report.hasErrors()) { + if (logger.isErrorEnabled()) { + logger.error( + "Failed to validate Json schema for message:\n{}\nand origin path:\n{}", + httpMessage.getPayload(String.class), httpMessage.getPath()); + } + throw new ValidationException(constructErrorMessage(validationReportData)); + } + + logger.debug("Json schema validation successful: All values OK"); + } + + /** + * When the correct {@link OpenApiRepository} is installed, schema validation can be performed + * on any JSON message where a response can be derived based on the request method, URL, and + * OpenAPI operation. Validation is also possible if the operationId is explicitly specified, as + * set by an OpenApiClient. + * + * @param messageType The type of message to be validated. + * @param message The message content for validation. + * @return Validation result or status. + */ + @Override + public boolean supportsMessageType(String messageType, Message message) { + return "JSON".equals(messageType) + || ( + message != null && (IsJsonPredicate.getInstance().test(message.getPayload(String.class)) + || message.getHeader(OAS_UNIQUE_OPERATION_ID) != null)); + } + + private String constructErrorMessage(ValidationReportData validationReportData) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("OpenApi "); + stringBuilder.append(validationReportData.type); + stringBuilder.append(" validation failed for operation: "); + stringBuilder.append(validationReportData.operationPathAdapter); + validationReportData.report.getMessages() + .forEach(message -> stringBuilder.append("\n\t").append(message)); + return stringBuilder.toString(); + } + + /** + * Find json schema repositories in test context. + */ + private List findSchemaRepositories(TestContext context) { + return new ArrayList<>( + context.getReferenceResolver().resolveAll(OpenApiRepository.class).values()); + } + + @Nullable + private ValidationReportData validate(TestContext context, + HttpMessage message, + List schemaRepositories, + OpenApiMessageValidationContext validationContext) { + + schemaRepositories = schemaRepositories != null ? schemaRepositories : emptyList(); + + if (!validationContext.isSchemaValidationEnabled()) { + return null; + } else { + String operationId = validationContext.getSchema() != null ? validationContext.getSchema() + : (String) message.getHeader(OAS_UNIQUE_OPERATION_ID); + String specificationId = validationContext.getSchemaRepository() != null + ? validationContext.getSchemaRepository() + : (String) message.getHeader(OAS_SPECIFICATION_ID); + + if (isNotEmpty(specificationId) && isNotEmpty(operationId)) { + return validateOpenApiOperation(context, message, schemaRepositories, + specificationId, operationId); + } + + return null; + + } + } + + private ValidationReportData validateOpenApiOperation(TestContext context, HttpMessage message, + List schemaRepositories, String specificationId, String operationId) { + OpenApiSpecification openApiSpecification = schemaRepositories + .stream() + .map(repository -> repository.openApi(specificationId)) + .filter(Objects::nonNull) + .findFirst() + .orElse((OpenApiSpecification) context.getVariables().get(specificationId)); + + if (openApiSpecification == null) { + throw new CitrusRuntimeException(""" + Unable to derive OpenAPI spec for operation '%s' for validation of message from available " + schema repositories. Known repository aliases are: %s""".formatted( + operationId, + OpenApiUtils.getKnownOpenApiAliases(context.getReferenceResolver()))); + } + + OperationPathAdapter operationPathAdapter = openApiSpecification.getOperation( + operationId, context) + .orElseThrow(() -> new CitrusRuntimeException( + "Unexpectedly could not resolve operation path adapter for operationId: " + + operationId)); + + ValidationReportData validationReportData = null; + if (isRequestMessage(message)) { + ValidationReport validationReport = new OpenApiRequestValidator( + openApiSpecification) + .validateRequestToReport(operationPathAdapter, message); + validationReportData = new ValidationReportData(operationPathAdapter, "request", + validationReport); + } else if (isResponseMessage(message)) { + ValidationReport validationReport = new OpenApiResponseValidator( + openApiSpecification) + .validateResponseToReport(operationPathAdapter, message); + validationReportData = new ValidationReportData(operationPathAdapter, "response", + validationReport); + } + return validationReportData; + } + + private boolean isResponseMessage(HttpMessage message) { + return OpenApiMessageHeaders.RESPONSE_TYPE.equals( + message.getHeader(OpenApiMessageHeaders.OAS_MESSAGE_TYPE)) || message.getHeader( + HttpMessageHeaders.HTTP_STATUS_CODE) != null; + } + + private boolean isRequestMessage(HttpMessage message) { + return OpenApiMessageHeaders.REQUEST_TYPE.equals( + message.getHeader(OpenApiMessageHeaders.OAS_MESSAGE_TYPE)) || message.getHeader( + HttpMessageHeaders.HTTP_STATUS_CODE) == null; + } + + @Override + public boolean canValidate(Message message, boolean schemaValidationEnabled) { + return schemaValidationEnabled + && message instanceof HttpMessage httpMessage + && (isRequestMessage(httpMessage) || isResponseMessage(httpMessage)); + } + + @Override + public void validate(Message message, TestContext context, String schemaRepository, + String schema) { + if (!(message instanceof HttpMessage)) { + return; + } + + validate(message, context, + new Builder().schemaValidation(true).schema(schema).schemaRepository(schemaRepository) + .build()); + + } + + private record ValidationReportData(OperationPathAdapter operationPathAdapter, String type, + ValidationReport report) { + + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidationContext.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidationContext.java new file mode 100644 index 0000000000..edf447c6ad --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidationContext.java @@ -0,0 +1,130 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.validation; + +import static org.citrusframework.openapi.OpenApiSettings.isRequestValidationEnabledGlobally; +import static org.citrusframework.openapi.OpenApiSettings.isResponseValidationEnabledGlobally; + +import com.atlassian.oai.validator.OpenApiInteractionValidator; +import com.atlassian.oai.validator.model.ApiOperation; +import com.atlassian.oai.validator.model.Request; +import com.atlassian.oai.validator.model.Response; +import com.atlassian.oai.validator.report.MessageResolver; +import com.atlassian.oai.validator.report.ValidationReport.Message; +import com.atlassian.oai.validator.schema.SchemaValidator; +import com.atlassian.oai.validator.schema.SwaggerV20Library; +import com.atlassian.oai.validator.whitelist.ValidationErrorsWhitelist; +import com.atlassian.oai.validator.whitelist.rule.WhitelistRule; +import io.swagger.v3.oas.models.OpenAPI; +import jakarta.annotation.Nonnull; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Represents the context for OpenAPI validation, providing configuration and validators for request and response validation. + * This context maintains settings for both request and response validation, which can be controlled at an instance level. + * By default, these settings are initialized based on a global configuration. + */ +public class OpenApiValidationContext { + + private static final List WHITELIST_RULES = List.of( + // Filtered because in general OpenAPI is not required to specify all response codes. + // So this should not be considered as validation error. + IgnoreByKeyWhitelistRule.ignoreByKey("Allow unknown response status rule", "validation.response.status.unknown") + ); + + private final OpenAPI openApi; + + private OpenApiInteractionValidator openApiInteractionValidator; + + private SchemaValidator schemaValidator; + + private boolean responseValidationEnabled = isResponseValidationEnabledGlobally(); + + private boolean requestValidationEnabled = isRequestValidationEnabledGlobally(); + + public OpenApiValidationContext(OpenAPI openApi) { + this.openApi = openApi; + } + + public OpenAPI getSwaggerOpenApi() { + return openApi; + } + + private static final ValidationErrorsWhitelist validationErrorsWhitelist; + + static { + ValidationErrorsWhitelist whiteList = ValidationErrorsWhitelist.create(); + for (IgnoreByKeyWhitelistRule rule : WHITELIST_RULES) { + whiteList = whiteList.withRule(rule.name, rule); + } + validationErrorsWhitelist = whiteList; + } + + public synchronized @Nonnull OpenApiInteractionValidator getOpenApiInteractionValidator() { + if (openApiInteractionValidator == null) { + openApiInteractionValidator = new OpenApiInteractionValidator.Builder().withApi(openApi).withWhitelist(validationErrorsWhitelist).build(); + } + return openApiInteractionValidator; + } + + public synchronized @Nonnull SchemaValidator getSchemaValidator() { + if (schemaValidator == null) { + schemaValidator = new SchemaValidator(openApi, new MessageResolver(), SwaggerV20Library::schemaFactory); + } + return schemaValidator; + } + + public boolean isResponseValidationEnabled() { + return responseValidationEnabled; + } + + public void setResponseValidationEnabled(boolean responseValidationEnabled) { + this.responseValidationEnabled = responseValidationEnabled; + } + + public boolean isRequestValidationEnabled() { + return requestValidationEnabled; + } + + public void setRequestValidationEnabled(boolean requestValidationEnabled) { + this.requestValidationEnabled = requestValidationEnabled; + } + + private static class IgnoreByKeyWhitelistRule implements WhitelistRule { + + private final String name; + + private final String key; + + private IgnoreByKeyWhitelistRule(@Nonnull String name, @Nonnull String key) { + this.name = name; + this.key = key; + } + + @Override + public boolean matches(Message message, @Nullable ApiOperation operation, + @Nullable Request request, @Nullable Response response) { + return key.equals(message.getKey()); + } + + public static IgnoreByKeyWhitelistRule ignoreByKey(String name, String key) { + return new IgnoreByKeyWhitelistRule(name, key); + } + } + +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidationContextLoader.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidationContextLoader.java new file mode 100644 index 0000000000..a2a14fe775 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidationContextLoader.java @@ -0,0 +1,136 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.validation; + +import static java.lang.String.format; +import static java.lang.String.join; +import static java.util.Collections.emptyList; +import static org.citrusframework.openapi.validation.OpenApiValidationPolicy.REPORT; +import static org.citrusframework.openapi.validation.OpenApiValidationPolicy.STRICT; + +import com.atlassian.oai.validator.OpenApiInteractionValidator.SpecSource; +import com.atlassian.oai.validator.util.OpenApiLoader; +import io.swagger.parser.OpenAPIParser; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.parser.core.models.ParseOptions; +import io.swagger.v3.parser.core.models.SwaggerParseResult; +import jakarta.annotation.Nonnull; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.exceptions.ValidationException; +import org.citrusframework.openapi.OpenApiResourceLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility class for creation of an {@link OpenApiValidationContext}. + */ +public final class OpenApiValidationContextLoader { + + private static final Logger logger = LoggerFactory.getLogger( + OpenApiValidationContextLoader.class); + + /** + * Cache for APIS which validation errors have already been logged. Used to avoid multiple + * validation error logging for the same api. + */ + private static final Set apisWithLoggedValidationErrors = new HashSet<>(); + + private OpenApiValidationContextLoader() { + // Static access only + } + + public static OpenApiValidationContext fromSpec(String openApiSpecAsString, + OpenApiValidationPolicy openApiValidationPolicy) { + OpenAPIParser openAPIParser = new OpenAPIParser(); + return createValidationContext( + handleSwaggerParserResult(openApiSpecAsString, openAPIParser.readContents(openApiSpecAsString, emptyList(), + defaultParseOptions()), openApiValidationPolicy)); + } + + private static OpenAPI handleSwaggerParserResult(String identifier, + SwaggerParseResult swaggerParseResult, + OpenApiValidationPolicy openApiValidationPolicy) { + logger.trace("Handling swagger parser result: {}", swaggerParseResult); + + if (swaggerParseResult == null) { + throw new CitrusRuntimeException( + "Unable to parse OpenApi from specSource: " + identifier); + } + + if (hasParseErrors(swaggerParseResult)) { + handleValidationException(identifier, openApiValidationPolicy, + swaggerParseResult.getMessages()); + } + + return swaggerParseResult.getOpenAPI(); + } + + private static boolean hasParseErrors(@Nullable final SwaggerParseResult parseResult) { + if (parseResult == null || parseResult.getOpenAPI() == null) { + return true; + } + return parseResult.getMessages() != null && !parseResult.getMessages().isEmpty(); + } + + private static void handleValidationException(String identifier, + OpenApiValidationPolicy openApiValidationPolicy, List errorMessages) { + if (REPORT.equals(openApiValidationPolicy) + && !apisWithLoggedValidationErrors.contains(identifier)) { + apisWithLoggedValidationErrors.add(identifier); + logger.warn("OpenApi '{}' has validation errors {}", identifier, errorMessages); + } else if (STRICT.equals(openApiValidationPolicy)) { + throw new ValidationException( + format( + """ + The API '%s' has failed STRICT validation: + %s + """, + identifier, + join(",", errorMessages) + ) + ); + } + } + + /** + * Creates an OpenApiValidationContext from an open api string. + * + * @param openApi the string representation of an OpenAPI + * @return the OpenApiValidationContext + */ + public static OpenApiValidationContext fromString(@Nonnull String openApi) { + return createValidationContext(new OpenApiLoader().loadApi( + SpecSource.inline(OpenApiResourceLoader.rawFromString(openApi)), emptyList(), + defaultParseOptions())); + } + + private static OpenApiValidationContext createValidationContext(OpenAPI openApi) { + return new OpenApiValidationContext(openApi); + } + + private static ParseOptions defaultParseOptions() { + final ParseOptions parseOptions = new ParseOptions(); + parseOptions.setResolve(true); + parseOptions.setResolveFully(true); + parseOptions.setResolveCombinators(false); + return parseOptions; + } +} diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidationPolicy.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidationPolicy.java new file mode 100644 index 0000000000..878579def2 --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidationPolicy.java @@ -0,0 +1,31 @@ +package org.citrusframework.openapi.validation; + +/** + * Enum that defines the policy for handling OpenAPI validation errors. + * This enum controls the behavior of the system when validation errors are encountered + * during OpenAPI validation, allowing for different levels of strictness. + */ +public enum OpenApiValidationPolicy { + + /** + * No validation will be performed. + * Any validation errors will be ignored, and the system will continue + * without reporting or failing due to OpenAPI validation issues. + */ + IGNORE, + + /** + * Perform validation and report any errors. + * Validation errors will be reported, but the system will continue running. + * This option allows for debugging or monitoring purposes without halting the application. + */ + REPORT, + + /** + * Perform validation and fail if any errors are encountered. + * Validation errors will cause the application to fail startup, + * ensuring that only valid OpenAPI specifications are allowed to proceed. + */ + STRICT, +} + diff --git a/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidator.java b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidator.java new file mode 100644 index 0000000000..fe1532339a --- /dev/null +++ b/connectors/citrus-openapi/src/main/java/org/citrusframework/openapi/validation/OpenApiValidator.java @@ -0,0 +1,56 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.validation; + +import com.atlassian.oai.validator.OpenApiInteractionValidator; +import com.atlassian.oai.validator.report.ValidationReport; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.model.OperationPathAdapter; + +public abstract class OpenApiValidator { + + protected final OpenApiInteractionValidator openApiInteractionValidator; + + protected OpenApiValidator(OpenApiSpecification openApiSpecification) { + OpenApiValidationContext openApiValidationContext = openApiSpecification.getOpenApiValidationContext(); + if (openApiValidationContext != null) { + openApiInteractionValidator = openApiSpecification.getOpenApiValidationContext() + .getOpenApiInteractionValidator(); + } else { + openApiInteractionValidator = null; + } + } + + protected abstract String getType(); + + /** + * Constructs the error message of a failed validation based on the processing report passed + * from {@link ValidationReport}. + * + * @param report The report containing the error message + * @return A string representation of all messages contained in the report + */ + protected String constructErrorMessage(OperationPathAdapter operationPathAdapter, ValidationReport report) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("OpenApi "); + stringBuilder.append(getType()); + stringBuilder.append(" validation failed for operation: "); + stringBuilder.append(operationPathAdapter); + report.getMessages().forEach(message -> stringBuilder.append("\n\t").append(message)); + return stringBuilder.toString(); + } +} 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 index 2f1bdbad34..ed36b5601f 100644 --- 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 @@ -30,21 +30,18 @@ * 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 index f7a12adc9d..f7060d44b4 100644 --- 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 @@ -16,30 +16,30 @@ 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 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.AutoFillType; 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.openapi.actions.OpenApiServerResponseActionBuilder; import org.citrusframework.spi.ReferenceResolver; import org.citrusframework.spi.ReferenceResolverAware; import org.citrusframework.xml.actions.Message; @@ -92,11 +92,19 @@ public OpenApi setHttpServer(String httpServer) { @XmlElement(name = "send-request") public OpenApi setSendRequest(ClientRequest request) { OpenApiClientRequestActionBuilder requestBuilder = - asClientBuilder().send(request.getOperation()); + asClientBuilder().send(request.getOperation()); requestBuilder.name("openapi:send-request"); requestBuilder.description(description); + if (request.getSchemaValidation() != null) { + requestBuilder.schemaValidation(request.getSchemaValidation()); + } + + if (request.getAutoFill() != null) { + requestBuilder.autoFill(request.getAutoFill()); + } + send = new Send(requestBuilder) { @Override protected SendMessageAction doBuild() { @@ -124,11 +132,15 @@ protected SendMessageAction doBuild() { @XmlElement(name = "receive-response") public OpenApi setReceiveResponse(ClientResponse response) { OpenApiClientResponseActionBuilder responseBuilder = - asClientBuilder().receive(response.getOperation(), response.getStatus()); + asClientBuilder().receive(response.getOperation(), response.getStatus()); responseBuilder.name("openapi:receive-response"); responseBuilder.description(description); + if (response.getSchemaValidation() != null) { + responseBuilder.schemaValidation(response.getSchemaValidation()); + } + receive = new Receive(responseBuilder) { @Override protected ReceiveMessageAction doBuild() { @@ -166,11 +178,15 @@ protected ReceiveMessageAction doBuild() { @XmlElement(name = "receive-request") public OpenApi setReceiveRequest(ServerRequest request) { OpenApiServerRequestActionBuilder requestBuilder = - asServerBuilder().receive(request.getOperation()); + asServerBuilder().receive(request.getOperation()); requestBuilder.name("openapi:receive-request"); requestBuilder.description(description); + if (request.getSchemaValidation() != null) { + requestBuilder.schemaValidation(request.getSchemaValidation()); + } + receive = new Receive(requestBuilder) { @Override protected ReceiveMessageAction doBuild() { @@ -205,12 +221,20 @@ protected ReceiveMessageAction doBuild() { @XmlElement(name = "send-response") public OpenApi setSendResponse(ServerResponse response) { - HttpServerResponseActionBuilder responseBuilder = - asServerBuilder().send(response.getOperation(), response.getStatus()); + OpenApiServerResponseActionBuilder responseBuilder = + asServerBuilder().send(response.getOperation(), response.getStatus()); responseBuilder.name("openapi:send-response"); responseBuilder.description(description); + if (response.getSchemaValidation() != null) { + responseBuilder.schemaValidation(response.getSchemaValidation()); + } + + if (response.getAutoFill() != null) { + responseBuilder.autoFill(response.getAutoFill()); + } + send = new Send(responseBuilder) { @Override protected SendMessageAction doBuild() { @@ -257,6 +281,7 @@ public void setReferenceResolver(ReferenceResolver referenceResolver) { /** * Converts current builder to client builder. + * * @return */ private OpenApiClientActionBuilder asClientBuilder() { @@ -265,11 +290,12 @@ private OpenApiClientActionBuilder asClientBuilder() { } 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"))); + Optional.ofNullable(builder).map(Object::getClass).map(Class::getName).orElse("null"))); } /** * Converts current builder to server builder. + * * @return */ private OpenApiServerActionBuilder asServerBuilder() { @@ -278,7 +304,7 @@ private OpenApiServerActionBuilder asServerBuilder() { } 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"))); + Optional.ofNullable(builder).map(Object::getClass).map(Class::getName).orElse("null"))); } @XmlAccessorType(XmlAccessType.FIELD) @@ -290,6 +316,10 @@ public static class ClientRequest { protected String uri; @XmlAttribute(name = "fork") protected Boolean fork; + @XmlAttribute(name = "schemaValidation") + protected Boolean schemaValidation; + @XmlAttribute(name = "autofill") + protected AutoFillType autoFill; @XmlElement protected Message.Extract extract; @@ -318,6 +348,14 @@ public void setFork(Boolean fork) { this.fork = fork; } + public Boolean getSchemaValidation() { + return schemaValidation; + } + + public void setSchemaValidation(Boolean schemaValidation) { + this.schemaValidation = schemaValidation; + } + public Message.Extract getExtract() { return extract; } @@ -325,6 +363,15 @@ public Message.Extract getExtract() { public void setExtract(Message.Extract extract) { this.extract = extract; } + + public AutoFillType getAutoFill() { + return autoFill; + } + + public void setAutoFill(AutoFillType autoFill) { + this.autoFill = autoFill; + } + } @XmlAccessorType(XmlAccessType.FIELD) @@ -351,6 +398,9 @@ public static class ServerRequest { @XmlAttribute(name = "header-validators") protected String headerValidators; + @XmlAttribute(name = "schemaValidation") + protected Boolean schemaValidation; + @XmlElement protected Receive.Selector selector; @@ -435,6 +485,15 @@ public Message.Extract getExtract() { public void setExtract(Message.Extract extract) { this.extract = extract; } + + public Boolean getSchemaValidation() { + return schemaValidation; + } + + public void setSchemaValidation(Boolean schemaValidation) { + this.schemaValidation = schemaValidation; + } + } @XmlAccessorType(XmlAccessType.FIELD) @@ -446,6 +505,12 @@ public static class ServerResponse { @XmlAttribute protected String status = "200"; + @XmlAttribute(name = "schemaValidation") + protected Boolean schemaValidation; + + @XmlAttribute(name = "autofill") + protected AutoFillType autoFill; + @XmlElement protected Message.Extract extract; @@ -472,6 +537,23 @@ public Message.Extract getExtract() { public void setExtract(Message.Extract extract) { this.extract = extract; } + + public Boolean getSchemaValidation() { + return schemaValidation; + } + + public void setSchemaValidation(Boolean schemaValidation) { + this.schemaValidation = schemaValidation; + } + + public AutoFillType getAutoFill() { + return autoFill; + } + + public void setAutoFill(AutoFillType autoFill) { + this.autoFill = autoFill; + } + } @XmlAccessorType(XmlAccessType.FIELD) @@ -501,6 +583,9 @@ public static class ClientResponse { @XmlAttribute(name = "header-validators") protected String headerValidators; + @XmlAttribute(name = "schemaValidation") + protected Boolean schemaValidation; + @XmlElement protected Receive.Selector selector; @@ -597,5 +682,14 @@ public Message.Extract getExtract() { public void setExtract(Message.Extract extract) { this.extract = extract; } + + public Boolean getSchemaValidation() { + return schemaValidation; + } + + public void setSchemaValidation(Boolean schemaValidation) { + this.schemaValidation = schemaValidation; + } + } } 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 index 4958fddc26..eb8bab0b29 100644 --- 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 @@ -16,10 +16,6 @@ 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; @@ -28,6 +24,8 @@ import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.http.actions.HttpServerResponseActionBuilder; import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.openapi.AutoFillType; +import org.citrusframework.openapi.OpenApiSettings; import org.citrusframework.openapi.actions.OpenApiActionBuilder; import org.citrusframework.openapi.actions.OpenApiClientActionBuilder; import org.citrusframework.openapi.actions.OpenApiClientRequestActionBuilder; @@ -36,9 +34,13 @@ import org.citrusframework.openapi.actions.OpenApiServerRequestActionBuilder; import org.citrusframework.spi.ReferenceResolver; import org.citrusframework.spi.ReferenceResolverAware; +import org.citrusframework.yaml.actions.Message; import org.citrusframework.yaml.actions.Receive; import org.citrusframework.yaml.actions.Send; -import org.citrusframework.yaml.actions.Message; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; public class OpenApi implements TestActionBuilder, ReferenceResolverAware { @@ -78,6 +80,7 @@ public void setSendRequest(ClientRequest request) { requestBuilder.name("openapi:send-request"); requestBuilder.description(description); + requestBuilder.autoFill(request.autoFill); send = new Send(requestBuilder) { @Override @@ -232,11 +235,12 @@ public void setReferenceResolver(ReferenceResolver referenceResolver) { /** * Converts current builder to client builder. + * * @return */ private OpenApiClientActionBuilder asClientBuilder() { - if (builder instanceof OpenApiClientActionBuilder) { - return (OpenApiClientActionBuilder) builder; + if (builder instanceof OpenApiClientActionBuilder openApiClientActionBuilder) { + return openApiClientActionBuilder; } throw new CitrusRuntimeException(String.format("Failed to convert '%s' to openapi client action builder", @@ -245,11 +249,12 @@ private OpenApiClientActionBuilder asClientBuilder() { /** * Converts current builder to server builder. + * * @return */ private OpenApiServerActionBuilder asServerBuilder() { - if (builder instanceof OpenApiServerActionBuilder) { - return (OpenApiServerActionBuilder) builder; + if (builder instanceof OpenApiServerActionBuilder openApiServerActionBuilder) { + return openApiServerActionBuilder; } throw new CitrusRuntimeException(String.format("Failed to convert '%s' to openapi server action builder", @@ -261,6 +266,8 @@ public static class ClientRequest { protected String uri; protected Boolean fork; + protected AutoFillType autoFill = OpenApiSettings.getRequestAutoFillRandomValues(); + protected Message.Extract extract; public String getOperation() { @@ -294,6 +301,14 @@ public Message.Extract getExtract() { public void setExtract(Message.Extract extract) { this.extract = extract; } + + public AutoFillType getAutoFill() { + return autoFill; + } + + public void setAutoFill(AutoFillType autoFill) { + this.autoFill = autoFill; + } } public static class ServerRequest { diff --git a/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/schemaValidator/openApi b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/schemaValidator/openApi new file mode 100644 index 0000000000..897edb75c0 --- /dev/null +++ b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/schemaValidator/openApi @@ -0,0 +1,2 @@ +name=defaultOpenApiSchemaValidator +type=org.citrusframework.openapi.validation.OpenApiSchemaValidation diff --git a/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/validator/openApi b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/validator/openApi new file mode 100644 index 0000000000..ad5d27c497 --- /dev/null +++ b/connectors/citrus-openapi/src/main/resources/META-INF/citrus/message/validator/openApi @@ -0,0 +1,2 @@ +name=defaultOpenApiMessageValidator +type=org.citrusframework.openapi.validation.OpenApiSchemaValidation diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiMessageTypeTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiMessageTypeTest.java new file mode 100644 index 0000000000..d42811ce18 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiMessageTypeTest.java @@ -0,0 +1,18 @@ +package org.citrusframework.openapi; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +public class OpenApiMessageTypeTest { + + @Test + public void testToHeaderNameRequest() { + assertEquals(OpenApiMessageType.REQUEST.toHeaderName(), OpenApiMessageHeaders.REQUEST_TYPE); + } + + @Test + public void testToHeaderNameResponse() { + assertEquals(OpenApiMessageType.RESPONSE.toHeaderName(), OpenApiMessageHeaders.RESPONSE_TYPE); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiRepositoryTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiRepositoryTest.java new file mode 100644 index 0000000000..fe846a6f59 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiRepositoryTest.java @@ -0,0 +1,114 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import static java.util.Collections.singletonList; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; + +import java.util.List; +import org.citrusframework.openapi.validation.OpenApiValidationPolicy; +import org.testng.annotations.Test; + +public class OpenApiRepositoryTest { + + private static final String ROOT = "/root"; + + @Test + public void shouldInitializeOpenApiRepository() { + OpenApiRepository openApiRepository = new OpenApiRepository(); + openApiRepository.setRootContextPath(ROOT); + openApiRepository.setLocations( + singletonList("org/citrusframework/openapi/petstore/petstore**.json")); + openApiRepository.initialize(); + + List openApiSpecifications = openApiRepository.getOpenApiSpecifications(); + + assertEquals(openApiRepository.getRootContextPath(), ROOT); + assertNotNull(openApiSpecifications); + assertEquals(openApiSpecifications.size(), 3); + + assertEquals(openApiSpecifications.get(0).getRootContextPath(), ROOT); + assertEquals(openApiSpecifications.get(1).getRootContextPath(), ROOT); + + assertTrue( + SampleOpenApiProcessor.processedSpecifications.contains(openApiSpecifications.get(0))); + assertTrue( + SampleOpenApiProcessor.processedSpecifications.contains(openApiSpecifications.get(1))); + assertTrue( + SampleOpenApiProcessor.processedSpecifications.contains(openApiSpecifications.get(2))); + } + + @Test + public void shouldInitializeFaultyOpenApiRepositoryByDefault() { + OpenApiRepository openApiRepository = new OpenApiRepository(); + openApiRepository.setLocations( + singletonList("org/citrusframework/openapi/faulty/faulty-ping-api.yaml")); + openApiRepository.initialize(); + + List openApiSpecifications = openApiRepository.getOpenApiSpecifications(); + + assertNotNull(openApiSpecifications); + assertEquals(openApiSpecifications.size(), 1); + + assertNotNull(openApiSpecifications.get(0).getOpenApiDoc(null)); + } + + @Test + public void shouldFailOnFaultyOpenApiRepositoryByStrictValidation() { + OpenApiRepository openApiRepository = new OpenApiRepository(); + openApiRepository.setValidationPolicy(OpenApiValidationPolicy.STRICT); + openApiRepository.setLocations( + singletonList("org/citrusframework/openapi/faulty/faulty-ping-api.yaml")); + + assertThrows(openApiRepository::initialize); + } + + @Test + public void shouldSetAndProvideProperties() { + // Given + OpenApiRepository openApiRepository = new OpenApiRepository(); + + // When + openApiRepository.setResponseValidationEnabled(true); + openApiRepository.setRequestValidationEnabled(true); + openApiRepository.setRootContextPath("/root"); + openApiRepository.setLocations(List.of("l1", "l2")); + + // Then + assertTrue(openApiRepository.isResponseValidationEnabled()); + assertTrue(openApiRepository.isRequestValidationEnabled()); + assertEquals(openApiRepository.getRootContextPath(), "/root"); + assertEquals(openApiRepository.getLocations(), List.of("l1", "l2")); + + // When + openApiRepository.setResponseValidationEnabled(false); + openApiRepository.setRequestValidationEnabled(false); + openApiRepository.setRootContextPath("/otherRoot"); + openApiRepository.setLocations(List.of("l3", "l4")); + + // Then + assertFalse(openApiRepository.isResponseValidationEnabled()); + assertFalse(openApiRepository.isRequestValidationEnabled()); + assertEquals(openApiRepository.getRootContextPath(), "/otherRoot"); + assertEquals(openApiRepository.getLocations(), List.of("l3", "l4")); + } + +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiSettingsTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiSettingsTest.java new file mode 100644 index 0000000000..3adcd29942 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiSettingsTest.java @@ -0,0 +1,312 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import static org.citrusframework.openapi.OpenApiSettings.GENERATE_OPTIONAL_FIELDS_PROPERTY; +import static org.citrusframework.openapi.OpenApiSettings.NEGLECT_OPEN_API_BASE_PATH_ENV; +import static org.citrusframework.openapi.OpenApiSettings.NEGLECT_OPEN_API_BASE_PATH_PROPERTY; +import static org.citrusframework.openapi.OpenApiSettings.OPEN_API_VALIDATION_POLICY_PROPERTY; +import static org.citrusframework.openapi.OpenApiSettings.REQUEST_AUTO_FILL_RANDOM_VALUES_PROPERTY; +import static org.citrusframework.openapi.OpenApiSettings.REQUEST_AUTO_FILL_RANDOM_VALUES_ENV; +import static org.citrusframework.openapi.OpenApiSettings.REQUEST_VALIDATION_ENABLED_PROPERTY; +import static org.citrusframework.openapi.OpenApiSettings.RESPONSE_AUTO_FILL_RANDOM_VALUES_PROPERTY; +import static org.citrusframework.openapi.OpenApiSettings.RESPONSE_AUTO_FILL_RANDOM_VALUES_ENV; +import static org.citrusframework.openapi.OpenApiSettings.RESPONSE_VALIDATION_ENABLED_PROPERTY; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import org.citrusframework.openapi.validation.OpenApiValidationPolicy; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; + +public class OpenApiSettingsTest { + + private static final boolean REQUEST_VALIDATION_ENABLED_GLOBALLY = OpenApiSettings.isRequestValidationEnabledGlobally(); + private static final boolean RESPONSE_VALIDATION_ENABLED_GLOBALLY = OpenApiSettings.isResponseValidationEnabledGlobally(); + private static final boolean GENERATE_OPTIONAL_FIELDS_ENABLED_GLOBALLY = OpenApiSettings.isGenerateOptionalFieldsGlobally(); + private static final AutoFillType REQUEST_AUTO_FILL_RANDOM_VALUES_ENABLED_GLOBALLY = OpenApiSettings.getRequestAutoFillRandomValues(); + private static final AutoFillType RESPONSE_AUTO_FILL_RANDOM_VALUES_ENABLED_GLOBALLY = OpenApiSettings.getResponseAutoFillRandomValues(); + private static final boolean NEGLECT_BASE_PATH_GLOBALLY = OpenApiSettings.isNeglectBasePathGlobally(); + private static final OpenApiValidationPolicy OPEN_API_VALIDATION_POLICY_GLOBALLY = OpenApiSettings.getOpenApiValidationPolicy(); + + private final EnvironmentVariables environmentVariables = new EnvironmentVariables(); + + @BeforeMethod + public void beforeMethod() { + System.clearProperty(GENERATE_OPTIONAL_FIELDS_PROPERTY); + System.clearProperty(REQUEST_VALIDATION_ENABLED_PROPERTY); + System.clearProperty(RESPONSE_VALIDATION_ENABLED_PROPERTY); + System.clearProperty(NEGLECT_OPEN_API_BASE_PATH_PROPERTY); + System.clearProperty(REQUEST_AUTO_FILL_RANDOM_VALUES_PROPERTY); + System.clearProperty(RESPONSE_AUTO_FILL_RANDOM_VALUES_PROPERTY); + System.clearProperty(OPEN_API_VALIDATION_POLICY_PROPERTY); + } + + @AfterMethod + public void afterMethod() throws Exception { + environmentVariables.teardown(); + + if (!GENERATE_OPTIONAL_FIELDS_ENABLED_GLOBALLY) { + System.clearProperty(GENERATE_OPTIONAL_FIELDS_PROPERTY); + } else { + System.setProperty(GENERATE_OPTIONAL_FIELDS_PROPERTY, "true"); + } + + if (!REQUEST_VALIDATION_ENABLED_GLOBALLY) { + System.clearProperty(REQUEST_VALIDATION_ENABLED_PROPERTY); + } else { + System.setProperty(REQUEST_VALIDATION_ENABLED_PROPERTY, "true"); + } + + if (!RESPONSE_VALIDATION_ENABLED_GLOBALLY) { + System.clearProperty(RESPONSE_VALIDATION_ENABLED_PROPERTY); + } else { + System.setProperty(RESPONSE_VALIDATION_ENABLED_PROPERTY, "true"); + } + + if (!NEGLECT_BASE_PATH_GLOBALLY) { + System.clearProperty(NEGLECT_OPEN_API_BASE_PATH_PROPERTY); + } else { + System.setProperty(NEGLECT_OPEN_API_BASE_PATH_PROPERTY, "true"); + } + + System.setProperty(RESPONSE_AUTO_FILL_RANDOM_VALUES_PROPERTY, + RESPONSE_AUTO_FILL_RANDOM_VALUES_ENABLED_GLOBALLY.toString()); + System.setProperty(REQUEST_AUTO_FILL_RANDOM_VALUES_PROPERTY, + REQUEST_AUTO_FILL_RANDOM_VALUES_ENABLED_GLOBALLY.toString()); + System.setProperty(OPEN_API_VALIDATION_POLICY_PROPERTY, + OPEN_API_VALIDATION_POLICY_GLOBALLY.toString()); + } + + @Test + public void testRequestValidationEnabledByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(REQUEST_VALIDATION_ENABLED_PROPERTY, "true"); + assertTrue(OpenApiSettings.isRequestValidationEnabledGlobally()); + } + + @Test + public void testRequestValidationDisabledByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(REQUEST_VALIDATION_ENABLED_PROPERTY, "false"); + assertFalse(OpenApiSettings.isRequestValidationEnabledGlobally()); + } + + @Test + public void testRequestValidationEnabledByEnvVar() throws Exception { + environmentVariables.set(OpenApiSettings.REQUEST_VALIDATION_ENABLED_ENV, "true"); + environmentVariables.setup(); + assertTrue(OpenApiSettings.isRequestValidationEnabledGlobally()); + } + + @Test + public void testRequestValidationDisabledByEnvVar() throws Exception { + environmentVariables.set(OpenApiSettings.REQUEST_VALIDATION_ENABLED_ENV, "false"); + environmentVariables.setup(); + assertFalse(OpenApiSettings.isRequestValidationEnabledGlobally()); + } + + @Test + public void testRequestValidationEnabledByDefault() { + assertTrue(OpenApiSettings.isRequestValidationEnabledGlobally()); + } + + @Test + public void testResponseValidationEnabledByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(RESPONSE_VALIDATION_ENABLED_PROPERTY, "true"); + assertTrue(OpenApiSettings.isResponseValidationEnabledGlobally()); + } + + @Test + public void testResponseValidationDisabledByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(RESPONSE_VALIDATION_ENABLED_PROPERTY, "false"); + assertFalse(OpenApiSettings.isResponseValidationEnabledGlobally()); + } + + @Test + public void testResponseValidationEnabledByEnvVar() throws Exception { + environmentVariables.set(OpenApiSettings.RESPONSE_VALIDATION_ENABLED_ENV, "true"); + environmentVariables.setup(); + assertTrue(OpenApiSettings.isResponseValidationEnabledGlobally()); + } + + @Test + public void testResponseValidationDisabledByEnvVar() throws Exception { + environmentVariables.set(OpenApiSettings.RESPONSE_VALIDATION_ENABLED_ENV, "false"); + environmentVariables.setup(); + assertFalse(OpenApiSettings.isResponseValidationEnabledGlobally()); + } + + @Test + public void testResponseValidationEnabledByDefault() { + assertTrue(OpenApiSettings.isResponseValidationEnabledGlobally()); + } + + @Test + public void testGenerateOptionalFieldsEnabledByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(GENERATE_OPTIONAL_FIELDS_PROPERTY, "true"); + assertTrue(OpenApiSettings.isGenerateOptionalFieldsGlobally()); + } + + @Test + public void testGenerateOptionalFieldsDisabledByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(GENERATE_OPTIONAL_FIELDS_PROPERTY, "false"); + assertFalse(OpenApiSettings.isGenerateOptionalFieldsGlobally()); + } + + @Test + public void testGenerateOptionalFieldsEnabledByEnvVar() throws Exception { + environmentVariables.set(OpenApiSettings.GENERATE_OPTIONAL_FIELDS_ENV, "true"); + environmentVariables.setup(); + assertTrue(OpenApiSettings.isGenerateOptionalFieldsGlobally()); + } + + @Test + public void testGenerateOptionalFieldsDisabledByEnvVar() throws Exception { + environmentVariables.set(OpenApiSettings.GENERATE_OPTIONAL_FIELDS_ENV, "false"); + environmentVariables.setup(); + assertFalse(OpenApiSettings.isGenerateOptionalFieldsGlobally()); + } + + @Test + public void testGenerateOptionalFieldsEnabledByDefault() { + assertTrue(OpenApiSettings.isGenerateOptionalFieldsGlobally()); + } + + @Test + public void testNeglectBasePathEnabledByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(NEGLECT_OPEN_API_BASE_PATH_PROPERTY, "true"); + assertTrue(OpenApiSettings.isNeglectBasePathGlobally()); + } + + @Test + public void testNeglectBasePathDisabledByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(NEGLECT_OPEN_API_BASE_PATH_PROPERTY, "false"); + assertFalse(OpenApiSettings.isNeglectBasePathGlobally()); + } + + @Test + public void testNeglectBasePathDisabledByEnvVar() throws Exception { + environmentVariables.set(NEGLECT_OPEN_API_BASE_PATH_ENV, "false"); + environmentVariables.setup(); + assertFalse(OpenApiSettings.isNeglectBasePathGlobally()); + } + + @Test + public void testNeglectBasePathEnabledByEnvVar() throws Exception { + environmentVariables.set(NEGLECT_OPEN_API_BASE_PATH_ENV, "true"); + environmentVariables.setup(); + assertTrue(OpenApiSettings.isNeglectBasePathGlobally()); + } + + @Test + public void testNeglectBasePathDisabledByDefault() { + assertFalse(OpenApiSettings.isNeglectBasePathGlobally()); + } + + @Test + public void testRequestAutoFillRandomValuesAllByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(REQUEST_AUTO_FILL_RANDOM_VALUES_PROPERTY, "ALL"); + assertEquals(OpenApiSettings.getRequestAutoFillRandomValues(), AutoFillType.ALL); + } + + @Test + public void testRequestAutoFillRandomValuesNoneByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(REQUEST_AUTO_FILL_RANDOM_VALUES_PROPERTY, "NONE"); + assertEquals(OpenApiSettings.getRequestAutoFillRandomValues(), AutoFillType.NONE); + } + + @Test + public void testRequestAutoFillRandomValuesAllByEnvVar() throws Exception { + environmentVariables.set(REQUEST_AUTO_FILL_RANDOM_VALUES_ENV, "ALL"); + environmentVariables.setup(); + assertEquals(OpenApiSettings.getRequestAutoFillRandomValues(), AutoFillType.ALL); + } + + @Test + public void testRequestAutoFillRandomValuesNoneByEnvVar() throws Exception { + environmentVariables.set(REQUEST_AUTO_FILL_RANDOM_VALUES_ENV, "NONE"); + environmentVariables.setup(); + assertEquals(OpenApiSettings.getRequestAutoFillRandomValues(), AutoFillType.NONE); + } + + @Test + public void testResponseAutoFillRandomValuesAllByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(RESPONSE_AUTO_FILL_RANDOM_VALUES_PROPERTY, "ALL"); + assertEquals(OpenApiSettings.getResponseAutoFillRandomValues(), AutoFillType.ALL); + } + + @Test + public void testResponseAutoFillRandomValuesNoneByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(RESPONSE_AUTO_FILL_RANDOM_VALUES_PROPERTY, "NONE"); + assertEquals(OpenApiSettings.getResponseAutoFillRandomValues(), AutoFillType.NONE); + } + + @Test + public void testResponseAutoFillRandomValuesAllByEnvVar() throws Exception { + environmentVariables.set(RESPONSE_AUTO_FILL_RANDOM_VALUES_ENV, "ALL"); + environmentVariables.setup(); + assertEquals(OpenApiSettings.getResponseAutoFillRandomValues(), AutoFillType.ALL); + } + + @Test + public void testResponseAutoFillRandomValuesNoneByEnvVar() throws Exception { + environmentVariables.set(RESPONSE_AUTO_FILL_RANDOM_VALUES_ENV, "NONE"); + environmentVariables.setup(); + assertEquals(OpenApiSettings.getResponseAutoFillRandomValues(), AutoFillType.NONE); + } + + @Test + public void testOpenApiValidationPolicyIgnoreByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(OPEN_API_VALIDATION_POLICY_PROPERTY, "IGNORE"); + assertEquals(OpenApiSettings.getOpenApiValidationPolicy(), OpenApiValidationPolicy.IGNORE); + } + + @Test + public void testOpenApiValidationPolicyStrictByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(OPEN_API_VALIDATION_POLICY_PROPERTY, "STRICT"); + assertEquals(OpenApiSettings.getOpenApiValidationPolicy(), OpenApiValidationPolicy.STRICT); + } + + @Test + public void testOpenApiValidationPolicyReportByProperty() throws Exception { + environmentVariables.setup(); + System.setProperty(OPEN_API_VALIDATION_POLICY_PROPERTY, "REPORT"); + assertEquals(OpenApiSettings.getOpenApiValidationPolicy(), OpenApiValidationPolicy.REPORT); + } + + @Test + public void testOpenApiValidationPolicyReportByDefault() throws Exception { + environmentVariables.setup(); + assertEquals(OpenApiSettings.getOpenApiValidationPolicy(), OpenApiValidationPolicy.REPORT); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiSpecificationTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiSpecificationTest.java new file mode 100644 index 0000000000..ae49846a53 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiSpecificationTest.java @@ -0,0 +1,475 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import static org.citrusframework.openapi.OpenApiSpecification.determineResourceAlias; +import static org.citrusframework.openapi.validation.OpenApiValidationPolicy.IGNORE; +import static org.citrusframework.openapi.validation.OpenApiValidationPolicy.REPORT; +import static org.citrusframework.openapi.validation.OpenApiValidationPolicy.STRICT; +import static org.citrusframework.util.FileUtils.readToString; +import static org.mockito.AdditionalAnswers.returnsFirstArg; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; + +import io.apicurio.datamodels.openapi.models.OasDocument; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import javax.net.ssl.HttpsURLConnection; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.http.client.HttpClient; +import org.citrusframework.http.client.HttpEndpointConfiguration; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.citrusframework.spi.ReferenceResolver; +import org.citrusframework.spi.Resource; +import org.citrusframework.spi.Resources.ClasspathResource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class OpenApiSpecificationTest { + + private static final String PING_API_HTTP_URL_STRING = "http://org.citrus.example.com/ping-api.yaml"; + + private static final String PING_API_HTTPS_URL_STRING = "https://org.citrus.example.com/ping-api.yaml"; + + private static final String PING_OPERATION_ID = "doPing"; + + private static final String PONG_OPERATION_ID = "doPong"; + + private static String PING_API_STRING; + + @Mock + private TestContext testContextMock; + + @Mock + private HttpClient httpClient; + + @Mock + private ReferenceResolver referenceResolverMock; + + @Mock + private HttpEndpointConfiguration endpointConfigurationMock; + + private AutoCloseable mockCloseable; + + @InjectMocks + private OpenApiSpecification openApiSpecification; + + @DataProvider(name = "protocollDataProvider") + public static Object[][] protocolls() { + return new Object[][]{{PING_API_HTTP_URL_STRING}, {PING_API_HTTPS_URL_STRING}}; + } + + @DataProvider(name = "lazyInitializationDataprovider") + public static Object[][] specSources() { + return new Object[][]{ + {null, "classpath:org/citrusframework/openapi/ping/ping-api.yaml"}, + {null, PING_API_HTTP_URL_STRING}, + {null, PING_API_HTTPS_URL_STRING}, + {null, "/ping-api.yaml"}, + {"http://org.citrus.sample", "/ping-api.yaml"} + }; + } + + private static URL mockUrlConnection(String urlString) { + try { + HttpsURLConnection httpsURLConnectionMock = mock(); + when(httpsURLConnectionMock.getResponseCode()).thenReturn(200); + when(httpsURLConnectionMock.getInputStream()).thenAnswer( + invocation -> new ByteArrayInputStream(PING_API_STRING.getBytes( + StandardCharsets.UTF_8))); + + URL urlSpy = spy(new URL(urlString)); + when(urlSpy.openConnection()).thenReturn(httpsURLConnectionMock); + return urlSpy; + } catch (Exception e) { + throw new CitrusRuntimeException("Unable to mock spec url!", e); + } + } + + @BeforeClass + public void beforeClass() throws IOException { + PING_API_STRING = readToString( + new ClasspathResource( + "classpath:org/citrusframework/openapi/ping/ping-api.yaml")); + } + + @BeforeMethod + public void setUp() { + mockCloseable = MockitoAnnotations.openMocks(this); + + testContextMock.setReferenceResolver(referenceResolverMock); + } + + @AfterMethod + public void tearDown() throws Exception { + mockCloseable.close(); + } + + @Test(dataProvider = "protocollDataProvider") + public void shouldInitializeFromUrl(String urlString) { + // Given + URL urlMock = mockUrlConnection(urlString); + + // When + OpenApiSpecification specification = OpenApiSpecification.from(urlMock); + + // Then + assertEquals(specification.getSpecUrl(), urlString); + assertPingApi(specification); + } + + private void assertPingApi(OpenApiSpecification specification) { + assertNotNull(specification); + assertNotNull(specification.getOpenApiValidationContext()); + Optional pingOperationPathAdapter = specification.getOperation( + PING_OPERATION_ID, + testContextMock); + assertTrue(pingOperationPathAdapter.isPresent()); + assertEquals(pingOperationPathAdapter.get().apiPath(), "/ping/{id}"); + assertEquals(pingOperationPathAdapter.get().contextPath(), "/services/rest/ping/v1"); + assertEquals(pingOperationPathAdapter.get().fullPath(), "/services/rest/ping/v1/ping/{id}"); + + Optional pongOperationPathAdapter = specification.getOperation( + PONG_OPERATION_ID, + testContextMock); + assertTrue(pongOperationPathAdapter.isPresent()); + assertEquals(pongOperationPathAdapter.get().apiPath(), "/pong/{id}"); + assertEquals(pongOperationPathAdapter.get().contextPath(), "/services/rest/ping/v1"); + assertEquals(pongOperationPathAdapter.get().fullPath(), "/services/rest/ping/v1/pong/{id}"); + } + + @Test + public void shouldInitializeFromResource() { + // Given + Resource resource = new ClasspathResource("classpath:org/citrusframework/openapi/ping/ping-api.yaml"); + + // When + OpenApiSpecification specification = OpenApiSpecification.from(resource); + + // Then + assertNotNull(specification); + assertEquals(specification.getSpecUrl(), resource.getLocation()); + assertPingApi(specification); + } + + @Test + public void shouldInitializeFaultyFromResource() { + // Given + Resource resource = new ClasspathResource("classpath:org/citrusframework/openapi/faulty/faulty-ping-api.yaml"); + + // When + OpenApiSpecification specification = OpenApiSpecification.from(resource); + + // Then + assertNotNull(specification); + assertEquals(specification.getSpecUrl(), resource.getLocation()); + + assertNotNull(specification.getOpenApiDoc(null)); + } + + @Test + public void shouldFailOnStrictPolicy() { + // Given + Resource resource = new ClasspathResource("classpath:org/citrusframework/openapi/faulty/faulty-ping-api.yaml"); + + // When + assertThrows( () -> OpenApiSpecification.from(resource, STRICT)); + } + + @Test + public void shouldSuccessOnIgnorePolicy() { + // Given + Resource resource = new ClasspathResource("classpath:org/citrusframework/openapi/faulty/faulty-ping-api.yaml"); + + // When + OpenApiSpecification specification = OpenApiSpecification.from(resource, IGNORE); + + // Then + assertNotNull(specification.getOpenApiDoc(null)); + } + + @Test + public void shouldSuccessOnReportPolicy() { + // Given + Resource resource = new ClasspathResource("classpath:org/citrusframework/openapi/faulty/faulty-ping-api.yaml"); + + // When + OpenApiSpecification specification = OpenApiSpecification.from(resource, REPORT); + + // Then + assertNotNull(specification.getOpenApiDoc(null)); + } + + @Test + public void shouldReturnOpenApiDocWhenInitialized() { + //Given + OpenApiSpecification specification = OpenApiSpecification.from(new ClasspathResource("classpath:org/citrusframework/openapi/ping/ping-api.yaml")); + OasDocument openApiDoc = specification.getOpenApiDoc(testContextMock); + + //When + OpenApiSpecification otherSpecification = new OpenApiSpecification(); + otherSpecification.setOpenApiDoc(openApiDoc); + OasDocument doc = otherSpecification.getOpenApiDoc(testContextMock); + + // Then + assertNotNull(doc); + assertEquals(doc, openApiDoc); + } + + @Test + public void shouldReturnEmptyOptionalWhenOperationIdIsNull() { + // When + Optional result = openApiSpecification.getOperation(null, + testContextMock); + + // Then + assertTrue(result.isEmpty()); + } + + @Test + public void shouldReturnOperationWhenExists() { + // Given/When + OpenApiSpecification specification = OpenApiSpecification.from(new ClasspathResource("classpath:org/citrusframework/openapi/ping/ping-api.yaml")); + + // Then + assertPingApi(specification); + } + + @Test + public void shouldInitializeDocumentWhenRequestingOperation() { + // Given/When + when(testContextMock.replaceDynamicContentInString(isA(String.class))).thenAnswer(answer -> + answer.getArgument(0) + ); + OpenApiSpecification specification = OpenApiSpecification.from("classpath:org/citrusframework/openapi/ping/ping-api.yaml"); + + // Then + Optional pingOperationPathAdapter = specification.getOperation( + PING_OPERATION_ID, + testContextMock); + assertTrue(pingOperationPathAdapter.isPresent()); + assertEquals(pingOperationPathAdapter.get().apiPath(), "/ping/{id}"); + assertEquals(pingOperationPathAdapter.get().contextPath(), "/services/rest/ping/v1"); + assertEquals(pingOperationPathAdapter.get().fullPath(), "/services/rest/ping/v1/ping/{id}"); + } + + @Test(dataProvider = "lazyInitializationDataprovider") + public void shouldDisableEnableRequestValidationWhenSet(String requestUrl, String specSource) { + + // Given + OpenApiSpecification specification = new OpenApiSpecification() { + + @Override + URL toSpecUrl(String resolvedSpecUrl) { + return mockUrlConnection(resolvedSpecUrl); + } + }; + + specification.setRequestUrl(requestUrl); + specification.setHttpClient("sampleHttpClient"); + specification.setSpecUrl(specSource); + when(testContextMock.replaceDynamicContentInString(isA(String.class))).thenAnswer(returnsFirstArg()); + + when(testContextMock.getReferenceResolver()).thenReturn(referenceResolverMock); + when(referenceResolverMock.isResolvable("sampleHttpClient", HttpClient.class)).thenReturn(true); + when(referenceResolverMock.resolve("sampleHttpClient", HttpClient.class)).thenReturn(httpClient); + when(httpClient.getEndpointConfiguration()).thenReturn(endpointConfigurationMock); + when(endpointConfigurationMock.getRequestUrl()).thenReturn("http://org.citrus.sample"); + + // When + specification.setApiRequestValidationEnabled(false); + + // Then (not yet initialized) + assertFalse(specification.isApiRequestValidationEnabled()); + assertNull(specification.getOpenApiValidationContext()); + + // When (initialize) + specification.getOpenApiDoc(testContextMock); + + // Then + assertFalse(specification.isApiRequestValidationEnabled()); + assertNotNull(specification.getOpenApiValidationContext()); + + // When + specification.setApiRequestValidationEnabled(true); + + // Then + assertTrue(specification.isApiRequestValidationEnabled()); + assertTrue(specification.getOpenApiValidationContext().isRequestValidationEnabled()); + + } + + @Test + public void shouldDisableEnableResponseValidationWhenSet() { + // Given + OpenApiSpecification specification = OpenApiSpecification.from(new ClasspathResource("classpath:org/citrusframework/openapi/ping/ping-api.yaml")); + + // When + specification.setApiResponseValidationEnabled(false); + + // Then + assertFalse(specification.isApiResponseValidationEnabled()); + assertNotNull(specification.getOpenApiValidationContext()); + assertFalse(specification.getOpenApiValidationContext().isResponseValidationEnabled()); + + // When + specification.setApiResponseValidationEnabled(true); + + // Then + assertTrue(specification.isApiResponseValidationEnabled()); + assertTrue(specification.getOpenApiValidationContext().isResponseValidationEnabled()); + + } + + @Test + public void shouldAddAlias() { + String alias = "alias1"; + openApiSpecification.addAlias(alias); + + assertTrue(openApiSpecification.getAliases().contains(alias)); + } + + @Test + public void shouldReturnSpecUrl() { + URL url = openApiSpecification.toSpecUrl(PING_API_HTTP_URL_STRING); + + assertNotNull(url); + + assertEquals(url.toString(), PING_API_HTTP_URL_STRING); + } + + @Test + public void shouldSetRootContextPathAndReinitialize() { + // Given/When + OpenApiSpecification specification = OpenApiSpecification.from(new ClasspathResource("classpath:org/citrusframework/openapi/ping/ping-api.yaml")); + + // Then + assertNull(openApiSpecification.getRootContextPath()); + + assertPingApi(specification); + + // When + specification.setRootContextPath("/root"); + + Optional pingOperationPathAdapter = specification.getOperation( + PING_OPERATION_ID, + testContextMock); + assertTrue(pingOperationPathAdapter.isPresent()); + assertEquals(pingOperationPathAdapter.get().apiPath(), "/ping/{id}"); + assertEquals(pingOperationPathAdapter.get().contextPath(), "/root/services/rest/ping/v1"); + assertEquals(pingOperationPathAdapter.get().fullPath(), "/root/services/rest/ping/v1/ping/{id}"); + + Optional pongOperationPathAdapter = specification.getOperation( + PONG_OPERATION_ID, + testContextMock); + assertTrue(pongOperationPathAdapter.isPresent()); + assertEquals(pongOperationPathAdapter.get().apiPath(), "/pong/{id}"); + assertEquals(pongOperationPathAdapter.get().contextPath(), "/root/services/rest/ping/v1"); + assertEquals(pongOperationPathAdapter.get().fullPath(), "/root/services/rest/ping/v1/pong/{id}"); + } + + @Test + public void shouldSetRootContextPathNeglectingBasePathAndReinitialize() { + // Given/When + OpenApiSpecification specification = OpenApiSpecification.from(new ClasspathResource("classpath:org/citrusframework/openapi/ping/ping-api.yaml")); + + // Then + assertPingApi(specification); + + // When + specification.setNeglectBasePath(true); + specification.setRootContextPath("/root"); + + Optional pingOperationPathAdapter = specification.getOperation( + PING_OPERATION_ID, + testContextMock); + assertTrue(pingOperationPathAdapter.isPresent()); + assertEquals(pingOperationPathAdapter.get().apiPath(), "/ping/{id}"); + assertEquals(pingOperationPathAdapter.get().contextPath(), "/root"); + assertEquals(pingOperationPathAdapter.get().fullPath(), "/root/ping/{id}"); + + Optional pongOperationPathAdapter = specification.getOperation( + PONG_OPERATION_ID, + testContextMock); + assertTrue(pongOperationPathAdapter.isPresent()); + assertEquals(pongOperationPathAdapter.get().apiPath(), "/pong/{id}"); + assertEquals(pongOperationPathAdapter.get().contextPath(), "/root"); + assertEquals(pongOperationPathAdapter.get().fullPath(), "/root/pong/{id}"); + } + + @Test + public void shouldReturnSpecUrlInAbsenceOfRequestUrl() { + + openApiSpecification.setSpecUrl(PING_API_HTTP_URL_STRING); + + assertEquals(openApiSpecification.getSpecUrl(), PING_API_HTTP_URL_STRING); + assertEquals(openApiSpecification.getRequestUrl(), PING_API_HTTP_URL_STRING); + + openApiSpecification.setSpecUrl("/ping-api.yaml"); + openApiSpecification.setRequestUrl("http://or.citrus.sample"); + + assertEquals(openApiSpecification.getSpecUrl(), "/ping-api.yaml"); + assertEquals(openApiSpecification.getRequestUrl(), "http://or.citrus.sample"); + } + + @Test + public void testResolveResourceAliasFromFile() { + File fileMock = mock(); + doReturn("MyApi.json").when(fileMock).getName(); + Resource resourceMock = mock(); + doReturn(fileMock).when(resourceMock).getFile(); + + Optional alias = determineResourceAlias(resourceMock); + assertTrue(alias.isPresent()); + assertEquals(alias.get(), "MyApi"); + } + + @Test + public void testResolveResourceAliasFromUrl() throws MalformedURLException { + URL urlMock = mock(); + doReturn("/C:/segment1/segment2/MyApi.json").when(urlMock).getPath(); + Resource resourceMock = mock(); + doThrow(new RuntimeException("Forced Exception")).when(resourceMock).getFile(); + doReturn(urlMock).when(resourceMock).getURL(); + + Optional alias = determineResourceAlias(resourceMock); + assertTrue(alias.isPresent()); + assertEquals(alias.get(), "MyApi"); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiTestDataGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiTestDataGeneratorTest.java new file mode 100644 index 0000000000..23d180eddb --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiTestDataGeneratorTest.java @@ -0,0 +1,383 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import com.atlassian.oai.validator.report.ValidationReport; +import com.atlassian.oai.validator.report.ValidationReport.Message; +import com.atlassian.oai.validator.schema.SchemaValidator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import io.swagger.v3.oas.models.media.Schema; +import org.apache.commons.lang3.StringUtils; +import org.citrusframework.context.TestContext; +import org.citrusframework.functions.DefaultFunctionRegistry; +import org.citrusframework.openapi.model.OasModelHelper; +import org.citrusframework.spi.Resources; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_DOUBLE; +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_FLOAT; +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_INT32; +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_INT64; +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_UUID; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_ARRAY; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_INTEGER; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_NUMBER; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_STRING; +import static org.citrusframework.openapi.OpenApiTestDataGenerator.createOutboundPayload; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +public class OpenApiTestDataGeneratorTest { + + private static final TestContext testContext = new TestContext(); + + private static OpenApiSpecification openApiSpecification; + + private static SchemaValidator schemaValidator; + + @BeforeClass + public static void beforeClass() { + testContext.setFunctionRegistry(new DefaultFunctionRegistry()); + + openApiSpecification = OpenApiSpecification.from( + Resources.fromClasspath("org/citrusframework/openapi/ping/ping-api.yaml")); + schemaValidator = openApiSpecification.getOpenApiValidationContext() + .getSchemaValidator(); + } + + @DataProvider(name = "testRandomNumber") + public static Object[][] testRandomNumber() { + return new Object[][]{ + {TYPE_INTEGER, FORMAT_INT32, null, 0, 2, true, true}, + {TYPE_INTEGER, FORMAT_INT32, null, null, null, false, false}, + {TYPE_INTEGER, FORMAT_INT32, null, 0, 100, false, false}, + {TYPE_INTEGER, FORMAT_INT32, null, 0, 2, false, false}, + {TYPE_INTEGER, FORMAT_INT32, null, -100, 0, false, false}, + {TYPE_INTEGER, FORMAT_INT32, null, -2, 0, false, false}, + {TYPE_INTEGER, FORMAT_INT32, null, 0, 100, true, true}, + {TYPE_INTEGER, FORMAT_INT32, null, -100, 0, true, true}, + {TYPE_INTEGER, FORMAT_INT32, null, -2, 0, true, true}, + {TYPE_INTEGER, FORMAT_INT32, null, 0, null, false, false}, + {TYPE_INTEGER, FORMAT_INT32, null, 0, 0, false, false}, + {TYPE_INTEGER, FORMAT_INT32, 11, 0, 12, true, true}, + {TYPE_INTEGER, FORMAT_INT32, 12, null, null, false, false}, + {TYPE_INTEGER, FORMAT_INT32, 13, 0, 100, false, false}, + {TYPE_INTEGER, FORMAT_INT32, 14, 0, 14, false, false}, + {TYPE_INTEGER, FORMAT_INT32, 15, -100, 0, false, false}, + {TYPE_INTEGER, FORMAT_INT32, 16, -16, 0, false, false}, + {TYPE_INTEGER, FORMAT_INT32, 17, 0, 100, true, true}, + {TYPE_INTEGER, FORMAT_INT32, 18, -100, 0, true, true}, + {TYPE_INTEGER, FORMAT_INT32, 19, -20, 0, true, true}, + {TYPE_INTEGER, FORMAT_INT32, 20, 0, null, false, false}, + {TYPE_INTEGER, FORMAT_INT32, 21, 21, 21, false, false}, + + {TYPE_INTEGER, FORMAT_INT64, null, 0, 2, true, true}, + {TYPE_INTEGER, FORMAT_INT64, null, null, null, false, false}, + {TYPE_INTEGER, FORMAT_INT64, null, 0, 100, false, false}, + {TYPE_INTEGER, FORMAT_INT64, null, 0, 2, false, false}, + {TYPE_INTEGER, FORMAT_INT64, null, -100, 0, false, false}, + {TYPE_INTEGER, FORMAT_INT64, null, -2, 0, false, false}, + {TYPE_INTEGER, FORMAT_INT64, null, 0, 100, true, true}, + {TYPE_INTEGER, FORMAT_INT64, null, -100, 0, true, true}, + {TYPE_INTEGER, FORMAT_INT64, null, -2, 0, true, true}, + {TYPE_INTEGER, FORMAT_INT64, null, 0, null, false, false}, + {TYPE_INTEGER, FORMAT_INT64, null, 0, 0, false, false}, + {TYPE_INTEGER, FORMAT_INT64, 11, 0, 12, true, true}, + {TYPE_INTEGER, FORMAT_INT64, 12, null, null, false, false}, + {TYPE_INTEGER, FORMAT_INT64, 13, 0, 100, false, false}, + {TYPE_INTEGER, FORMAT_INT64, 14, 0, 14, false, false}, + {TYPE_INTEGER, FORMAT_INT64, 15, -100, 0, false, false}, + {TYPE_INTEGER, FORMAT_INT64, 16, -16, 0, false, false}, + {TYPE_INTEGER, FORMAT_INT64, 17, 0, 100, true, true}, + {TYPE_INTEGER, FORMAT_INT64, 18, -100, 0, true, true}, + {TYPE_INTEGER, FORMAT_INT64, 19, -20, 0, true, true}, + {TYPE_INTEGER, FORMAT_INT64, 20, 0, null, false, false}, + {TYPE_INTEGER, FORMAT_INT64, 21, 21, 21, false, false}, + + {TYPE_NUMBER, FORMAT_FLOAT, null, 0, 2, true, true}, + {TYPE_NUMBER, FORMAT_FLOAT, null, null, null, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, null, 0, 100, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, null, 0, 2, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, null, -100, 0, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, null, -2, 0, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, null, 0, 100, true, true}, + {TYPE_NUMBER, FORMAT_FLOAT, null, -100, 0, true, true}, + {TYPE_NUMBER, FORMAT_FLOAT, null, -2, 0, true, true}, + {TYPE_NUMBER, FORMAT_FLOAT, null, 0, null, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, null, 0, 0, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, 11.123f, 0, 13, true, true}, + {TYPE_NUMBER, FORMAT_FLOAT, 12.123f, null, null, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, 13.123f, 0, 100, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, 14.123f, 0, 14, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, 15.123f, -100, 0, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, 16.123f, -16, 0, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, 17.123f, 0, 100, true, true}, + {TYPE_NUMBER, FORMAT_FLOAT, 18.123f, -100, 0, true, true}, + {TYPE_NUMBER, FORMAT_FLOAT, 19.123f, -21, 0, true, true}, + {TYPE_NUMBER, FORMAT_FLOAT, 20.123f, 0, null, false, false}, + {TYPE_NUMBER, FORMAT_FLOAT, 21.123f, 21.122f, 21.124f, false, false}, + + {TYPE_NUMBER, FORMAT_DOUBLE, null, 0, 2, true, true}, + {TYPE_NUMBER, FORMAT_DOUBLE, null, null, null, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, null, 0, 100, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, null, 0, 2, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, null, -100, 0, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, null, -2, 0, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, null, 0, 100, true, true}, + {TYPE_NUMBER, FORMAT_DOUBLE, null, -100, 0, true, true}, + {TYPE_NUMBER, FORMAT_DOUBLE, null, -2, 0, true, true}, + {TYPE_NUMBER, FORMAT_DOUBLE, null, 0, null, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, null, 0, 0, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, 11.123d, 0, 13, true, true}, + {TYPE_NUMBER, FORMAT_DOUBLE, 12.123d, null, null, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, 13.123d, 0, 100, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, 14.123d, 0, 14, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, 15.123d, -100, 0, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, 16.123d, -16, 0, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, 17.123d, 0, 100, true, true}, + {TYPE_NUMBER, FORMAT_DOUBLE, 18.123d, -100, 0, true, true}, + {TYPE_NUMBER, FORMAT_DOUBLE, 19.123d, -21, 0, true, true}, + {TYPE_NUMBER, FORMAT_DOUBLE, 20.123d, 0, null, false, false}, + {TYPE_NUMBER, FORMAT_DOUBLE, 21.123d, 21.122d, 21.124d, false, false}, + }; + } + + @DataProvider(name = "testPingApiSchemas") + public static Object[][] testPingApiSchemas() { + return new Object[][]{ + // Composites currently do not work properly - validation fails + //{"AnyOfType"}, + //{"AllOfType"}, + //{"PingRespType"}, + {"OneOfType"}, + {"StringsType"}, + {"DatesType"}, + {"NumbersType"}, + {"PingReqType"}, + {"Detail1"}, + {"Detail2"}, + {"BooleanType"}, + {"EnumType"}, + {"NestedType"}, + {"MultipleOfType"}, + {"SimpleArrayType"}, + {"ComplexArrayType"}, + {"ArrayOfArraysType"}, + {"NullableType"}, + {"DefaultValueType"}, + }; + } + + @Test + public void testUuidFormat() { + Oas30Schema stringSchema = new Oas30Schema(); + stringSchema.type = TYPE_STRING; + stringSchema.format = FORMAT_UUID; + + String uuidRandomValue = OpenApiTestDataGenerator.createRandomValueExpression(stringSchema); + String finalUuidRandomValue = testContext.replaceDynamicContentInString(uuidRandomValue); + Pattern uuidPattern = Pattern.compile( + "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + + assertTrue(uuidPattern.matcher(finalUuidRandomValue).matches()); + } + + @Test(dataProvider = "testRandomNumber") + public void testRandomNumber(String type, + String format, + Number multipleOf, + Number minimum, + Number maximum, + boolean exclusiveMinimum, + boolean exclusiveMaximum) { + Oas30Schema testSchema = new Oas30Schema(); + testSchema.type = type; + testSchema.format = format; + testSchema.multipleOf = multipleOf; + testSchema.minimum = minimum; + testSchema.maximum = maximum; + testSchema.exclusiveMinimum = exclusiveMinimum; + testSchema.exclusiveMaximum = exclusiveMaximum; + + for (int i = 0; i < 1000; i++) { + String randomValue = createOutboundPayload( + testSchema, openApiSpecification); + String finalRandomValue = testContext.resolveDynamicValue(randomValue); + BigDecimal value = new BigDecimal(finalRandomValue); + + if (multipleOf != null) { + BigDecimal remainder = value.remainder(new BigDecimal(multipleOf.toString())); + + assertEquals( + remainder.compareTo(BigDecimal.ZERO), 0, + "Expected %s to be a multiple of %s! Remainder is %s".formatted( + finalRandomValue, multipleOf, + remainder)); + } + + if (maximum != null) { + if (exclusiveMaximum) { + assertTrue(value.doubleValue() < testSchema.maximum.doubleValue(), + "Expected %s to be lower than %s!".formatted( + finalRandomValue, maximum)); + } else { + assertTrue(value.doubleValue() <= testSchema.maximum.doubleValue(), + "Expected %s to be lower or equal than %s!".formatted( + finalRandomValue, maximum)); + } + } + + if (minimum != null) { + if (exclusiveMinimum) { + assertTrue(value.doubleValue() > testSchema.minimum.doubleValue(), + "Expected %s to be larger than %s!".formatted( + finalRandomValue, minimum)); + } else { + assertTrue(value.doubleValue() >= testSchema.minimum.doubleValue(), + "Expected %s to be larger or equal than %s!".formatted( + finalRandomValue, minimum)); + } + } + } + } + + @Test + public void testPattern() { + Oas30Schema stringSchema = new Oas30Schema(); + stringSchema.type = TYPE_STRING; + + String exp = "[0-3]([a-c]|[e-g]{1,2})"; + stringSchema.pattern = exp; + + String randomValue = OpenApiTestDataGenerator.createRandomValueExpression(stringSchema); + String finalRandomValue = testContext.replaceDynamicContentInString(randomValue); + assertTrue(finalRandomValue.matches(exp), "Value '%s' does not match expression '%s'".formatted(finalRandomValue, exp)); + } + + @Test(dataProvider = "testPingApiSchemas") + public void testPingApiSchemas(String schemaType) throws IOException { + OasSchema schema = OasModelHelper.getSchemaDefinitions( + openApiSpecification.getOpenApiDoc(null)).get(schemaType); + + Schema swaggerValidationSchema = openApiSpecification.getOpenApiValidationContext() + .getSwaggerOpenApi().getComponents().getSchemas().get(schemaType); + + assertNotNull(schema); + + for (int i = 0; i < 100; i++) { + String randomValue = createOutboundPayload(schema, openApiSpecification); + assertNotNull(randomValue); + + String finalJsonAsText = testContext.replaceDynamicContentInString(randomValue); + + JsonNode valueNode = new ObjectMapper().readTree( + testContext.replaceDynamicContentInString(finalJsonAsText)); + ValidationReport validationReport = schemaValidator.validate(() -> valueNode, + swaggerValidationSchema, null); + + String message = """ + Json is invalid according to schema. + Message: %s + Report: %s + """.formatted(finalJsonAsText, validationReport.getMessages().stream().map( + Message::getMessage).collect(Collectors.joining("\n"))); + assertFalse(validationReport.hasErrors(), message); + } + } + + @Test + public void testArray() { + Oas30Schema arraySchema = new Oas30Schema(); + arraySchema.type = TYPE_ARRAY; + + Oas30Schema stringSchema = new Oas30Schema(); + stringSchema.type = TYPE_STRING; + stringSchema.minLength = 5; + stringSchema.maxLength = 15; + + arraySchema.items = stringSchema; + + for (int i = 0; i < 10; i++) { + String randomValue = createOutboundPayload(arraySchema, openApiSpecification); + int nElements = StringUtils.countMatches(randomValue, "citrus:randomString"); + assertTrue(nElements > 0); + } + } + + @Test + public void testArrayMinItems() { + Oas30Schema arraySchema = new Oas30Schema(); + arraySchema.type = TYPE_ARRAY; + arraySchema.minItems = 5; + + Oas30Schema stringSchema = new Oas30Schema(); + stringSchema.type = TYPE_STRING; + stringSchema.minLength = 5; + stringSchema.maxLength = 15; + + arraySchema.items = stringSchema; + + for (int i = 0; i < 10; i++) { + String randomValue = createOutboundPayload(arraySchema, openApiSpecification); + int nElements = StringUtils.countMatches(randomValue, "citrus:randomString(15)"); + assertTrue(nElements <= 5); + } + } + + @Test + public void testArrayMaxItems() { + Oas30Schema arraySchema = new Oas30Schema(); + arraySchema.type = TYPE_ARRAY; + arraySchema.minItems = 2; + arraySchema.maxItems = 5; + + Oas30Schema stringSchema = new Oas30Schema(); + stringSchema.type = TYPE_STRING; + stringSchema.minLength = 10; + stringSchema.maxLength = 15; + + arraySchema.items = stringSchema; + + Pattern pattern = Pattern.compile("citrus:randomString\\(1[0-5],MIXED,true,10\\)"); + for (int i = 0; i < 100; i++) { + String randomArrayValue = createOutboundPayload(arraySchema, openApiSpecification); + + Matcher matcher = pattern.matcher(randomArrayValue); + int matches = 0; + while (matcher.find()) { + matches++; + } + + assertTrue(2 <= matches && matches <= 5, + "Expected random array string with number of elements between 2 and 4 but found %s: %s".formatted( + matches, randomArrayValue)); + } + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiUtilsTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiUtilsTest.java new file mode 100644 index 0000000000..887237cbf8 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/OpenApiUtilsTest.java @@ -0,0 +1,126 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import org.citrusframework.spi.ReferenceResolver; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.Map; +import java.util.Set; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.citrusframework.openapi.util.OpenApiUtils.getKnownOpenApiAliases; +import static org.citrusframework.openapi.util.OpenApiUtils.getMethodPath; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class OpenApiUtilsTest { + + private AutoCloseable mockCloseable; + + @BeforeMethod + public void beforeMethod() { + mockCloseable = MockitoAnnotations.openMocks(this); + } + + @AfterMethod + public void afterMethod() throws Exception { + mockCloseable.close(); + } + + @Test + public void shouldReturnFormattedMethodPathWhenMethodAndPathAreProvided() { + // When + String methodPath = getMethodPath("POST", "/api/path"); + // Then + assertEquals(methodPath, "POST_/api/path"); + } + + @Test + public void shouldReturnFormattedMethodPathWhenMethodIsEmptyAndPathIsProvided() { + // When + String methodPath = getMethodPath("", "/api/path"); + // Then + assertEquals(methodPath, "_/api/path"); + } + + @Test + public void shouldReturnFormattedMethodPathWhenMethodAndPathAreEmpty() { + // When + String methodPath = getMethodPath("", ""); + // Then + assertEquals(methodPath, "_/"); + } + + @Test + public void testGetKnownOpenApiAliases() { + + ReferenceResolver resolver = mock(); + OpenApiRepository repository1 = mock(); + OpenApiRepository repository2 = mock(); + OpenApiSpecification spec1 = mock(); + OpenApiSpecification spec2 = mock(); + + when(resolver.resolveAll(OpenApiRepository.class)).thenReturn( + Map.of( + "repo1", repository1, + "repo2", repository2 + ) + ); + + when(repository1.getOpenApiSpecifications()).thenReturn(singletonList(spec1)); + when(repository2.getOpenApiSpecifications()).thenReturn(singletonList(spec2)); + + when(spec1.getAliases()).thenReturn(Set.of("alias1", "alias2")); + when(spec2.getAliases()).thenReturn(Set.of("alias3")); + + String result = getKnownOpenApiAliases(resolver); + + assertTrue(result.contains("alias1")); + assertTrue(result.contains("alias2")); + assertTrue(result.contains("alias3")); + } + + @Test + public void testGetKnownOpenApiAliasesNoAliases() { + ReferenceResolver resolver = mock(); + OpenApiRepository repository1 = mock(); + OpenApiRepository repository2 = mock(); + + when(resolver.resolveAll(OpenApiRepository.class)).thenReturn( + Map.of( + "repo1", repository1, + "repo2", repository2 + ) + ); + + when(repository1.getOpenApiSpecifications()).thenReturn(emptyList()); + when(repository2.getOpenApiSpecifications()).thenReturn(emptyList()); + + // Call the method under test + String result = getKnownOpenApiAliases(resolver); + + // Verify the result + assertEquals(result, ""); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/SampleOpenApiProcessor.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/SampleOpenApiProcessor.java new file mode 100644 index 0000000000..c39605e35e --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/SampleOpenApiProcessor.java @@ -0,0 +1,30 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi; + +import java.util.ArrayList; +import java.util.List; + +public class SampleOpenApiProcessor implements OpenApiSpecificationProcessor { + + public static List processedSpecifications = new ArrayList<>(); + + @Override + public void process(OpenApiSpecification openApiSpecification) { + processedSpecifications.add(openApiSpecification); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilderTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilderTest.java index 1095cba8e2..e739fc9b65 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilderTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiClientActionBuilderTest.java @@ -1,13 +1,11 @@ package org.citrusframework.openapi.actions; import org.citrusframework.endpoint.Endpoint; -import org.citrusframework.openapi.OpenApiSpecification; -import org.citrusframework.spi.AbstractReferenceResolverAwareTestActionBuilder; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import static org.mockito.Mockito.mock; -import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertNotNull; public class OpenApiClientActionBuilderTest { @@ -15,11 +13,11 @@ public class OpenApiClientActionBuilderTest { @BeforeMethod public void beforeMethod() { - fixture = new OpenApiClientActionBuilder(mock(Endpoint.class), mock(OpenApiSpecification.class)); + fixture = new OpenApiClientActionBuilder(mock(Endpoint.class), mock(OpenApiSpecificationSource.class)); } @Test public void isReferenceResolverAwareTestActionBuilder() { - assertTrue(fixture instanceof AbstractReferenceResolverAwareTestActionBuilder, "Is instanceof AbstractReferenceResolverAwareTestActionBuilder"); + assertNotNull(fixture, "Is instanceof AbstractReferenceResolverAwareTestActionBuilder"); } } diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiClientRequestMessageBuilderTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiClientRequestMessageBuilderTest.java index 1de662ca01..8bdfa20d07 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiClientRequestMessageBuilderTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiClientRequestMessageBuilderTest.java @@ -1,26 +1,28 @@ package org.citrusframework.openapi.actions; +import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi; + import org.citrusframework.context.TestContext; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.message.Message; +import org.citrusframework.openapi.AutoFillType; import org.citrusframework.openapi.OpenApiSpecification; import org.citrusframework.spi.Resources; import org.testng.Assert; import org.testng.annotations.Test; -import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi; - public class OpenApiClientRequestMessageBuilderTest { private final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-derivation-for-message-builder-test.json")); @Test - public void shouldAddRandomDataForOperation() { + public void shouldAddRandomDataForOperationWhenAutoFillAll() { Message message = openapi() .specification(petstoreSpec) .client() .send("addPet") // operationId + .autoFill(AutoFillType.ALL) .build() .getMessageBuilder() .build(new TestContext(), ""); @@ -31,8 +33,53 @@ public void shouldAddRandomDataForOperation() { Assert.assertNotNull(payload); Assert.assertTrue(payload instanceof String); // test header - Object header = httpMessage.getHeader("X-SAMPLE-HEADER"); - Assert.assertNotNull(header); + Assert.assertNotNull(httpMessage.getHeader("X-SAMPLE-HEADER")); + Assert.assertNotNull(httpMessage.getQueryParams().get("sample-param")); + Assert.assertNotNull(httpMessage.getQueryParams().get("non-required-sample-param")); + } + + @Test + public void shouldAddRandomDataForOperationWhenAutoFillRequired() { + Message message = openapi() + .specification(petstoreSpec) + .client() + .send("addPet") // operationId + .autoFill(AutoFillType.REQUIRED) + .build() + .getMessageBuilder() + .build(new TestContext(), ""); + Assert.assertTrue(message instanceof HttpMessage); + HttpMessage httpMessage = (HttpMessage) message; + // test payload + Object payload = httpMessage.getPayload(); + Assert.assertNotNull(payload); + Assert.assertTrue(payload instanceof String); + // test header + Assert.assertNotNull(httpMessage.getHeader("X-SAMPLE-HEADER")); + Assert.assertNotNull(httpMessage.getQueryParams().get("sample-param")); + Assert.assertNull(httpMessage.getQueryParams().get("non-required-sample-param")); + } + + @Test + public void shouldNotAddRandomDataForOperationWhenAutoFillNone() { + Message message = openapi() + .specification(petstoreSpec) + .client() + .send("addPet") // operationId + .autoFill(AutoFillType.NONE) + .build() + .getMessageBuilder() + .build(new TestContext(), ""); + Assert.assertTrue(message instanceof HttpMessage); + HttpMessage httpMessage = (HttpMessage) message; + // test payload + Object payload = httpMessage.getPayload(); + Assert.assertNotNull(payload); + Assert.assertTrue(payload instanceof String); + // test header + Assert.assertNull(httpMessage.getHeader("X-SAMPLE-HEADER")); + Assert.assertNull(httpMessage.getQueryParams().get("sample-param")); + Assert.assertNull(httpMessage.getQueryParams().get("non-required-sample-param")); } @Test diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiPayloadBuilderTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiPayloadBuilderTest.java new file mode 100644 index 0000000000..f4ed6a3b05 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiPayloadBuilderTest.java @@ -0,0 +1,67 @@ +package org.citrusframework.openapi.actions; + +import org.citrusframework.context.TestContext; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class OpenApiPayloadBuilderTest { + + private TestContext context; + + @BeforeClass + public void setUp() { + context = new TestContext(); + } + + @Test + public void testBuildPayloadWithMultiValueMap() { + // Given + MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); + multiValueMap.add("key1", "value1"); + multiValueMap.add("key2", "Hello ${user}, welcome!"); + multiValueMap.add("key2", "Another ${user} message"); + multiValueMap.add("${k3}", "a"); + multiValueMap.add("${k3}", "b"); + multiValueMap.add("${k3}", "${user}"); + + context.setVariable("user", "John"); + context.setVariable("k3", "key3"); + + OpenApiPayloadBuilder payloadBuilder = new OpenApiPayloadBuilder(multiValueMap); + + // When + Object payload = payloadBuilder.buildPayload(context); + + // Then + assertTrue(payload instanceof MultiValueMap); + MultiValueMap result = (MultiValueMap) payload; + + assertEquals(result.get("key1").get(0), "value1"); + assertEquals(result.get("key2").get(0), "Hello John, welcome!"); + assertEquals(result.get("key2").get(1), "Another John message"); + assertEquals(result.get("key3").get(0), "a"); + assertEquals(result.get("key3").get(1), "b"); + assertEquals(result.get("key3").get(2), "John"); + } + + @Test + public void testBuildPayloadWithPlainObject() { + // Given + String simplePayload = "This is a simple ${message}"; + context.setVariable("message", "test"); + + OpenApiPayloadBuilder payloadBuilder = new OpenApiPayloadBuilder(simplePayload); + + // When + Object payload = payloadBuilder.buildPayload(context); + + // Then + assertTrue(payload instanceof String); + assertEquals(payload, "This is a simple test"); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilderTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilderTest.java index dce3c9962f..23e387ab17 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilderTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/actions/OpenApiServerActionBuilderTest.java @@ -1,7 +1,6 @@ package org.citrusframework.openapi.actions; import org.citrusframework.endpoint.Endpoint; -import org.citrusframework.openapi.OpenApiSpecification; import org.citrusframework.spi.AbstractReferenceResolverAwareTestActionBuilder; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -15,7 +14,7 @@ public class OpenApiServerActionBuilderTest { @BeforeMethod public void beforeMethod() { - fixture = new OpenApiServerActionBuilder(mock(Endpoint.class), mock(OpenApiSpecification.class)); + fixture = new OpenApiServerActionBuilder(mock(Endpoint.class), mock(OpenApiSpecificationSource.class)); } @Test 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 index b1c9bc7ad9..430d00ea25 100644 --- 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 @@ -62,7 +62,7 @@ protected TestContext createTestContext() { CitrusAnnotations.parseConfiguration(invocationOnMock.getArgument(0, Object.class), citrusContext); return null; }).when(citrusContext).parseConfiguration((Object) any()); - doAnswer(invocationOnMock-> { + doAnswer(invocationOnMock -> { context.getReferenceResolver().bind(invocationOnMock.getArgument(0), invocationOnMock.getArgument(1)); return null; }).when(citrusContext).addComponent(anyString(), any()); diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiClientTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiClientTest.java index b65e3f9ed7..bc9a4d14b9 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiClientTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/groovy/OpenApiClientTest.java @@ -16,11 +16,6 @@ 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; @@ -52,12 +47,21 @@ 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 java.util.Map; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; + import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; public class OpenApiClientTest extends AbstractGroovyActionDslTest { @@ -72,13 +76,10 @@ public class OpenApiClientTest extends AbstractGroovyActionDslTest { 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); + private HttpServer httpServer; + private HttpClient httpClient; @BeforeClass public void setupEndpoints() { @@ -113,7 +114,7 @@ public void cleanupEndpoints() { } @Test - public void shouldLoadOpenApiClientActions() throws IOException { + public void shouldLoadOpenApiClientActions() { GroovyTestLoader testLoader = createTestLoader("classpath:org/citrusframework/openapi/groovy/openapi-client.test.groovy"); context.setVariable("port", port); @@ -138,39 +139,39 @@ public void shouldLoadOpenApiClientActions() throws IOException { ], "status": "available" } - """).status(HttpStatus.OK).contentType("application/json")); + """).status(HttpStatus.OK).contentType(APPLICATION_JSON_VALUE)); 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"); + assertEquals(result.getName(), "OpenApiClientTest"); + assertEquals(result.getMetaInfo().getAuthor(), "Christoph"); + assertEquals(result.getMetaInfo().getStatus(), TestCaseMetaInfo.Status.FINAL); + assertEquals(result.getActionCount(), 4L); + assertEquals(result.getTestAction(0).getClass(), SendMessageAction.class); + 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"); + assertEquals(result.getTestAction(1).getClass(), ReceiveMessageAction.class); + 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); + assertFalse(sendMessageAction.isForkMode()); + assertTrue(sendMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); + HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder) sendMessageAction.getMessageBuilder()); + assertNotNull(httpMessageBuilder); + assertEquals(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()), ""); + assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.GET.name()); + assertEquals(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/petstore/v3/pet/${petId}"); + assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REQUEST_URI), "/petstore/v3/pet/${petId}"); + assertNull(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_QUERY_PARAMS)); + assertNull(httpMessageBuilder.getMessage().getHeaders().get(EndpointUriResolver.ENDPOINT_URI_HEADER_NAME)); + assertEquals(sendMessageAction.getEndpoint(), httpClient); Message controlMessage = new DefaultMessage(""); Message request = inboundQueue.receive(); @@ -178,63 +179,60 @@ public void shouldLoadOpenApiClientActions() throws IOException { validator.validateMessage(request, controlMessage, context, new DefaultValidationContext()); ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); - Assert.assertEquals(receiveMessageAction.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); + assertEquals(receiveMessageAction.getValidationContexts().size(), 4); + assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); + assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + + httpMessageBuilder = ((HttpMessageBuilder) receiveMessageAction.getMessageBuilder()); + assertNotNull(httpMessageBuilder); + + assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 4L); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_STATUS_CODE), 200); + assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REASON_PHRASE), "OK"); + assertNull(receiveMessageAction.getEndpoint()); + assertEquals(receiveMessageAction.getEndpointUri(), "httpClient"); + assertEquals(receiveMessageAction.getMessageProcessors().size(), 1); + 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)); + assertFalse(sendMessageAction.isForkMode()); + httpMessageBuilder = ((HttpMessageBuilder) sendMessageAction.getMessageBuilder()); + assertNotNull(httpMessageBuilder); + assertTrue(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()).toString().startsWith("{\"id\": ")); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + 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); + assertEquals(requestHeaders.size(), 4L); + assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_METHOD), HttpMethod.POST.name()); + assertEquals(requestHeaders.get(EndpointUriResolver.REQUEST_PATH_HEADER_NAME), "/petstore/v3/pet"); + assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_REQUEST_URI), "/petstore/v3/pet"); + assertEquals(requestHeaders.get(HttpMessageHeaders.HTTP_CONTENT_TYPE), APPLICATION_JSON_VALUE); + assertNull(sendMessageAction.getEndpointUri()); + 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)); + assertEquals(receiveMessageAction.getValidationContexts().size(), 4); + assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + + httpMessageBuilder = ((HttpMessageBuilder) receiveMessageAction.getMessageBuilder()); + assertNotNull(httpMessageBuilder); + assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + 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"); + assertEquals(responseHeaders.size(), 2L); + assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_STATUS_CODE), 201); + assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_REASON_PHRASE), "CREATED"); + assertNull(receiveMessageAction.getEndpoint()); + assertEquals(receiveMessageAction.getEndpointUri(), "httpClient"); - Assert.assertEquals(receiveMessageAction.getVariableExtractors().size(), 0L); + 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 index 151687da4d..43164a750b 100644 --- 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 @@ -16,8 +16,6 @@ package org.citrusframework.openapi.groovy; -import java.util.Map; - import org.citrusframework.TestActor; import org.citrusframework.TestCase; import org.citrusframework.TestCaseMetaInfo; @@ -35,6 +33,7 @@ import org.citrusframework.message.DefaultMessageQueue; import org.citrusframework.message.MessageHeaders; import org.citrusframework.message.MessageQueue; +import org.citrusframework.openapi.validation.OpenApiMessageValidationContext; import org.citrusframework.spi.BindToRegistry; import org.citrusframework.validation.context.HeaderValidationContext; import org.citrusframework.validation.json.JsonMessageValidationContext; @@ -45,22 +44,26 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.util.Map; + import static org.citrusframework.endpoint.direct.DirectEndpoints.direct; import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; 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()); + private HttpServer httpServer; @BeforeClass public void setupEndpoints() { @@ -81,119 +84,111 @@ public void shouldLoadOpenApiServerActions() { 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")); + .method(HttpMethod.GET) + .path("/petstore/v3/pet/12345") + .version("HTTP/1.1") + .accept(APPLICATION_JSON_VALUE) + .contentType(APPLICATION_JSON_VALUE)); 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")); + { + "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_VALUE)); 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"); + assertEquals(result.getName(), "OpenApiServerTest"); + assertEquals(result.getMetaInfo().getAuthor(), "Christoph"); + assertEquals(result.getMetaInfo().getStatus(), TestCaseMetaInfo.Status.FINAL); + assertEquals(result.getActionCount(), 4L); + assertEquals(result.getTestAction(0).getClass(), ReceiveMessageAction.class); + 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"); + assertEquals(result.getTestAction(1).getClass(), SendMessageAction.class); + 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); + assertEquals(receiveMessageAction.getValidationContexts().size(), 4); + assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + assertTrue(receiveMessageAction.getValidationContexts().get(3) instanceof OpenApiMessageValidationContext); + assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); + + assertTrue(receiveMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); + HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder) receiveMessageAction.getMessageBuilder()); + assertNotNull(httpMessageBuilder); + assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); + assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 2L); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + assertEquals(receiveMessageAction.getEndpoint(), httpServer); + 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"); + httpMessageBuilder = ((HttpMessageBuilder) sendMessageAction.getMessageBuilder()); + assertNotNull(httpMessageBuilder); + + assertTrue(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()).toString().startsWith("{\"id\": ")); + assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.TIMESTAMP)); + assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_STATUS_CODE), 200); + assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_REASON_PHRASE), "OK"); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_CONTENT_TYPE), APPLICATION_JSON_VALUE); + Assert.assertNull(sendMessageAction.getEndpoint()); - Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); - Assert.assertEquals(sendMessageAction.getMessageProcessors().size(), 0); + assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); + assertEquals(sendMessageAction.getMessageProcessors().size(), 1); 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)); + assertEquals(receiveMessageAction.getValidationContexts().size(), 4); + assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); + assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); + assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + assertTrue(receiveMessageAction.getValidationContexts().get(3) instanceof OpenApiMessageValidationContext); + assertEquals(receiveMessageAction.getReceiveTimeout(), 2000L); + + httpMessageBuilder = ((HttpMessageBuilder) receiveMessageAction.getMessageBuilder()); + assertNotNull(httpMessageBuilder); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + 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)@"); + assertEquals(requestHeaders.size(), 0L); Assert.assertNull(receiveMessageAction.getEndpointUri()); - Assert.assertEquals(receiveMessageAction.getEndpoint(), httpServer); + 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)); + httpMessageBuilder = ((HttpMessageBuilder) sendMessageAction.getMessageBuilder()); + assertNotNull(httpMessageBuilder); + assertEquals(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()), ""); + assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); + 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"); + assertEquals(responseHeaders.size(), 2L); + assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_STATUS_CODE), 201); + assertEquals(responseHeaders.get(HttpMessageHeaders.HTTP_REASON_PHRASE), "CREATED"); Assert.assertNull(sendMessageAction.getEndpoint()); - Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); + assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); } } diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientApiContextPathIT.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientApiContextPathIT.java new file mode 100644 index 0000000000..f3386fcc41 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientApiContextPathIT.java @@ -0,0 +1,155 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.integration; + +import static org.citrusframework.http.actions.HttpActionBuilder.http; +import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +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.AutoFillType; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.integration.OpenApiClientIT.Config; +import org.citrusframework.spi.Resources; +import org.citrusframework.testng.spring.TestNGCitrusSpringSupport; +import org.citrusframework.util.SocketUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ContextConfiguration; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * This integration test demonstrates how to configure and test OpenAPI specifications with different context paths + * using the Citrus framework. {@link OpenApiSpecification}s can be hooked to different context paths, with detailed control + * over how the context path is constructed. + *

+ * By default, the basePath from the OpenAPI specification is used. If the basePath is not present, the plain operation + * path is used. However, you can configure detailed control over the context path using the `contextPath` and `neglectBasePath` properties. + *

+ * The test uses these specifications with different context paths to verify the correct API paths are used in the HTTP requests and responses. + * + * @see OpenApiSpecification + * @see CitrusTest + */ +@Test +@ContextConfiguration(classes = {Config.class}) +public class OpenApiClientApiContextPathIT extends TestNGCitrusSpringSupport { + + public static final String VALID_PET_PATH = "classpath:org/citrusframework/openapi/petstore/pet.json"; + + @Autowired + private HttpServer httpServer; + + @Autowired + private HttpClient httpClient; + + private static final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( + Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")) + .alias("spec1"); + + private static final OpenApiSpecification petstoreSpecNeglectingBasePath = OpenApiSpecification.from( + Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")) + .alias("spec2") + .neglectBasePath(true); + + private static final OpenApiSpecification petstoreSpecWithContextPath = OpenApiSpecification.from( + Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")) + .alias("spec3") + .rootContextPath("/api"); + + private static final OpenApiSpecification petstoreSpecWithContextPathNeglectingBasePath = OpenApiSpecification.from( + Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")) + .alias("spec4") + .neglectBasePath(true) + .rootContextPath("/api"); + + @DataProvider + public static Object[][] openApiSpecifications() { + return new Object[][]{ + {petstoreSpec, "/petstore/v3/pet"}, + {petstoreSpecNeglectingBasePath, "/pet"}, + {petstoreSpecWithContextPath, "/api/petstore/v3/pet"}, + {petstoreSpecWithContextPathNeglectingBasePath, "/api/pet"}, + }; + } + + @CitrusTest + @Test(dataProvider = "openApiSpecifications") + public void shouldExecuteGetPetById(OpenApiSpecification spec, String contextPath) { + + variable("petId", "1001"); + + when(openapi(spec) + .client(httpClient) + .send("getPetById") + .autoFill(AutoFillType.ALL) + .message() + .fork(true)); + + then(http().server(httpServer) + .receive() + .get(contextPath+"/1001") + .message() + .accept("@contains('application/json')@")); + + then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create(VALID_PET_PATH)) + .contentType(APPLICATION_JSON_VALUE)); + + then(openapi(spec) + .client(httpClient).receive("getPetById", OK) + .schemaValidation(true)); + + } + + + @Configuration + public static class Config { + + private final int port = SocketUtils.findAvailableTcpPort(8080); + + @Bean + public HttpServer httpServer() { + + return new HttpServerBuilder() + .port(port) + .timeout(5000L) + .autoStart(true) + .defaultStatus(HttpStatus.NO_CONTENT) + .build(); + } + + @Bean + public HttpClient httpClient() { + return new HttpClientBuilder() + .requestUrl("http://localhost:%d".formatted(port)) + .build(); + } + + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientIT.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientIT.java index 2c7e8b8f49..1b49c3c1bf 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientIT.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiClientIT.java @@ -16,84 +16,210 @@ package org.citrusframework.openapi.integration; +import static java.util.Collections.singletonList; +import static org.citrusframework.http.actions.HttpActionBuilder.http; +import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi; +import static org.citrusframework.openapi.validation.OpenApiMessageValidationContext.Builder.openApi; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.fail; + import org.citrusframework.annotations.CitrusTest; +import org.citrusframework.exceptions.TestCaseFailedException; +import org.citrusframework.http.actions.HttpClientRequestActionBuilder.HttpMessageBuilderSupport; +import org.citrusframework.http.actions.HttpClientResponseActionBuilder; 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.message.MessageType; +import org.citrusframework.openapi.AutoFillType; +import org.citrusframework.openapi.OpenApiRepository; +import org.citrusframework.openapi.actions.OpenApiClientResponseActionBuilder; +import org.citrusframework.openapi.integration.OpenApiClientIT.Config; +import org.citrusframework.openapi.validation.OpenApiValidationPolicy; import org.citrusframework.spi.Resources; import org.citrusframework.testng.spring.TestNGCitrusSpringSupport; import org.citrusframework.util.SocketUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; +import org.springframework.test.context.ContextConfiguration; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import static org.citrusframework.http.actions.HttpActionBuilder.http; -import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi; - @Test +@ContextConfiguration(classes = {Config.class}) public class OpenApiClientIT extends TestNGCitrusSpringSupport { - private final int port = SocketUtils.findAvailableTcpPort(8080); + public static final String VALID_PET_PATH = "classpath:org/citrusframework/openapi/petstore/pet.json"; + public static final String INVALID_PET_PATH = "classpath:org/citrusframework/openapi/petstore/pet_invalid.json"; + + @Autowired + private HttpServer httpServer; + + @Autowired + private HttpClient httpClient; + + @CitrusTest + @Test + public void shouldExecuteGetPetById() { + + variable("petId", "1001"); + + when(openapi("petstore-v3") + .client(httpClient) + .send("getPetById") + .message() + .fork(true)); + + then(http().server(httpServer) + .receive() + .get("/pet/1001") + .message() + .accept("@contains('application/json')@")); - @BindToRegistry - private final HttpServer httpServer = new HttpServerBuilder() - .port(port) - .timeout(5000L) - .autoStart(true) - .defaultStatus(HttpStatus.NO_CONTENT) - .build(); + then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create(VALID_PET_PATH)) + .contentType(APPLICATION_JSON_VALUE)); - @BindToRegistry - private final HttpClient httpClient = new HttpClientBuilder() - .requestUrl("http://localhost:%d".formatted(port)) - .build(); + then(openapi("petstore-v3") + .client(httpClient).receive("getPetById", OK) + .schemaValidation(true)); - private final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( - Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); + } @CitrusTest - public void getPetById() { + @Test + public void shouldFailOnMissingNameInResponse() { + variable("petId", "1001"); - when(openapi(petstoreSpec) - .client(httpClient) - .send("getPetById") - .fork(true)); + when(openapi("petstore-v3") + .client(httpClient) + .send("getPetById") + .autoFill(AutoFillType.ALL) + .message() + .fork(true)); then(http().server(httpServer) - .receive() - .get("/pet/${petId}") - .message() - .accept("@contains('application/json')@")); + .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)); + .send() + .response(OK) + .message() + .body(Resources.create(INVALID_PET_PATH)) + .contentType(APPLICATION_JSON_VALUE)); + + OpenApiClientResponseActionBuilder clientResponseActionBuilder = openapi("petstore-v3") + .client(httpClient).receive("getPetById", OK) + .schemaValidation(true); + assertThrows(() -> then(clientResponseActionBuilder)); } + /** + * Tests validation of plain http client open api schema validation using + * {@link org.citrusframework.openapi.validation.OpenApiValidationContext} + */ @CitrusTest - public void getAddPet() { + @Test + public void shouldFailOnMissingNameInResponseWhenUsingExplicitSchemaValidation() { variable("petId", "1001"); - when(openapi(petstoreSpec) - .client(httpClient) - .send("addPet") - .fork(true)); + when(http() + .client(httpClient) + .send() + .get("/pet/1001") + .message() + .fork(true)); then(http().server(httpServer) - .receive() - .post("/pet") - .message() - .body(""" + .receive() + .get("/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create(INVALID_PET_PATH)) + .contentType(APPLICATION_JSON_VALUE)); + + // We can validate an OpenAPI response against a specific schema, + // even without using open api client. + HttpClientResponseActionBuilder.HttpMessageBuilderSupport receiveGetPetById = http().client( + httpClient) + .receive() + .response(OK) + .message() + .type(MessageType.JSON) + .validate(openApi(null) + .schemaValidation(true) + .schemaRepository("petstore-v3.json") + .schema("getPetById")); + + assertThrows(TestCaseFailedException.class, () -> then(receiveGetPetById)); + + } + + @CitrusTest + @Test + public void shouldSucceedOnMissingNameInResponseWithValidationDisabled() { + + variable("petId", "1001"); + + when(openapi("petstore-v3") + .client(httpClient) + .send("getPetById") + .autoFill(AutoFillType.ALL) + .message() + .fork(true)); + + then(http().server(httpServer) + .receive() + .get("/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create(INVALID_PET_PATH)) + .contentType(APPLICATION_JSON_VALUE)); + + then(openapi("petstore-v3") + .client(httpClient).receive("getPetById", OK) + .schemaValidation(false)); + + } + + @CitrusTest + @Test + public void shouldProperlyExecuteAddPetFromRepository() { + variable("petId", "1001"); + + when(openapi("petstore-v3") + .client(httpClient) + .send("addPet") + .autoFill(AutoFillType.ALL) + .fork(true)); + + then(http().server(httpServer) + .receive() + .post("/pet") + .message() + .body(""" { "id": "@isNumber()@", "name": "@notEmpty()@", @@ -106,15 +232,134 @@ public void getAddPet() { "status": "@matches(sold|pending|available)@" } """) - .contentType("application/json;charset=UTF-8")); + .contentType("application/json;charset=UTF-8")); + + then(http().server(httpServer) + .send() + .response(HttpStatus.CREATED) + .message()); + + then(openapi("petstore-v3") + .client(httpClient) + .receive("addPet", HttpStatus.CREATED)); + + } + + @CitrusTest + @Test + public void shouldFailOnMissingNameInRequest() { + variable("petId", "1001"); + + HttpMessageBuilderSupport addPetBuilder = openapi("petstore-v3") + .client(httpClient) + .send("addPet") + .message().body(Resources.create(INVALID_PET_PATH)); + + assertThrows(TestCaseFailedException.class, () -> when(addPetBuilder)); + } + + @CitrusTest + @Test + public void shouldFailOnWrongQueryIdType() { + variable("petId", "xxxx"); + HttpMessageBuilderSupport addPetBuilder = openapi("petstore-v3") + .client(httpClient) + .send("addPet") + .message().body(Resources.create(VALID_PET_PATH)); + assertThrows(TestCaseFailedException.class, () -> when(addPetBuilder)); + } + + @CitrusTest + @Test + public void shouldSucceedOnWrongQueryIdTypeWithOasDisabled() { + variable("petId", "xxxx"); + HttpMessageBuilderSupport addPetBuilder = openapi("petstore-v3") + .client(httpClient) + .send("addPet") + .schemaValidation(false) + .message().body(Resources.create(VALID_PET_PATH)); + + try { + when(addPetBuilder); + } catch (Exception e) { + fail("Method threw an exception: " + e.getMessage()); + } + + } + + @DataProvider + public Object[][] aliases() { + return new Object[][] { + {"petstore-v3"}, + {"Swagger Petstore"}, + {"Swagger Petstore/1.0.1"}, + }; + } + + /** + * Using OpenApiRepository, we should be able to resolve the OpenAPI spec from the name of the + * spec file or one of its aliases. + */ + @CitrusTest + @Test(dataProvider = "aliases") + public void getPetByIdUsingOpenApiRepositoryAndAlias(String alias) { + variable("petId", "1001"); + + when(openapi(alias) + .client(httpClient) + .send("getPetById") + .autoFill(AutoFillType.ALL) + .message() + .fork(true)); then(http().server(httpServer) - .send() - .response(HttpStatus.CREATED) - .message()); + .receive() + .get("/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create(VALID_PET_PATH)) + .contentType(APPLICATION_JSON_VALUE)); + + then(openapi(alias) + .client(httpClient).receive("getPetById", OK) + .schemaValidation(true)); + } + + @Configuration + public static class Config { + + private final int port = SocketUtils.findAvailableTcpPort(8080); + + @Bean + public HttpServer httpServer() { + + return new HttpServerBuilder() + .port(port) + .timeout(5000L) + .autoStart(true) + .defaultStatus(HttpStatus.NO_CONTENT) + .build(); + } + + @Bean + public HttpClient httpClient() { + return new HttpClientBuilder() + .requestUrl("http://localhost:%d".formatted(port)) + .build(); + } - then(openapi(petstoreSpec) - .client(httpClient) - .receive("addPet", HttpStatus.CREATED)); + @Bean + public OpenApiRepository petstoreOpenApiRepository() { + return new OpenApiRepository() + .locations(singletonList( + "classpath:org/citrusframework/openapi/petstore/petstore-v3.json")) + .neglectBasePath(true) + .validationPolicy(OpenApiValidationPolicy.REPORT); + } } } diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiServerIT.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiServerIT.java index 0d2c7bcc05..1a1a19e820 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiServerIT.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/integration/OpenApiServerIT.java @@ -16,62 +16,217 @@ package org.citrusframework.openapi.integration; +import static java.util.Collections.singletonList; +import static org.citrusframework.http.actions.HttpActionBuilder.http; +import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.fail; + import org.citrusframework.annotations.CitrusTest; +import org.citrusframework.exceptions.TestCaseFailedException; +import org.citrusframework.http.actions.HttpServerResponseActionBuilder.HttpMessageBuilderSupport; 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.AutoFillType; +import org.citrusframework.openapi.OpenApiRepository; import org.citrusframework.openapi.OpenApiSpecification; -import org.citrusframework.spi.BindToRegistry; +import org.citrusframework.openapi.actions.OpenApiActionBuilder; +import org.citrusframework.openapi.actions.OpenApiServerRequestActionBuilder; +import org.citrusframework.openapi.actions.OpenApiServerResponseActionBuilder; +import org.citrusframework.openapi.integration.OpenApiServerIT.Config; import org.citrusframework.spi.Resources; import org.citrusframework.testng.spring.TestNGCitrusSpringSupport; import org.citrusframework.util.SocketUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; +import org.springframework.test.context.ContextConfiguration; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import static org.citrusframework.http.actions.HttpActionBuilder.http; -import static org.citrusframework.openapi.actions.OpenApiActionBuilder.openapi; - @Test +@ContextConfiguration(classes = {Config.class}) public class OpenApiServerIT extends TestNGCitrusSpringSupport { - private final int port = SocketUtils.findAvailableTcpPort(8080); + public static final String VALID_PET_PATH = "classpath:org/citrusframework/openapi/petstore/pet.json"; + public static final String INVALID_PET_PATH = "classpath:org/citrusframework/openapi/petstore/pet_invalid.json"; - @BindToRegistry - private final HttpServer httpServer = new HttpServerBuilder() - .port(port) - .timeout(5000L) - .autoStart(true) - .defaultStatus(HttpStatus.NO_CONTENT) - .build(); + @Autowired + private HttpServer httpServer; - @BindToRegistry - private final HttpClient httpClient = new HttpClientBuilder() - .requestUrl("http://localhost:%d/petstore/v3".formatted(port)) - .build(); + @Autowired + private HttpClient httpClient; private final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( - Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); + Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); @CitrusTest public void getPetById() { variable("petId", "1001"); + when(http() + .client(httpClient) + .send() + .get("/pet/${petId}") + .message() + .accept(APPLICATION_JSON_VALUE) + .fork(true)); + + then(openapi(petstoreSpec) + .server(httpServer) + .receive("getPetById") + .message() + ); + + HttpStatus httpStatus = HttpStatus.OK; + then(openapi(petstoreSpec) + .server(httpServer) + .send("getPetById", httpStatus)); + + then(http() + .client(httpClient) + .receive() + .response(httpStatus) + .message() + .body(""" + { + "id": "@isNumber()@", + "name": "@notEmpty()@", + "category": { + "id": "@isNumber()@", + "name": "@notEmpty()@" + }, + "photoUrls": "@notEmpty()@", + "tags": "@ignore@", + "status": "@matches(sold|pending|available)@" + } + """)); + } + + /** + * Sending an open api response with a status not specified in the spec should not fail. + * This is because the OpenAPI spec does not strictly require modelling of all possible + * responses. + */ + @CitrusTest + public void getPetByIdWithUnknownResponse() { + variable("petId", "1001"); + + when(http() + .client(httpClient) + .send() + .get("/pet/${petId}") + .message() + .accept(APPLICATION_JSON_VALUE) + .fork(true)); + + then(openapi(petstoreSpec) + .server(httpServer) + .receive("getPetById") + .message() + ); + + // Fake any unexpected status code, to test whether we do not run into + // validation issues. + HttpStatus httpStatus = HttpStatus.CREATED; + then(openapi(petstoreSpec) + .server(httpServer) + .send("getPetById", httpStatus)); + + then(http() + .client(httpClient) + .receive() + .response(httpStatus) + .message() + .body(""" + { + "id": "@isNumber()@", + "name": "@notEmpty()@", + "category": { + "id": "@isNumber()@", + "name": "@notEmpty()@" + }, + "photoUrls": "@notEmpty()@", + "tags": "@ignore@", + "status": "@matches(sold|pending|available)@" + } + """)); + } + + + @CitrusTest + public void getPetByIdShouldFailOnInvalidResponse() { + variable("petId", "1001"); + when(http() .client(httpClient) .send() .get("/pet/${petId}") .message() - .accept("application/json") + .accept(APPLICATION_JSON_VALUE) .fork(true)); then(openapi(petstoreSpec) .server(httpServer) .receive("getPetById")); + HttpMessageBuilderSupport getPetByIdResponseBuilder = openapi(petstoreSpec) + .server(httpServer) + .send("getPetById", HttpStatus.OK) + .message().body(""" + { + "id": "xxxx", + "name": "Garfield", + "category": { + "id": 111, + "name": "Comic" + }, + "photoUrls": [], + "tags": [], + "status": "available" + } + """); + assertThrows(TestCaseFailedException.class, () -> then(getPetByIdResponseBuilder)); + } + + @CitrusTest + public void getPetByIdShouldSucceedOnInvalidResponseWithValidationDisabled() { + variable("petId", "1001"); + + when(http() + .client(httpClient) + .send() + .get("/pet/${petId}") + .message() + .accept(APPLICATION_JSON_VALUE) + .fork(true)); + then(openapi(petstoreSpec) .server(httpServer) - .send("getPetById", HttpStatus.OK)); + .receive("getPetById")); + + HttpMessageBuilderSupport getPetByIdResponseBuilder = openapi(petstoreSpec) + .server(httpServer) + .send("getPetById", HttpStatus.OK) + .schemaValidation(false) + .message().body(""" + { + "id": "xxxx", + "name": "Garfield", + "category": { + "id": 111, + "name": "Comic" + }, + "photoUrls": [], + "tags": [], + "status": "available" + } + """); + then(getPetByIdResponseBuilder); then(http() .client(httpClient) @@ -79,44 +234,244 @@ public void getPetById() { .response(HttpStatus.OK) .message() .body(""" - { - "id": "@isNumber()@", - "name": "@notEmpty()@", - "category": { - "id": "@isNumber()@", - "name": "@notEmpty()@" - }, - "photoUrls": "@notEmpty()@", - "tags": "@ignore@", - "status": "@matches(sold|pending|available)@" - } - """)); + { + "id": "xxxx", + "name": "Garfield", + "category": { + "id": 111, + "name": "Comic" + }, + "photoUrls": [], + "tags": [], + "status": "available" + } + """)); + } + + @CitrusTest + public void shouldExecuteAddPet() { + shouldExecuteAddPet(openapi(petstoreSpec), VALID_PET_PATH, true, true); + } + + @CitrusTest + public void shouldFailOnMissingNameInRequest() { + shouldExecuteAddPet(openapi(petstoreSpec), INVALID_PET_PATH, false, true); } @CitrusTest - public void getAddPet() { + public void shouldPassOnMissingNameInRequestIfValidationIsDisabled() { + shouldExecuteAddPet(openapi(petstoreSpec), INVALID_PET_PATH, false, false); + } + + @CitrusTest + public void shouldFailOnMissingNameInResponse() { variable("petId", "1001"); when(http() .client(httpClient) .send() - .post("/pet") + .get("/pet/${petId}") .message() - .body(Resources.create("classpath:org/citrusframework/openapi/petstore/pet.json")) - .contentType("application/json") + .accept(APPLICATION_JSON_VALUE) .fork(true)); then(openapi(petstoreSpec) .server(httpServer) - .receive("addPet")); + .receive("getPetById")); + + OpenApiServerResponseActionBuilder sendMessageActionBuilder = openapi(petstoreSpec) + .server(httpServer) + .send("getPetById", HttpStatus.OK); + sendMessageActionBuilder.message().body(Resources.create(INVALID_PET_PATH)); + + assertThrows(TestCaseFailedException.class, () -> then(sendMessageActionBuilder)); + } + + @CitrusTest + public void shouldFailOnMissingPayload() { + variable("petId", "1001"); + + when(http() + .client(httpClient) + .send() + .get("/pet/${petId}") + .message() + .accept(APPLICATION_JSON_VALUE) + .fork(true)); then(openapi(petstoreSpec) + .server(httpServer) + .receive("getPetById")); + + OpenApiServerResponseActionBuilder sendMessageActionBuilder = openapi(petstoreSpec) + .server(httpServer) + .send("getPetById", HttpStatus.OK) + .autoFill(AutoFillType.NONE); + + assertThrows(TestCaseFailedException.class, () -> then(sendMessageActionBuilder)); + } + + @CitrusTest + public void shouldFailOnWrongQueryIdTypeWithOasDisabled() { + variable("petId", "xxx"); + + when(http() + .client(httpClient) + .send() + .post("/pet") + .message() + .body(Resources.create(VALID_PET_PATH)) + .contentType(APPLICATION_JSON_VALUE) + .fork(true)); + + OpenApiServerRequestActionBuilder addPetBuilder = openapi(petstoreSpec) .server(httpServer) - .send("addPet", HttpStatus.CREATED)); + .receive("addPet"); - then(http() + assertThrows(TestCaseFailedException.class, () -> then(addPetBuilder)); + } + + @CitrusTest + public void shouldSucceedOnWrongQueryIdTypeWithOasDisabled() { + variable("petId", -1); + + when(http() .client(httpClient) - .receive() - .response(HttpStatus.CREATED)); + .send() + .post("/pet") + .message() + .body(Resources.create(VALID_PET_PATH)) + .contentType(APPLICATION_JSON_VALUE) + .fork(true)); + + OpenApiServerRequestActionBuilder addPetBuilder = openapi(petstoreSpec) + .server(httpServer) + .receive("addPet") + .schemaValidation(false); + + try { + when(addPetBuilder); + } catch (Exception e) { + fail("Method threw an exception: " + e.getMessage()); + } + } + + private void shouldExecuteAddPet(OpenApiActionBuilder openapi, String requestFile, boolean valid, boolean validationEnabled) { + variable("petId", "1001"); + + when(http() + .client(httpClient) + .send() + .post("/pet") + .message() + .body(Resources.create(requestFile)) + .contentType(APPLICATION_JSON_VALUE) + .fork(true)); + + OpenApiServerRequestActionBuilder receiveActionBuilder = openapi + .server(httpServer) + .receive("addPet"); + + if (valid) { + then(receiveActionBuilder); + + then(openapi + .server(httpServer) + .send("addPet", HttpStatus.CREATED)); + + then(http() + .client(httpClient) + .receive() + .response(HttpStatus.CREATED)); + + } else { + assertThrows(() -> then(receiveActionBuilder)); + } + } + + @DataProvider + public Object[][] aliases() { + return new Object[][] { + {"petstore-v3"}, + {"Swagger Petstore"}, + {"Swagger Petstore/1.0.1"}, + }; + } + + /** + * Using OpenApiRepository, we should be able to resolve the OpenAPI spec from the name of the + * spec file or one of its aliases. + */ + @CitrusTest + @Test(dataProvider = "aliases") + public void getPetByIdUsingOpenApiRepositoryAndAlias(String alias) { + variable("petId", "1001"); + + when(http() + .client(httpClient) + .send() + .get("/pet/${petId}") + .message() + .accept(APPLICATION_JSON_VALUE) + .fork(true)); + + then(openapi(alias) + .server(httpServer) + .receive("getPetById") + .message() + ); + + HttpStatus httpStatus = HttpStatus.OK; + then(openapi(alias) + .server(httpServer) + .send("getPetById", httpStatus)); + + then(http() + .client(httpClient) + .receive() + .response(httpStatus) + .message() + .body(""" + { + "id": "@isNumber()@", + "name": "@notEmpty()@", + "category": { + "id": "@isNumber()@", + "name": "@notEmpty()@" + }, + "photoUrls": "@notEmpty()@", + "tags": "@ignore@", + "status": "@matches(sold|pending|available)@" + } + """)); + } + + @Configuration + public static class Config { + + private final int port = SocketUtils.findAvailableTcpPort(8080); + + @Bean + public HttpServer httpServer() { + return new HttpServerBuilder() + .port(port) + .timeout(5000L) + .autoStart(true) + .defaultStatus(HttpStatus.NO_CONTENT) + .build(); + } + + @Bean + public HttpClient httpClient() { + return new HttpClientBuilder() + .requestUrl("http://localhost:%d/petstore/v3".formatted(port)) + .build(); + } + + @Bean + public OpenApiRepository petstoreOpenApiRepository() { + return new OpenApiRepository() + .locations(singletonList("classpath:org/citrusframework/openapi/petstore/petstore-v3.json")); + } } } diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/OasModelHelperTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/OasModelHelperTest.java new file mode 100644 index 0000000000..8ca57aaf97 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/OasModelHelperTest.java @@ -0,0 +1,63 @@ +package org.citrusframework.openapi.model; + +import java.util.List; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class OasModelHelperTest { + + @Test + public void testToAcceptedRandomizableMediaTypes_WithValidAcceptHeader() { + // Given + String accept = "application/json, text/plain, application/xml, application/json;charset=UTF-8"; + + // When + List result = OasModelHelper.toAcceptedRandomizableMediaTypes(accept); + + // Then + Assert.assertNotNull(result, "Result should not be null"); + Assert.assertTrue(result.contains("application/json"), "Result should contain 'application/json'"); + Assert.assertTrue(result.contains("application/json;charset=UTF-8"), "Result should contain 'text/plain'"); + Assert.assertTrue(result.contains("text/plain"), "Result should contain 'text/plain'"); + Assert.assertFalse(result.contains("application/xml"), "Result should not contain 'application/xml'"); + } + + @Test + public void testToAcceptedRandomizableMediaTypes_WithEmptyAcceptHeader() { + // Given + String accept = ""; + + // When + List result = OasModelHelper.toAcceptedRandomizableMediaTypes(accept); + + // Then + Assert.assertNotNull(result, "Result should not be null"); + Assert.assertEquals(result, OasModelHelper.DEFAULT_ACCEPTED_MEDIA_TYPES, "Should return the default media types"); + } + + @Test + public void testToAcceptedRandomizableMediaTypes_WithNullAcceptHeader() { + // Given + String accept = null; + + // When + List result = OasModelHelper.toAcceptedRandomizableMediaTypes(accept); + + // Then + Assert.assertNotNull(result, "Result should not be null"); + Assert.assertEquals(result, OasModelHelper.DEFAULT_ACCEPTED_MEDIA_TYPES, "Should return the default media types"); + } + + @Test + public void testToAcceptedRandomizableMediaTypes_WithUnrelatedMediaTypes() { + // Given + String accept = "image/png, application/xml"; + + // When + List result = OasModelHelper.toAcceptedRandomizableMediaTypes(accept); + + // Then + Assert.assertNotNull(result, "Result should not be null"); + Assert.assertTrue(result.isEmpty(), "Result should be empty for unrelated media types"); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/OperationPathAdapterTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/OperationPathAdapterTest.java new file mode 100644 index 0000000000..fd7322ff44 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/OperationPathAdapterTest.java @@ -0,0 +1,42 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.model; + +import io.apicurio.datamodels.openapi.v3.models.Oas30Operation; +import org.citrusframework.openapi.util.OpenApiUtils; +import org.testng.annotations.Test; + +import static java.lang.String.format; +import static org.testng.Assert.assertEquals; + +public class OperationPathAdapterTest { + + @Test + public void shouldReturnFormattedStringWhenToStringIsCalled() { + // Given + Oas30Operation oas30Operation = new Oas30Operation("get"); + oas30Operation.operationId = "operationId"; + + OperationPathAdapter adapter = new OperationPathAdapter("/api/path", "/context/path", "/full/path", oas30Operation, oas30Operation.operationId); + + // When + String expectedString = format("%s (%s)", OpenApiUtils.getMethodPath("GET", "/api/path"), "operationId"); + + // Then + assertEquals(adapter.toString(), expectedString); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/v2/Oas20ModelHelperTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/v2/Oas20ModelHelperTest.java new file mode 100644 index 0000000000..cfa0b69440 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/v2/Oas20ModelHelperTest.java @@ -0,0 +1,145 @@ +package org.citrusframework.openapi.model.v2; + +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.Oas20Items; +import io.apicurio.datamodels.openapi.v2.models.Oas20Operation; +import io.apicurio.datamodels.openapi.v2.models.Oas20Parameter; +import io.apicurio.datamodels.openapi.v2.models.Oas20Response; +import io.apicurio.datamodels.openapi.v2.models.Oas20Responses; +import io.apicurio.datamodels.openapi.v2.models.Oas20Schema; +import org.citrusframework.openapi.model.OasModelHelper; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Optional; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class Oas20ModelHelperTest { + + @Test + public void shouldFindRandomResponseWithGoodStatusCode() { + Oas20Document document = new Oas20Document(); + Oas20Operation operation = new Oas20Operation("GET"); + + operation.responses = new Oas20Responses(); + + Oas20Response nokResponse = new Oas20Response("403"); + nokResponse.schema = new Oas20Schema(); + + Oas20Response okResponse = new Oas20Response("200"); + okResponse.schema = new Oas20Schema(); + + operation.responses = new Oas20Responses(); + operation.responses.addResponse("403", nokResponse); + operation.responses.addResponse("200", okResponse); + + Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration( + document, operation, null, null); + assertTrue(responseForRandomGeneration.isPresent()); + assertEquals(okResponse, responseForRandomGeneration.get()); + } + + @Test + public void shouldFindFirstResponseInAbsenceOfAGoodOne() { + Oas20Document document = new Oas20Document(); + Oas20Operation operation = new Oas20Operation("GET"); + + operation.responses = new Oas20Responses(); + + Oas20Response nokResponse403 = new Oas20Response("403"); + nokResponse403.schema = new Oas20Schema(); + Oas20Response nokResponse407 = new Oas20Response("407"); + nokResponse407.schema = new Oas20Schema(); + + operation.responses = new Oas20Responses(); + operation.responses.addResponse("403", nokResponse403); + operation.responses.addResponse("407", nokResponse407); + + Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration( + document, operation, null, null); + assertTrue(responseForRandomGeneration.isPresent()); + assertEquals(responseForRandomGeneration.get().getStatusCode(), "403"); + } + + @Test + public void shouldFindDefaultResponseInAbsenceOfAGoodOne() { + Oas20Document document = new Oas20Document(); + Oas20Operation operation = new Oas20Operation("GET"); + + operation.responses = new Oas20Responses(); + + Oas20Response nokResponse403 = new Oas20Response("403"); + nokResponse403.schema = new Oas20Schema(); + Oas20Response nokResponse407 = new Oas20Response("407"); + nokResponse407.schema = new Oas20Schema(); + + operation.responses = new Oas20Responses(); + operation.responses.default_ = nokResponse407; + operation.responses.addResponse("403", nokResponse403); + operation.responses.addResponse("407", nokResponse407); + + Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration( + document, operation, null, null); + assertTrue(responseForRandomGeneration.isPresent()); + assertEquals(responseForRandomGeneration.get().getStatusCode(), "407"); + } + + @Test + public void shouldFindParameterSchema() { + Oas20Parameter parameter = new Oas20Parameter(); + parameter.schema = new Oas20Schema(); + + Optional parameterSchema = Oas20ModelHelper.getParameterSchema(parameter); + assertTrue(parameterSchema.isPresent()); + assertEquals(parameter.schema, parameterSchema.get()); + } + + @Test + public void shouldFindSchemaFromParameter() { + Oas20Parameter parameter = new Oas20Parameter("testParameter"); + parameter.type = "string"; + parameter.format = "date-time"; + parameter.items = new Oas20Items(); + parameter.multipleOf = 2; + parameter.default_ = "defaultValue"; + parameter.enum_ = List.of("value1", "value2"); + parameter.pattern = "pattern"; + parameter.description = "description"; + parameter.uniqueItems = true; + parameter.maximum = 100.0; + parameter.maxItems = 10; + parameter.maxLength = 20; + parameter.exclusiveMaximum = true; + parameter.minimum = 0.0; + parameter.minItems = 1; + parameter.minLength = 5; + parameter.exclusiveMinimum = false; + + Optional schemaOptional = Oas20ModelHelper.getParameterSchema(parameter); + assertTrue(schemaOptional.isPresent()); + + OasSchema parameterSchema = schemaOptional.get(); + assertEquals(parameterSchema.title, "testParameter"); + assertEquals(parameterSchema.type, "string"); + assertEquals(parameterSchema.format, "date-time"); + assertEquals(parameter.items, parameterSchema.items); + assertEquals(parameter.multipleOf, parameterSchema.multipleOf); + assertEquals(parameter.default_, parameterSchema.default_); + assertEquals(parameter.enum_, parameterSchema.enum_); + assertEquals(parameter.pattern, parameterSchema.pattern); + assertEquals(parameter.description, parameterSchema.description); + assertEquals(parameter.uniqueItems, parameterSchema.uniqueItems); + assertEquals(parameter.maximum, parameterSchema.maximum); + assertEquals(parameter.maxItems, parameterSchema.maxItems); + assertEquals(parameter.maxLength, parameterSchema.maxLength); + assertEquals(parameter.exclusiveMaximum, parameterSchema.exclusiveMaximum); + assertEquals(parameter.minimum, parameterSchema.minimum); + assertEquals(parameter.minItems, parameterSchema.minItems); + assertEquals(parameter.minLength, parameterSchema.minLength); + assertEquals(parameter.exclusiveMinimum, parameterSchema.exclusiveMinimum); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/v3/Oas30ModelHelperTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/v3/Oas30ModelHelperTest.java index 38d8b13fde..dd00a89dfc 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/v3/Oas30ModelHelperTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/model/v3/Oas30ModelHelperTest.java @@ -1,13 +1,26 @@ package org.citrusframework.openapi.model.v3; +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.Oas30Header; +import io.apicurio.datamodels.openapi.v3.models.Oas30MediaType; +import io.apicurio.datamodels.openapi.v3.models.Oas30Operation; +import io.apicurio.datamodels.openapi.v3.models.Oas30Parameter; import io.apicurio.datamodels.openapi.v3.models.Oas30Response; +import io.apicurio.datamodels.openapi.v3.models.Oas30Responses; import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; -import org.testng.Assert; +import org.citrusframework.openapi.model.OasModelHelper; +import org.springframework.http.MediaType; import org.testng.annotations.Test; +import java.util.Collection; import java.util.Map; +import java.util.Optional; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; public class Oas30ModelHelperTest { @@ -15,40 +28,159 @@ public class Oas30ModelHelperTest { public void shouldNotFindRequiredHeadersWithoutRequiredAttribute() { var header = new Oas30Header("X-TEST"); header.schema = new Oas30Schema(); - header.required = null; // explicitely assigned because this is test case + header.required = null; var response = new Oas30Response("200"); response.headers.put(header.getName(), header); Map result = Oas30ModelHelper.getRequiredHeaders(response); - Assert.assertEquals(result.size(), 0); + assertEquals(result.size(), 0); } @Test public void shouldFindRequiredHeaders() { var header = new Oas30Header("X-TEST"); header.schema = new Oas30Schema(); - header.required = Boolean.TRUE; // explicitely assigned because this is test case + header.required = Boolean.TRUE; var response = new Oas30Response("200"); response.headers.put(header.getName(), header); Map result = Oas30ModelHelper.getRequiredHeaders(response); - Assert.assertEquals(result.size(), 1); - Assert.assertSame(result.get(header.getName()), header.schema); + assertEquals(result.size(), 1); + assertSame(result.get(header.getName()), header.schema); } @Test public void shouldNotFindOptionalHeaders() { var header = new Oas30Header("X-TEST"); header.schema = new Oas30Schema(); - header.required = Boolean.FALSE; // explicitely assigned because this is test case + header.required = Boolean.FALSE; var response = new Oas30Response("200"); response.headers.put(header.getName(), header); Map result = Oas30ModelHelper.getRequiredHeaders(response); - Assert.assertEquals(result.size(), 0); + assertEquals(result.size(), 0); } -} \ No newline at end of file + @Test + public void shouldFindAllRequestTypesForOperation() { + Oas30Operation operation = new Oas30Operation("GET"); + operation.responses = new Oas30Responses(); + + Oas30Response response = new Oas30Response("200"); + response.content = Map.of(MediaType.APPLICATION_JSON_VALUE, + new Oas30MediaType(MediaType.APPLICATION_JSON_VALUE), + MediaType.APPLICATION_XML_VALUE, new Oas30MediaType(MediaType.APPLICATION_XML_VALUE)); + + operation.responses = new Oas30Responses(); + operation.responses.addResponse("200", response); + + Collection responseTypes = Oas30ModelHelper.getResponseTypes(operation, response); + + assertTrue(responseTypes.contains(MediaType.APPLICATION_JSON_VALUE)); + assertTrue(responseTypes.contains(MediaType.APPLICATION_XML_VALUE)); + } + + @Test + public void shouldFindRandomResponseWithGoodStatusCode() { + Oas30Document document = new Oas30Document(); + Oas30Operation operation = new Oas30Operation("GET"); + + operation.responses = new Oas30Responses(); + + Oas30Response nokResponse = new Oas30Response("403"); + Oas30MediaType plainTextMediaType = new Oas30MediaType(MediaType.TEXT_PLAIN_VALUE); + plainTextMediaType.schema = new Oas30Schema(); + nokResponse.content = Map.of(MediaType.TEXT_PLAIN_VALUE, plainTextMediaType); + + Oas30Response okResponse = new Oas30Response("200"); + Oas30MediaType jsonMediaType = new Oas30MediaType(MediaType.APPLICATION_JSON_VALUE); + jsonMediaType.schema = new Oas30Schema(); + + Oas30MediaType xmlMediaType = new Oas30MediaType(MediaType.APPLICATION_XML_VALUE); + xmlMediaType.schema = new Oas30Schema(); + + okResponse.content = Map.of(MediaType.APPLICATION_JSON_VALUE, jsonMediaType, + MediaType.APPLICATION_XML_VALUE, xmlMediaType); + + operation.responses = new Oas30Responses(); + operation.responses.addResponse("403", nokResponse); + operation.responses.addResponse("200", okResponse); + + Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration( + document, operation, null, null); + assertTrue(responseForRandomGeneration.isPresent()); + assertEquals(okResponse, responseForRandomGeneration.get()); + } + + @Test + public void shouldFindFirstResponseInAbsenceOfAGoodOne() { + Oas30Document document = new Oas30Document(); + Oas30Operation operation = new Oas30Operation("GET"); + + operation.responses = new Oas30Responses(); + + Oas30Response nokResponse403 = new Oas30Response("403"); + Oas30MediaType plainTextMediaType = new Oas30MediaType(MediaType.TEXT_PLAIN_VALUE); + plainTextMediaType.schema = new Oas30Schema(); + nokResponse403.content = Map.of(MediaType.TEXT_PLAIN_VALUE, plainTextMediaType); + + Oas30Response nokResponse407 = new Oas30Response("407"); + nokResponse407.content = Map.of(MediaType.TEXT_PLAIN_VALUE, plainTextMediaType); + + operation.responses = new Oas30Responses(); + operation.responses.addResponse("403", nokResponse403); + operation.responses.addResponse("407", nokResponse407); + + Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration( + document, operation, null, null); + assertTrue(responseForRandomGeneration.isPresent()); + assertEquals(responseForRandomGeneration.get().getStatusCode(), "403"); + } + + @Test + public void shouldFindDefaultResponseInAbsenceOfAGoodOne() { + Oas30Document document = new Oas30Document(); + Oas30Operation operation = new Oas30Operation("GET"); + + operation.responses = new Oas30Responses(); + + Oas30Response nokResponse403 = new Oas30Response("403"); + Oas30MediaType plainTextMediaType = new Oas30MediaType(MediaType.TEXT_PLAIN_VALUE); + plainTextMediaType.schema = new Oas30Schema(); + nokResponse403.content = Map.of(MediaType.TEXT_PLAIN_VALUE, plainTextMediaType); + + Oas30Response nokResponse407 = new Oas30Response("407"); + nokResponse407.content = Map.of(MediaType.TEXT_PLAIN_VALUE, plainTextMediaType); + + operation.responses = new Oas30Responses(); + operation.responses.default_ = nokResponse407; + operation.responses.addResponse("403", nokResponse403); + operation.responses.addResponse("407", nokResponse407); + + Optional responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration( + document, operation, null, null); + assertTrue(responseForRandomGeneration.isPresent()); + assertEquals(responseForRandomGeneration.get().getStatusCode(), "407"); + } + + @Test + public void shouldFindParameterSchema() { + Oas30Parameter parameter = new Oas30Parameter(); + parameter.schema = new Oas30Schema(); + + Optional parameterSchema = Oas30ModelHelper.getParameterSchema(parameter); + assertTrue(parameterSchema.isPresent()); + assertEquals(parameter.schema, parameterSchema.get()); + } + + @Test + public void shouldNotFindParameterSchema() { + Oas30Parameter parameter = new Oas30Parameter(); + + Optional parameterSchema = Oas30ModelHelper.getParameterSchema(parameter); + assertTrue(parameterSchema.isEmpty()); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/OasRandomConfigurationTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/OasRandomConfigurationTest.java new file mode 100644 index 0000000000..6af97c566f --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/OasRandomConfigurationTest.java @@ -0,0 +1,183 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.List; + +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_DATE; +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_DATE_TIME; +import static org.citrusframework.openapi.OpenApiConstants.FORMAT_UUID; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_BOOLEAN; +import static org.citrusframework.openapi.OpenApiConstants.TYPE_STRING; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; + +public class OasRandomConfigurationTest { + + private RandomConfiguration randomConfiguration; + + @BeforeClass + public void setUp() { + randomConfiguration = RandomConfiguration.RANDOM_CONFIGURATION; + } + + @Test + public void testGetGeneratorForDateFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = FORMAT_DATE; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForDateTimeFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = FORMAT_DATE_TIME; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForUUIDFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = FORMAT_UUID; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForEmailFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = "email"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForURIFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = "uri"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForHostnameFormat() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = "hostname"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForIPv4Format() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = "ipv4"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForIPv6Format() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + schema.format = "ipv6"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForBooleanType() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_BOOLEAN; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForStringType() { + OasSchema schema = new Oas30Schema(); + schema.type = TYPE_STRING; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForNumberType() { + OasSchema schema = new Oas30Schema(); + schema.type = "number"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForObjectType() { + OasSchema schema = new Oas30Schema(); + schema.type = "object"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForArrayType() { + OasSchema schema = new Oas30Schema(); + schema.type = "array"; + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForEnum() { + OasSchema schema = new Oas30Schema(); + schema.enum_ = List.of("value1", "value2"); + + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertTrue(generator.handles(schema)); + } + + @Test + public void testGetGeneratorForNullSchema() { + OasSchema schema = new Oas30Schema(); + RandomGenerator generator = randomConfiguration.getGenerator(schema); + assertNotNull(generator); + assertSame(generator, RandomGenerator.NOOP_RANDOM_GENERATOR); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomArrayGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomArrayGeneratorTest.java new file mode 100644 index 0000000000..da923caa77 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomArrayGeneratorTest.java @@ -0,0 +1,106 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.citrusframework.openapi.OpenApiConstants; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class RandomArrayGeneratorTest { + + private RandomArrayGenerator generator; + private RandomContext mockContext; + private RandomModelBuilder builderSpy; + + @BeforeMethod + public void setUp() { + generator = new RandomArrayGenerator(); + mockContext = mock(); + + builderSpy = spy(new RandomModelBuilder(true)); + + when(mockContext.getRandomModelBuilder()).thenReturn(builderSpy); + } + + @Test + public void testGenerateArrayWithDefaultItems() { + Oas30Schema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_ARRAY; + + Oas30Schema itemsSchema = new Oas30Schema(); + itemsSchema.type = OpenApiConstants.TYPE_STRING; + schema.items = itemsSchema; + + generator.generate(mockContext, schema); + + verify(builderSpy, atLeastOnce()).array(any()); + verify(mockContext, atLeastOnce()).generate(any(OasSchema.class)); + } + + @Test + public void testGenerateArrayWithMinItems() { + Oas30Schema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_ARRAY; + schema.minItems = 5; + + Oas30Schema itemsSchema = new Oas30Schema(); + itemsSchema.type = OpenApiConstants.TYPE_STRING; + schema.items = itemsSchema; + + generator.generate(mockContext, schema); + + verify(builderSpy, atLeastOnce()).array(any()); + verify(mockContext, atLeast(5)).generate(any(OasSchema.class)); + } + + @Test + public void testGenerateArrayWithMaxItems() { + Oas30Schema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_ARRAY; + schema.maxItems = 3; + + Oas30Schema itemsSchema = new Oas30Schema(); + itemsSchema.type = OpenApiConstants.TYPE_STRING; + schema.items = itemsSchema; + + generator.generate(mockContext, schema); + + verify(builderSpy, atLeastOnce()).array(any()); + verify(mockContext, atMost(3)).generate(any(OasSchema.class)); + } + + @Test + public void testGenerateArrayWithMinMaxItems() { + Oas30Schema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_ARRAY; + schema.minItems = 2; + schema.maxItems = 5; + + Oas30Schema itemsSchema = new Oas30Schema(); + itemsSchema.type = OpenApiConstants.TYPE_STRING; + schema.items = itemsSchema; + + generator.generate(mockContext, schema); + + verify(builderSpy, atLeastOnce()).array(any()); + verify(mockContext, atLeast(2)).generate(any(OasSchema.class)); + verify(mockContext, atMost(5)).generate(any(OasSchema.class)); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void testGenerateArrayWithUnsupportedItems() { + Oas30Schema schema = new Oas30Schema(); + schema.items = new Object(); // Unsupported items type + + generator.generate(mockContext, schema); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomCompositeGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomCompositeGeneratorTest.java new file mode 100644 index 0000000000..818f6488ae --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomCompositeGeneratorTest.java @@ -0,0 +1,91 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.assertArg; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class RandomCompositeGeneratorTest { + + private RandomCompositeGenerator generator; + private RandomContext mockContext; + private RandomModelBuilder builderSpy; + + @BeforeMethod + public void setUp() { + generator = new RandomCompositeGenerator(); + mockContext = mock(RandomContext.class); + builderSpy = spy(new RandomModelBuilder(true)); + + when(mockContext.getRandomModelBuilder()).thenReturn(builderSpy); + } + + @Test + public void testHandlesCompositeSchema() { + Oas30Schema schema = new Oas30Schema(); + schema.allOf = Collections.singletonList(new Oas30Schema()); + + assertThat(generator.handles(schema)).isTrue(); + } + + @Test + public void testGenerateAllOf() { + Oas30Schema schema = new Oas30Schema(); + schema.allOf = List.of(new Oas30Schema(), new Oas30Schema(), new Oas30Schema()); + + generator.generate(mockContext, schema); + + verify(builderSpy).object(any()); + verify(mockContext).generate(schema.allOf.get(0)); + verify(mockContext).generate(schema.allOf.get(1)); + verify(mockContext).generate(schema.allOf.get(2)); + } + + @Test + public void testGenerateAnyOf() { + Oas30Schema schema = new Oas30Schema(); + schema.anyOf = List.of(new Oas30Schema(), new Oas30Schema(), new Oas30Schema()); + + generator.generate(mockContext, schema); + + verify(builderSpy, atMost(2)).object(any()); + verify(mockContext, atLeast(1)).generate(assertArg(arg -> assertThat(schema.anyOf).contains(arg))); + verify(mockContext, atMost(3)).generate(assertArg(arg -> assertThat(schema.anyOf).contains(arg))); + } + + @Test + public void testGenerateOneOf() { + Oas30Schema schema = new Oas30Schema(); + schema.oneOf = List.of(new Oas30Schema(), new Oas30Schema(), new Oas30Schema()); + + generator.generate(mockContext, schema); + + verify(builderSpy, atLeastOnce()).object(any()); + verify(mockContext).generate(any(OasSchema.class)); + } + + @Test + public void testGenerateWithNoCompositeSchema() { + Oas30Schema schema = new Oas30Schema(); + + generator.generate(mockContext, schema); + + verify(builderSpy, never()).object(any()); + verify(mockContext, never()).generate(any(OasSchema.class)); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomContextTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomContextTest.java new file mode 100644 index 0000000000..9e9ac1ae60 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomContextTest.java @@ -0,0 +1,77 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.citrusframework.openapi.OpenApiSpecification; +import org.springframework.test.util.ReflectionTestUtils; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertSame; + +public class RandomContextTest { + + private OpenApiSpecification specificationMock; + + private RandomContext randomContext; + + private Map schemaDefinitions; + + @BeforeMethod + public void setUp() { + RandomModelBuilder randomModelBuilderMock = mock(); + specificationMock = mock(); + + schemaDefinitions = new HashMap<>(); + + randomContext = spy(new RandomContext(specificationMock, true)); + ReflectionTestUtils.setField(randomContext, "randomModelBuilder", randomModelBuilderMock); + + doReturn(schemaDefinitions).when(randomContext).getSchemaDefinitions(); + } + + @Test + public void testGenerateWithResolvedSchema() { + OasSchema oasSchema = new Oas30Schema(); + randomContext.generate(oasSchema); + verify(randomContext).doGenerate(oasSchema); + } + + @Test + public void testGenerateWithReferencedSchema() { + OasSchema referencedSchema = new Oas30Schema(); + schemaDefinitions.put("reference", referencedSchema); + OasSchema oasSchema = new Oas30Schema(); + oasSchema.$ref = "reference"; + + randomContext.generate(oasSchema); + verify(randomContext).doGenerate(referencedSchema); + } + + @Test + public void testGetRandomModelBuilder() { + assertNotNull(randomContext.getRandomModelBuilder()); + } + + @Test + public void testGetSpecification() { + assertEquals(randomContext.getSpecification(), specificationMock); + } + + @Test + public void testCacheVariable() { + HashMap cachedValue1 = randomContext.get("testKey", k -> new HashMap<>()); + HashMap cachedValue2 = randomContext.get("testKey", k -> new HashMap<>()); + + assertSame(cachedValue1, cachedValue2); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomElementTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomElementTest.java new file mode 100644 index 0000000000..8a5c0bd700 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomElementTest.java @@ -0,0 +1,89 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.random; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +public class RandomElementTest { + + private RandomElement.RandomList randomList; + private RandomElement.RandomObject randomObject; + private RandomElement.RandomValue randomValue; + + @BeforeMethod + public void setUp() { + randomList = new RandomElement.RandomList(); + randomObject = new RandomElement.RandomObject(); + randomValue = new RandomElement.RandomValue(); + } + + @Test + public void testRandomListPushValue() { + randomList.push("testValue"); + assertEquals(randomList.size(), 1); + assertEquals(randomList.get(0), "testValue"); + } + + @Test + public void testRandomListPushKeyValue() { + randomList.push(new RandomElement.RandomObject()); + randomList.push("key", "value"); + assertEquals(((RandomElement.RandomObject) randomList.get(0)).get("key"), "value"); + } + + @Test + public void testRandomObjectPushKeyValue() { + randomObject.push("key", "value"); + assertEquals(randomObject.get("key"), "value"); + } + + @Test + public void testRandomObjectPushRandomObject() { + RandomElement.RandomObject nestedObject = new RandomElement.RandomObject(); + nestedObject.push("nestedKey", "nestedValue"); + randomObject.push(nestedObject); + assertEquals(randomObject.size(), 1); + assertEquals(randomObject.get("nestedKey"), "nestedValue"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testRandomObjectPushValueThrowsException() { + randomObject.push("value"); + } + + @Test + public void testRandomValuePushValue() { + randomValue.push("testValue"); + assertEquals(randomValue.getValue(), "testValue"); + } + + @Test + public void testRandomValuePushRandomElement() { + RandomElement.RandomObject nestedObject = new RandomElement.RandomObject(); + randomValue = new RandomElement.RandomValue(nestedObject); + randomValue.push("key", "value"); + assertEquals(((RandomElement.RandomObject) randomValue.getValue()).get("key"), "value"); + } + + @Test(expectedExceptions = IllegalStateException.class) + public void testRandomValuePushKeyValueThrowsException() { + randomValue.push("key", "value"); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomEnumGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomEnumGeneratorTest.java new file mode 100644 index 0000000000..3aaa86f541 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomEnumGeneratorTest.java @@ -0,0 +1,79 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.List; + +import static java.util.Collections.emptyList; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class RandomEnumGeneratorTest { + + private RandomEnumGenerator generator; + private RandomContext mockContext; + private RandomModelBuilder mockBuilder; + private OasSchema mockSchema; + + @BeforeMethod + public void setUp() { + generator = new RandomEnumGenerator(); + mockContext = mock(RandomContext.class); + mockBuilder = mock(RandomModelBuilder.class); + mockSchema = mock(OasSchema.class); + + when(mockContext.getRandomModelBuilder()).thenReturn(mockBuilder); + } + + @Test + public void testHandlesWithEnum() { + mockSchema.enum_ = List.of("value1", "value2", "value3"); + + boolean result = generator.handles(mockSchema); + + assertTrue(result); + } + + @Test + public void testHandlesWithoutEnum() { + mockSchema.enum_ = null; + + boolean result = generator.handles(mockSchema); + + assertFalse(result); + } + + @Test + public void testGenerateWithEnum() { + mockSchema.enum_ = List.of("value1", "value2", "value3"); + + generator.generate(mockContext, mockSchema); + + verify(mockBuilder).appendSimpleQuoted("citrus:randomEnumValue('value1','value2','value3')"); + } + + @Test + public void testGenerateWithEmptyEnum() { + mockSchema.enum_ = emptyList(); + + generator.generate(mockContext, mockSchema); + + verify(mockBuilder).appendSimpleQuoted("citrus:randomEnumValue()"); + } + + @Test + public void testGenerateWithNullEnum() { + mockSchema.enum_ = null; + + generator.generate(mockContext, mockSchema); + + verify(mockBuilder, never()).appendSimpleQuoted(anyString()); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorBuilderTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorBuilderTest.java new file mode 100644 index 0000000000..a5d442a73d --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorBuilderTest.java @@ -0,0 +1,98 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import org.springframework.test.util.ReflectionTestUtils; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.function.BiConsumer; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +public class RandomGeneratorBuilderTest { + + private BiConsumer consumerMock; + private RandomContext contextMock; + private OasSchema schemaMock; + + @BeforeMethod + public void setUp() { + consumerMock = mock(); + contextMock = mock(); + schemaMock = mock(); + } + + @Test + public void testRandomGeneratorBuilderWithTypeAndFormat() { + String type = "type1"; + String format = "format1"; + + RandomGenerator generator = RandomGeneratorBuilder.randomGeneratorBuilder(type, format) + .build(consumerMock); + OasSchema schema = (OasSchema) ReflectionTestUtils.getField(generator, "schema"); + assertNotNull(schema); + assertEquals(schema.type, type); + assertEquals(schema.format, format); + } + + @Test + public void testRandomGeneratorBuilderWithType() { + String type = "type1"; + + RandomGenerator generator = RandomGeneratorBuilder.randomGeneratorBuilder() + .withType(type) + .build(consumerMock); + OasSchema schema = (OasSchema) ReflectionTestUtils.getField(generator, "schema"); + assertNotNull(schema); + assertEquals(schema.type, type); + } + + @Test + public void testRandomGeneratorBuilderWithFormat() { + String format = "format1"; + + RandomGenerator generator = RandomGeneratorBuilder.randomGeneratorBuilder() + .withFormat(format) + .build(consumerMock); + OasSchema schema = (OasSchema) ReflectionTestUtils.getField(generator, "schema"); + assertNotNull(schema); + assertEquals(schema.format, format); + } + + @Test + public void testRandomGeneratorBuilderWithPattern() { + String pattern = "pattern1"; + + RandomGenerator generator = RandomGeneratorBuilder.randomGeneratorBuilder() + .withPattern(pattern) + .build(consumerMock); + OasSchema schema = (OasSchema) ReflectionTestUtils.getField(generator, "schema"); + assertNotNull(schema); + assertEquals(schema.pattern, pattern); + } + + @Test + public void testRandomGeneratorBuilderWithEnum() { + RandomGenerator generator = RandomGeneratorBuilder.randomGeneratorBuilder() + .withEnum() + .build(consumerMock); + OasSchema schema = (OasSchema) ReflectionTestUtils.getField(generator, "schema"); + assertNotNull(schema); + assertNotNull(schema.enum_); + assertTrue(schema.enum_.isEmpty()); + } + + @Test + public void testBuildGenerator() { + RandomGenerator generator = RandomGeneratorBuilder.randomGeneratorBuilder() + .build(consumerMock); + + generator.generate(contextMock, schemaMock); + + verify(consumerMock).accept(contextMock, schemaMock); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorTest.java new file mode 100644 index 0000000000..6825b963b6 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomGeneratorTest.java @@ -0,0 +1,153 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class RandomGeneratorTest { + + private RandomGenerator generator; + private OasSchema mockSchema; + + @BeforeMethod + public void setUp() { + mockSchema = mock(OasSchema.class); + generator = new RandomGenerator(mockSchema) { + @Override + void generate(RandomContext randomContext, OasSchema schema) { + // Implementation not needed for this test + } + }; + } + + @Test + public void testHandlesWithMatchingTypeAndFormat() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.format = "format1"; + + mockSchema.type = "type1"; + mockSchema.format = "format1"; + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithTypeAny() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.format = "format1"; + + mockSchema.type = RandomGenerator.ANY; + mockSchema.format = "format1"; + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithFormatAny() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.format = "format1"; + + mockSchema.type = "type1"; + mockSchema.format = RandomGenerator.ANY; + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithPatternAny() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.pattern = "pattern1"; + + mockSchema.type = "type1"; + mockSchema.pattern = RandomGenerator.ANY; + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithMatchingPattern() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.pattern = "pattern1"; + + mockSchema.type = "type1"; + mockSchema.pattern = "pattern1"; + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithMatchingEnum() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.enum_ = List.of("value1", "value2"); + + mockSchema.type = "type1"; + mockSchema.enum_ = List.of("value1", "value2"); + + assertTrue(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithNonMatchingType() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type2"; + otherSchema.format = "format1"; + + mockSchema.type = "type1"; + mockSchema.format = "format1"; + + assertFalse(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithNonMatchingFormat() { + OasSchema otherSchema = new Oas30Schema(); + otherSchema.type = "type1"; + otherSchema.format = "format2"; + + mockSchema.type = "type1"; + mockSchema.format = "format1"; + + assertFalse(generator.handles(otherSchema)); + } + + @Test + public void testHandlesWithNullSchema() { + assertFalse(generator.handles(null)); + } + + @Test + public void testHandlesWithNullGeneratorSchema() { + RandomGenerator generatorWithNullSchema = new RandomGenerator() { + @Override + void generate(RandomContext randomContext, OasSchema schema) { + // Do nothing + } + }; + + assertFalse(generatorWithNullSchema.handles(mockSchema)); + } + + @Test + public void testNullGenerator() { + RandomContext mockContext = mock(RandomContext.class); + + RandomGenerator.NOOP_RANDOM_GENERATOR.generate(mockContext, mockSchema); + + verify(mockContext, never()).getRandomModelBuilder(); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomModelBuilderTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomModelBuilderTest.java new file mode 100644 index 0000000000..223e67cc52 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomModelBuilderTest.java @@ -0,0 +1,155 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.random; + +import org.springframework.test.util.ReflectionTestUtils; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.ArrayDeque; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.expectThrows; + +public class RandomModelBuilderTest { + + private RandomModelBuilder builder; + + @BeforeMethod + public void setUp() { + builder = new RandomModelBuilder(true); + } + + @Test + public void testInitialState() { + String text = builder.write(); + assertEquals(text, ""); + } + + @Test + public void testAppendSimple() { + builder.appendSimple("testValue"); + String json = builder.write(); + assertEquals(json, "testValue"); + } + + @Test + public void testAppendSimpleToEmptyQueue() { + ReflectionTestUtils.setField(builder, "deque", new ArrayDeque<>()); + builder.appendSimple("testValue"); + String json = builder.write(); + assertEquals(json, "testValue"); + } + + @Test + public void testAppendSimpleQuoted() { + builder.appendSimpleQuoted("testValue"); + String json = builder.write(); + assertEquals(json, "\"testValue\""); + } + + @Test + public void testAppendSimpleQuotedIfNotQuoting() { + ReflectionTestUtils.setField(builder, "quote", false); + builder.appendSimpleQuoted("testValue"); + String json = builder.write(); + assertEquals(json, "testValue"); + } + + @Test + public void testObjectWithProperties() { + builder.object(() -> { + builder.property("key1", () -> builder.appendSimple("\"value1\"")); + builder.property("key2", () -> builder.appendSimple("\"value2\"")); + }); + String json = builder.write(); + assertEquals(json, "{\"key1\": \"value1\",\"key2\": \"value2\"}"); + } + + @Test + public void testNestedObject() { + builder.object(() -> + builder.property("outerKey", () -> builder.object(() -> + builder.property("innerKey", () -> builder.appendSimple("\"innerValue\"")) + )) + ); + String json = builder.write(); + assertEquals(json, "{\"outerKey\": {\"innerKey\": \"innerValue\"}}"); + } + + @Test + public void testArray() { + builder.array(() -> { + builder.appendSimple("\"value1\""); + builder.appendSimple("\"value2\""); + builder.appendSimple("\"value3\""); + }); + String json = builder.write(); + assertEquals(json, "[\"value1\",\"value2\",\"value3\"]"); + } + + @Test + public void testNestedArray() { + builder.array(() -> { + builder.appendSimple("\"value1\""); + builder.array(() -> { + builder.appendSimple("\"nestedValue1\""); + builder.appendSimple("\"nestedValue2\""); + }); + builder.appendSimple("\"value2\""); + }); + String json = builder.write(); + assertEquals(json, "[\"value1\",[\"nestedValue1\",\"nestedValue2\"],\"value2\"]"); + } + + @Test + public void testMixedStructure() { + builder.object(() -> { + builder.property("key1", () -> builder.array(() -> { + builder.appendSimple("\"value1\""); + builder.object(() -> + builder.property("nestedKey", () -> builder.appendSimple("\"nestedValue\"")) + ); + })); + builder.property("key2", () -> builder.appendSimple("\"value2\"")); + }); + String json = builder.write(); + assertEquals(json, "{\"key1\": [\"value1\",{\"nestedKey\": \"nestedValue\"}],\"key2\": \"value2\"}"); + } + + @Test + public void testIllegalStateOnEmptyDeque() { + builder.deque.clear(); + + Exception exception = expectThrows(IllegalStateException.class, () -> + builder.property("key", () -> builder.appendSimple("value")) + ); + assertEquals(exception.getMessage(), "Encountered empty stack!"); + + exception = expectThrows(IllegalStateException.class, () -> + builder.object(() -> { + }) + ); + assertEquals(exception.getMessage(), "Encountered empty stack!"); + + exception = expectThrows(IllegalStateException.class, () -> + builder.array(() -> { + }) + ); + assertEquals(exception.getMessage(), "Encountered empty stack!"); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomNumberGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomNumberGeneratorTest.java new file mode 100644 index 0000000000..02d987b0a1 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomNumberGeneratorTest.java @@ -0,0 +1,216 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.citrusframework.openapi.OpenApiConstants; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.math.BigDecimal; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class RandomNumberGeneratorTest { + + private RandomNumberGenerator generator; + private RandomContext mockContext; + private RandomModelBuilder mockBuilder; + private OasSchema schema; + + @DataProvider(name = "findLeastSignificantDecimalPlace") + public static Object[][] findLeastSignificantDecimalPlace() { + return new Object[][]{ + {new BigDecimal("1234.5678"), 4}, + {new BigDecimal("123.567"), 3}, + {new BigDecimal("123.56"), 2}, + {new BigDecimal("123.5"), 1}, + {new BigDecimal("123.0"), 0}, + {new BigDecimal("123"), 0} + }; + } + + @BeforeMethod + public void setUp() { + generator = new RandomNumberGenerator(); + mockContext = mock(RandomContext.class); + mockBuilder = mock(RandomModelBuilder.class); + schema = new Oas30Schema(); + + when(mockContext.getRandomModelBuilder()).thenReturn(mockBuilder); + } + + @Test + public void testGenerateDefaultBounds() { + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '-1000', '1000', 'false', 'false')"); + } + + @Test + public void testGenerateWithMinimum() { + schema.minimum = BigDecimal.valueOf(5); + generator.generate(mockContext, schema); + // Max is because of guessing a reasonable range + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '5', '1005', 'false', 'false')"); + } + + @Test + public void testGenerateWithMaximum() { + schema.maximum = BigDecimal.valueOf(15); + generator.generate(mockContext, schema); + // Min is because of guessing a reasonable range + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '-985', '15', 'false', 'false')"); + } + + @Test + public void testGenerateWithMinimumAndMaximum() { + schema.minimum = BigDecimal.valueOf(5); + schema.maximum = BigDecimal.valueOf(15); + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '5', '15', 'false', 'false')"); + } + + @Test + public void testGenerateWithExclusiveMinimum() { + schema.minimum = BigDecimal.valueOf(5); + schema.exclusiveMinimum = true; + generator.generate(mockContext, schema); + // Max is because of guessing a reasonable range + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '5', '1005', 'true', 'false')"); + } + + @Test + public void testGenerateWithExclusiveMaximum() { + schema.maximum = BigDecimal.valueOf(15); + schema.exclusiveMaximum = true; + generator.generate(mockContext, schema); + // Min is because of guessing a reasonable range + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '-985', '15', 'false', 'true')"); + } + + @Test + public void testGenerateWithMultipleOf() { + schema.multipleOf = BigDecimal.valueOf(5); + schema.minimum = BigDecimal.valueOf(10); + schema.maximum = BigDecimal.valueOf(50); + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('0', '10', '50', 'false', 'false', '5')"); + } + + @Test + public void testGenerateWithIntegerType() { + schema.type = "integer"; + schema.minimum = BigDecimal.valueOf(1); + schema.maximum = BigDecimal.valueOf(10); + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('0', '1', '10', 'false', 'false')"); + } + + @Test + public void testGenerateWithFloatType() { + schema.type = "number"; + schema.minimum = BigDecimal.valueOf(1.5); + schema.maximum = BigDecimal.valueOf(10.5); + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('2', '1.5', '10.5', 'false', 'false')"); + } + + @Test + public void testGenerateWithMultipleOfFloat() { + schema.type = "number"; + schema.multipleOf = BigDecimal.valueOf(0.5); + schema.minimum = BigDecimal.valueOf(1.0); + schema.maximum = BigDecimal.valueOf(5.0); + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimple("citrus:randomNumberGenerator('1', '1.0', '5.0', 'false', 'false', '0.5')"); + } + + @Test + public void testCalculateMinRelativeToMaxWithMultipleOf() { + BigDecimal max = new BigDecimal("1000"); + Number multipleOf = new BigDecimal("10"); + + BigDecimal result = RandomNumberGenerator.calculateMinRelativeToMax(max, multipleOf); + + BigDecimal expected = max.subtract(new BigDecimal(multipleOf.toString()).abs().multiply(RandomNumberGenerator.HUNDRED)); + assertEquals(result, expected); + } + + @Test + public void testCalculateMinRelativeToMaxWithoutMultipleOf() { + BigDecimal max = new BigDecimal("1000"); + + BigDecimal result = RandomNumberGenerator.calculateMinRelativeToMax(max, null); + + BigDecimal expected = max.subtract(max.multiply(BigDecimal.valueOf(2)).max(RandomNumberGenerator.THOUSAND)); + assertEquals(result, expected); + } + + @Test + public void testCalculateMaxRelativeToMinWithMultipleOf() { + BigDecimal min = new BigDecimal("1000"); + Number multipleOf = new BigDecimal("10"); + + BigDecimal result = RandomNumberGenerator.calculateMaxRelativeToMin(min, multipleOf); + + BigDecimal expected = min.add(new BigDecimal(multipleOf.toString()).abs().multiply(RandomNumberGenerator.HUNDRED)); + assertEquals(result, expected); + } + + @Test + public void testCalculateMaxRelativeToMinWithoutMultipleOf() { + BigDecimal min = new BigDecimal("1000"); + + BigDecimal result = RandomNumberGenerator.calculateMaxRelativeToMin(min, null); + + BigDecimal expected = min.add(min.multiply(BigDecimal.valueOf(2)).max(RandomNumberGenerator.THOUSAND)); + assertEquals(result, expected); + } + + @Test + public void testHandlesWithIntegerType() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_INTEGER; + + assertTrue(generator.handles(schema)); + } + + @Test + public void testHandlesWithNumberType() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_NUMBER; + + assertTrue(generator.handles(schema)); + } + + @Test + public void testHandlesWithOtherType() { + OasSchema schema = new Oas30Schema(); + schema.type = "string"; + + assertFalse(generator.handles(schema)); + } + + @Test + public void testHandlesWithNullType() { + OasSchema schema = new Oas30Schema(); + schema.type = null; + + assertFalse(generator.handles(schema)); + } + + @Test + public void testHandlesWithNullSchema() { + assertFalse(generator.handles(null)); + } + + @Test(dataProvider = "findLeastSignificantDecimalPlace") + public void findLeastSignificantDecimalPlace(BigDecimal number, int expectedSignificance) { + assertEquals(generator.findLeastSignificantDecimalPlace(number), expectedSignificance); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomObjectGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomObjectGeneratorTest.java new file mode 100644 index 0000000000..b77ec6d8fb --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomObjectGeneratorTest.java @@ -0,0 +1,136 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.citrusframework.openapi.OpenApiConstants; +import org.citrusframework.openapi.OpenApiSpecification; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class RandomObjectGeneratorTest { + + private RandomObjectGenerator generator; + private RandomContext contextMock; + private RandomModelBuilder randomModelBuilderSpy; + private OpenApiSpecification specificationMock; + + @BeforeMethod + public void setUp() { + generator = new RandomObjectGenerator(); + contextMock = mock(); + specificationMock = mock(); + + randomModelBuilderSpy = spy(new RandomModelBuilder(true)); + when(contextMock.getRandomModelBuilder()).thenReturn(randomModelBuilderSpy); + when(contextMock.getSpecification()).thenReturn(specificationMock); + when(contextMock.get(eq("OBJECT_STACK"), any())).thenReturn(new ArrayDeque<>()); + } + + @Test + public void testHandlesObjectType() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + + assertTrue(generator.handles(schema)); + } + + @Test + public void testDoesNotHandleNonObjectType() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_STRING; + + assertFalse(generator.handles(schema)); + } + + @Test + public void testGenerateObjectWithoutProperties() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + + generator.generate(contextMock, schema); + + verify(randomModelBuilderSpy).object(any()); + } + + @Test + public void testGenerateObjectWithProperties() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + schema.properties = new HashMap<>(); + OasSchema propertySchema = new Oas30Schema(); + schema.properties.put("property1", propertySchema); + + when(specificationMock.isGenerateOptionalFields()).thenReturn(true); + + generator.generate(contextMock, schema); + + verify(randomModelBuilderSpy).object(any()); + verify(randomModelBuilderSpy).property(eq("property1"), any()); + verify(contextMock).generate(propertySchema); + } + + @Test + public void testGenerateObjectWithRequiredProperties() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + schema.properties = new HashMap<>(); + OasSchema propertySchema = new Oas30Schema(); + schema.properties.put("property1", propertySchema); + schema.required = singletonList("property1"); + + when(specificationMock.isGenerateOptionalFields()).thenReturn(false); + + generator.generate(contextMock, schema); + + verify(randomModelBuilderSpy).object(any()); + verify(randomModelBuilderSpy).property(eq("property1"), any()); + verify(contextMock).generate(propertySchema); + } + + @Test + public void testGenerateObjectWithOptionalProperties() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + schema.properties = new HashMap<>(); + OasSchema propertySchema = new Oas30Schema(); + schema.properties.put("property1", propertySchema); + schema.required = emptyList(); + when(specificationMock.isGenerateOptionalFields()).thenReturn(false); + generator.generate(contextMock, schema); + + verify(randomModelBuilderSpy).object(any()); + verify(randomModelBuilderSpy, never()).property(eq("property1"), any()); + verify(contextMock, never()).generate(propertySchema); + } + + @Test + public void testGenerateObjectWithRecursion() { + OasSchema schema = new Oas30Schema(); + schema.type = OpenApiConstants.TYPE_OBJECT; + Deque objectStack = new ArrayDeque<>(); + objectStack.push(schema); + + when(contextMock.get(eq("OBJECT_STACK"), any())).thenReturn(objectStack); + + generator.generate(contextMock, schema); + + verify(randomModelBuilderSpy, never()).object(any()); + } + +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomStringGeneratorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomStringGeneratorTest.java new file mode 100644 index 0000000000..b45ae31f87 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/random/RandomStringGeneratorTest.java @@ -0,0 +1,70 @@ +package org.citrusframework.openapi.random; + +import io.apicurio.datamodels.openapi.models.OasSchema; +import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class RandomStringGeneratorTest { + + private RandomStringGenerator generator; + private RandomContext mockContext; + private RandomModelBuilder mockBuilder; + private OasSchema schema; + + @BeforeMethod + public void setUp() { + generator = new RandomStringGenerator(); + mockContext = mock(RandomContext.class); + mockBuilder = mock(RandomModelBuilder.class); + schema = new Oas30Schema(); + + when(mockContext.getRandomModelBuilder()).thenReturn(mockBuilder); + } + + @Test + public void testGenerateDefaultLength() { + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(10,MIXED,true,1)"); + } + + @Test + public void testGenerateWithMinLength() { + schema.minLength = 5; + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(10,MIXED,true,5)"); + } + + @Test + public void testGenerateWithMaxLength() { + schema.maxLength = 15; + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(15,MIXED,true,1)"); + } + + @Test + public void testGenerateWithMinAndMaxLength() { + schema.minLength = 3; + schema.maxLength = 8; + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(8,MIXED,true,3)"); + } + + @Test + public void testGenerateWithZeroMinLength() { + schema.minLength = 0; + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(10,MIXED,true,1)"); + } + + @Test + public void testGenerateWithZeroMaxLength() { + schema.maxLength = 0; + generator.generate(mockContext, schema); + verify(mockBuilder).appendSimpleQuoted("citrus:randomString(10,MIXED,true,1)"); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiOperationToMessageHeadersProcessorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiOperationToMessageHeadersProcessorTest.java new file mode 100644 index 0000000000..bcc3f4e503 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiOperationToMessageHeadersProcessorTest.java @@ -0,0 +1,63 @@ +package org.citrusframework.openapi.validation; + +import org.citrusframework.context.TestContext; +import org.citrusframework.message.Message; +import org.citrusframework.openapi.OpenApiMessageHeaders; +import org.citrusframework.openapi.OpenApiMessageType; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.Optional; + +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class OpenApiOperationToMessageHeadersProcessorTest { + + private OpenApiSpecification openApiSpecification; + private String operationId; + private OpenApiMessageType type; + private OpenApiOperationToMessageHeadersProcessor processor; + private Message message; + private TestContext context; + + @BeforeMethod + public void setUp() { + openApiSpecification = mock(OpenApiSpecification.class); + operationId = "testOperationId"; + type = mock(OpenApiMessageType.class); + processor = new OpenApiOperationToMessageHeadersProcessor(openApiSpecification, operationId, type); + + message = mock(Message.class); + context = mock(TestContext.class); + } + + @Test + public void testProcess() { + OperationPathAdapter operationPathAdapter = mock(OperationPathAdapter.class); + when(openApiSpecification.getOperation(operationId, context)) + .thenReturn(Optional.of(operationPathAdapter)); + when(operationPathAdapter.uniqueOperationId()).thenReturn("uniqueOperationId"); + when(type.toHeaderName()).thenReturn("headerName"); + + processor.process(message, context); + + verify(message).setHeader(OpenApiMessageHeaders.OAS_UNIQUE_OPERATION_ID, "uniqueOperationId"); + verify(message).setHeader(OpenApiMessageHeaders.OAS_MESSAGE_TYPE, "headerName"); + } + + @Test + public void testProcessOperationNotPresent() { + when(openApiSpecification.getOperation(operationId, context)) + .thenReturn(Optional.empty()); + + processor.process(message, context); + + verify(message, never()).setHeader(anyString(), anyString()); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiRequestValidatorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiRequestValidatorTest.java new file mode 100644 index 0000000000..938c98cc57 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiRequestValidatorTest.java @@ -0,0 +1,179 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.validation; + +import com.atlassian.oai.validator.OpenApiInteractionValidator; +import com.atlassian.oai.validator.model.Request; +import com.atlassian.oai.validator.model.Request.Method; +import com.atlassian.oai.validator.report.ValidationReport; +import org.citrusframework.exceptions.ValidationException; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestMethod; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static org.citrusframework.http.message.HttpMessageHeaders.HTTP_REQUEST_URI; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +public class OpenApiRequestValidatorTest { + + @Mock + private OpenApiSpecification openApiSpecificationMock; + + @Mock + private OpenApiValidationContext openApiValidationContextMock; + + @Mock + private OpenApiInteractionValidator openApiInteractionValidatorMock; + + @Mock + private OperationPathAdapter operationPathAdapterMock; + + @Mock + private HttpMessage httpMessageMock; + + @Mock + private ValidationReport validationReportMock; + + private OpenApiRequestValidator openApiRequestValidator; + + private AutoCloseable mockCloseable; + + @BeforeMethod + public void beforeMethod() { + mockCloseable = MockitoAnnotations.openMocks(this); + + doReturn(openApiValidationContextMock).when(openApiSpecificationMock).getOpenApiValidationContext(); + doReturn(openApiInteractionValidatorMock).when(openApiValidationContextMock).getOpenApiInteractionValidator(); + + openApiRequestValidator = new OpenApiRequestValidator(openApiSpecificationMock); + } + + @AfterMethod + public void afterMethod() throws Exception { + mockCloseable.close(); + } + + @Test + public void shouldValidateRequestWithNoErrors() { + // Given + when(httpMessageMock.getHeader(HTTP_REQUEST_URI)).thenReturn("/api/test"); + when(httpMessageMock.getRequestMethod()).thenReturn(RequestMethod.GET); + when(openApiInteractionValidatorMock.validateRequest(any(Request.class))) + .thenReturn(validationReportMock); + when(validationReportMock.hasErrors()).thenReturn(false); + + // When + openApiRequestValidator.validateRequest(operationPathAdapterMock, httpMessageMock); + + // Then + verify(openApiInteractionValidatorMock).validateRequest(any(Request.class)); + verify(validationReportMock).hasErrors(); + } + + @Test(expectedExceptions = ValidationException.class) + public void shouldValidateRequestWithErrors() { + // Given + when(httpMessageMock.getHeader(HTTP_REQUEST_URI)).thenReturn("/api/test"); + when(httpMessageMock.getRequestMethod()).thenReturn(RequestMethod.GET); + when(openApiInteractionValidatorMock.validateRequest(any(Request.class))) + .thenReturn(validationReportMock); + when(validationReportMock.hasErrors()).thenReturn(true); + + // When + openApiRequestValidator.validateRequest(operationPathAdapterMock, httpMessageMock); + + // Then + verify(openApiInteractionValidatorMock).validateRequest(any(Request.class)); + verify(validationReportMock).hasErrors(); + } + + @Test + public void shouldCreateRequestFromMessage() throws IOException { + // Given + when(httpMessageMock.getPayload()).thenReturn("payload"); + + Map headers = new HashMap<>(); + headers.put("array", List.of("e1", "e2")); + headers.put("nullarray", null); + headers.put("simple", "s1"); + + when(httpMessageMock.getHeaders()).thenReturn(headers); + when(httpMessageMock.getHeader(HTTP_REQUEST_URI)).thenReturn("/api/test"); + when(httpMessageMock.getRequestMethod()).thenReturn(RequestMethod.GET); + when(httpMessageMock.getAccept()).thenReturn(APPLICATION_JSON_VALUE); + when(operationPathAdapterMock.contextPath()).thenReturn("/api"); + + // When + Request request = openApiRequestValidator.createRequestFromMessage(operationPathAdapterMock, httpMessageMock); + + // Then + assertNotNull(request); + assertEquals(request.getPath(), "/test"); + assertEquals(request.getMethod(), Method.GET); + assertEquals(request.getHeaders().get("array"), List.of("e1", "e2")); + assertEquals(request.getHeaders().get("simple"), singletonList("s1")); + List nullList = new ArrayList<>(); + nullList.add(null); + assertEquals(request.getHeaders().get("nullarray"), nullList); + assertTrue(request.getRequestBody().isPresent()); + + assertEquals(request.getRequestBody().get().toString(StandardCharsets.UTF_8), "payload"); + } + + @Test + public void shouldCreateFormRequestFromMessage() throws IOException { + // Given + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("name", "John Doe"); + formData.add("age", 30); + formData.add("city", "New York"); + + when(httpMessageMock.getPayload()).thenReturn(formData); + + when(httpMessageMock.getHeader(HTTP_REQUEST_URI)).thenReturn("/api/test"); + when(httpMessageMock.getRequestMethod()).thenReturn(RequestMethod.GET); + + // When + Request request = openApiRequestValidator.createRequestFromMessage(operationPathAdapterMock, httpMessageMock); + + // Then + assertEquals(request.getRequestBody().orElseThrow(IllegalArgumentException::new).toString(StandardCharsets.UTF_8), "name=John+Doe&age=30&city=New+York"); + } +} diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidatorTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidatorTest.java new file mode 100644 index 0000000000..3bcb184b83 --- /dev/null +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/validation/OpenApiResponseValidatorTest.java @@ -0,0 +1,151 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.validation; + +import com.atlassian.oai.validator.OpenApiInteractionValidator; +import com.atlassian.oai.validator.model.Request.Method; +import com.atlassian.oai.validator.model.Response; +import com.atlassian.oai.validator.report.ValidationReport; +import io.apicurio.datamodels.openapi.models.OasOperation; +import org.citrusframework.exceptions.ValidationException; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.model.OperationPathAdapter; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatusCode; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +public class OpenApiResponseValidatorTest { + + @Mock + private OpenApiSpecification openApiSpecificationMock; + + @Mock + private OpenApiValidationContext openApiValidationContextMock; + + @Mock + private OpenApiInteractionValidator openApiInteractionValidatorMock; + + @Mock + private OasOperation operationMock; + + @Mock + private OperationPathAdapter operationPathAdapterMock; + + @Mock + private HttpMessage httpMessageMock; + + @Mock + private ValidationReport validationReportMock; + + @InjectMocks + private OpenApiResponseValidator openApiResponseValidator; + + private AutoCloseable mockCloseable; + + @BeforeMethod + public void beforeMethod() { + mockCloseable = MockitoAnnotations.openMocks(this); + + doReturn(openApiValidationContextMock).when(openApiSpecificationMock).getOpenApiValidationContext(); + doReturn(openApiInteractionValidatorMock).when(openApiValidationContextMock).getOpenApiInteractionValidator(); + + openApiResponseValidator = new OpenApiResponseValidator(openApiSpecificationMock); + } + + @AfterMethod + public void afterMethod() throws Exception { + mockCloseable.close(); + } + + @Test + public void shouldValidateWithNoErrors() { + // Given + when(openApiInteractionValidatorMock.validateResponse(anyString(), any(Method.class), any(Response.class))) + .thenReturn(validationReportMock); + when(validationReportMock.hasErrors()).thenReturn(false); + + when(operationPathAdapterMock.operation()).thenReturn(operationMock); + when(operationPathAdapterMock.apiPath()).thenReturn("/api/path"); + when(operationMock.getMethod()).thenReturn("get"); + when(httpMessageMock.getStatusCode()).thenReturn(HttpStatusCode.valueOf(200)); + + // When + openApiResponseValidator.validateResponse(operationPathAdapterMock, httpMessageMock); + + // Then + verify(openApiInteractionValidatorMock).validateResponse(anyString(), any(Method.class), any(Response.class)); + verify(validationReportMock).hasErrors(); + } + + @Test(expectedExceptions = ValidationException.class) + public void shouldValidateWithErrors() { + // Given + when(openApiInteractionValidatorMock.validateResponse(anyString(), any(Method.class), any(Response.class))) + .thenReturn(validationReportMock); + when(validationReportMock.hasErrors()).thenReturn(true); + + when(operationPathAdapterMock.operation()).thenReturn(operationMock); + when(operationPathAdapterMock.apiPath()).thenReturn("/api/path"); + when(operationMock.getMethod()).thenReturn("get"); + when(httpMessageMock.getStatusCode()).thenReturn(HttpStatusCode.valueOf(200)); + + // When + openApiResponseValidator.validateResponse(operationPathAdapterMock, httpMessageMock); + + // Then + verify(openApiInteractionValidatorMock).validateResponse(anyString(), any(Method.class), any(Response.class)); + verify(validationReportMock).hasErrors(); + } + + @Test + public void shouldCreateResponseMessage() throws IOException { + // Given + when(httpMessageMock.getPayload()).thenReturn("payload"); + when(httpMessageMock.getHeaders()).thenReturn(Map.of("Content-Type", APPLICATION_JSON_VALUE)); + when(httpMessageMock.getStatusCode()).thenReturn(HttpStatusCode.valueOf(200)); + + // When + Response response = openApiResponseValidator.createResponseFromMessage(httpMessageMock, 200); + + // Then + assertNotNull(response); + assertTrue(response.getResponseBody().isPresent()); + assertEquals(response.getResponseBody().get().toString(StandardCharsets.UTF_8), "payload"); + assertTrue(response.getHeaderValue("Content-Type").isPresent()); + assertEquals(response.getHeaderValue("Content-Type").get(), APPLICATION_JSON_VALUE); + assertEquals(response.getStatus(), Integer.valueOf(200)); + } +} 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 index 6d5fe30b8c..98f777dc40 100644 --- 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 @@ -57,7 +57,7 @@ protected TestContext createTestContext() { CitrusAnnotations.parseConfiguration(invocationOnMock.getArgument(0, Object.class), citrusContext); return null; }).when(citrusContext).parseConfiguration((Object) any()); - doAnswer(invocationOnMock-> { + doAnswer(invocationOnMock -> { context.getReferenceResolver().bind(invocationOnMock.getArgument(0), invocationOnMock.getArgument(1)); return null; }).when(citrusContext).addComponent(anyString(), any()); diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiClientTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiClientTest.java index 101e604c6e..768c957d4b 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiClientTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/xml/OpenApiClientTest.java @@ -16,11 +16,6 @@ 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; @@ -60,7 +55,13 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.io.IOException; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; + import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; public class OpenApiClientTest extends AbstractXmlActionTest { @@ -80,7 +81,6 @@ public class OpenApiClientTest extends AbstractXmlActionTest { private HttpClient httpClient; private final MessageQueue inboundQueue = new DefaultMessageQueue("inboundQueue"); - private final Queue responses = new ArrayBlockingQueue<>(6); @BeforeClass @@ -142,7 +142,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { ], "status": "available" } - """).status(HttpStatus.OK).contentType("application/json")); + """).status(HttpStatus.OK).contentType(APPLICATION_JSON_VALUE)); responses.add(new HttpMessage().status(HttpStatus.CREATED)); testLoader.load(); @@ -151,7 +151,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { 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.getActionCount(), 6L); Assert.assertEquals(result.getTestAction(0).getClass(), SendMessageAction.class); Assert.assertEquals(result.getTestAction(0).getName(), "openapi:send-request"); @@ -163,15 +163,15 @@ public void shouldLoadOpenApiClientActions() throws IOException { SendMessageAction sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); Assert.assertFalse(sendMessageAction.isForkMode()); Assert.assertTrue(sendMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); - HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + 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.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(sendMessageAction.getEndpointUri(), "httpClient"); @@ -182,31 +182,28 @@ public void shouldLoadOpenApiClientActions() throws IOException { validator.validateMessage(request, controlMessage, context, new DefaultValidationContext()); ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof HeaderValidationContext); - httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + 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.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 4L); 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.getMessageProcessors().size(), 1); Assert.assertEquals(receiveMessageAction.getControlMessageProcessors().size(), 0); sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); Assert.assertFalse(sendMessageAction.isForkMode()); - httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + 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)); @@ -215,19 +212,19 @@ public void shouldLoadOpenApiClientActions() throws IOException { 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.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), APPLICATION_JSON_VALUE); Assert.assertNull(sendMessageAction.getEndpoint()); Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpClient"); receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof HeaderValidationContext); - httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + httpMessageBuilder = ((HttpMessageBuilder) receiveMessageAction.getMessageBuilder()); Assert.assertNotNull(httpMessageBuilder); Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); @@ -247,4 +244,5 @@ 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 index 4f49613c96..a773b9c182 100644 --- 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 @@ -16,8 +16,6 @@ package org.citrusframework.openapi.xml; -import java.util.Map; - import org.citrusframework.TestActor; import org.citrusframework.TestCase; import org.citrusframework.TestCaseMetaInfo; @@ -34,6 +32,7 @@ import org.citrusframework.message.DefaultMessageQueue; import org.citrusframework.message.MessageHeaders; import org.citrusframework.message.MessageQueue; +import org.citrusframework.openapi.validation.OpenApiMessageValidationContext; import org.citrusframework.spi.BindToRegistry; import org.citrusframework.validation.context.HeaderValidationContext; import org.citrusframework.validation.json.JsonMessageValidationContext; @@ -45,22 +44,23 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.util.Map; + import static org.citrusframework.endpoint.direct.DirectEndpoints.direct; import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; 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()); + private HttpServer httpServer; @BeforeClass public void setupEndpoints() { @@ -81,32 +81,32 @@ public void shouldLoadOpenApiServerActions() { 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")); + .method(HttpMethod.GET) + .path("/petstore/v3/pet/12345") + .version("HTTP/1.1") + .accept(APPLICATION_JSON_VALUE) + .contentType(APPLICATION_JSON_VALUE)); 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")); + { + "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_VALUE)); testLoader.load(); @@ -124,29 +124,25 @@ public void shouldLoadOpenApiServerActions() { int actionIndex = 0; ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(3) instanceof OpenApiMessageValidationContext); Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); Assert.assertTrue(receiveMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); - HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder) receiveMessageAction.getMessageBuilder()); Assert.assertNotNull(httpMessageBuilder); Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); - Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 2L); 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()); + httpMessageBuilder = ((HttpMessageBuilder) sendMessageAction.getMessageBuilder()); Assert.assertNotNull(httpMessageBuilder); Assert.assertTrue(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()).toString().startsWith("{\"id\": ")); @@ -155,36 +151,32 @@ public void shouldLoadOpenApiServerActions() { 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.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_CONTENT_TYPE), APPLICATION_JSON_VALUE); + Assert.assertNull(sendMessageAction.getEndpoint()); Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); - Assert.assertEquals(sendMessageAction.getMessageProcessors().size(), 0); + Assert.assertEquals(sendMessageAction.getMessageProcessors().size(), 1); receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(3) instanceof OpenApiMessageValidationContext); Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 2000L); - httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + 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.assertEquals(requestHeaders.size(), 0L); Assert.assertNull(receiveMessageAction.getEndpoint()); Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpServer"); sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex); - httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + httpMessageBuilder = ((HttpMessageBuilder) sendMessageAction.getMessageBuilder()); Assert.assertNotNull(httpMessageBuilder); Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()), ""); Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); 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 index 6958c83ddc..aafbd77441 100644 --- 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 @@ -57,7 +57,7 @@ protected TestContext createTestContext() { CitrusAnnotations.parseConfiguration(invocationOnMock.getArgument(0, Object.class), citrusContext); return null; }).when(citrusContext).parseConfiguration((Object) any()); - doAnswer(invocationOnMock-> { + doAnswer(invocationOnMock -> { context.getReferenceResolver().bind(invocationOnMock.getArgument(0), invocationOnMock.getArgument(1)); return null; }).when(citrusContext).addComponent(anyString(), any()); diff --git a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiClientTest.java b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiClientTest.java index 0b09fa9c52..8685381ef7 100644 --- a/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiClientTest.java +++ b/connectors/citrus-openapi/src/test/java/org/citrusframework/openapi/yaml/OpenApiClientTest.java @@ -16,11 +16,6 @@ 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; @@ -58,7 +53,12 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; + import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; public class OpenApiClientTest extends AbstractYamlActionTest { @@ -73,13 +73,10 @@ public class OpenApiClientTest extends AbstractYamlActionTest { 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); + private HttpServer httpServer; + private HttpClient httpClient; @BeforeClass public void setupEndpoints() { @@ -114,7 +111,7 @@ public void cleanupEndpoints() { } @Test - public void shouldLoadOpenApiClientActions() throws IOException { + public void shouldLoadOpenApiClientActions() { YamlTestLoader testLoader = createTestLoader("classpath:org/citrusframework/openapi/yaml/openapi-client-test.yaml"); context.setVariable("port", port); @@ -139,7 +136,7 @@ public void shouldLoadOpenApiClientActions() throws IOException { ], "status": "available" } - """).status(HttpStatus.OK).contentType("application/json")); + """).status(HttpStatus.OK).contentType(APPLICATION_JSON_VALUE)); responses.add(new HttpMessage().status(HttpStatus.CREATED)); testLoader.load(); @@ -160,15 +157,15 @@ public void shouldLoadOpenApiClientActions() throws IOException { SendMessageAction sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); Assert.assertFalse(sendMessageAction.isForkMode()); Assert.assertTrue(sendMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); - HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + 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.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(sendMessageAction.getEndpointUri(), "httpClient"); @@ -179,31 +176,28 @@ public void shouldLoadOpenApiClientActions() throws IOException { validator.validateMessage(request, controlMessage, context, new DefaultValidationContext()); ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof HeaderValidationContext); - httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + 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.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 4L); 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.getMessageProcessors().size(), 1); Assert.assertEquals(receiveMessageAction.getControlMessageProcessors().size(), 0); sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex++); Assert.assertFalse(sendMessageAction.isForkMode()); - httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + 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)); @@ -212,19 +206,19 @@ public void shouldLoadOpenApiClientActions() throws IOException { 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.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), APPLICATION_JSON_VALUE); Assert.assertNull(sendMessageAction.getEndpoint()); Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpClient"); receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof JsonMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof HeaderValidationContext); - httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + httpMessageBuilder = ((HttpMessageBuilder) receiveMessageAction.getMessageBuilder()); Assert.assertNotNull(httpMessageBuilder); Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); 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 index d8dac9eb1d..af987a9983 100644 --- 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 @@ -16,8 +16,6 @@ package org.citrusframework.openapi.yaml; -import java.util.Map; - import org.citrusframework.TestActor; import org.citrusframework.TestCase; import org.citrusframework.TestCaseMetaInfo; @@ -34,6 +32,7 @@ import org.citrusframework.message.DefaultMessageQueue; import org.citrusframework.message.MessageHeaders; import org.citrusframework.message.MessageQueue; +import org.citrusframework.openapi.validation.OpenApiMessageValidationContext; import org.citrusframework.spi.BindToRegistry; import org.citrusframework.validation.context.HeaderValidationContext; import org.citrusframework.validation.json.JsonMessageValidationContext; @@ -45,22 +44,23 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.util.Map; + import static org.citrusframework.endpoint.direct.DirectEndpoints.direct; import static org.citrusframework.http.endpoint.builder.HttpEndpoints.http; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; 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()); + private HttpServer httpServer; @BeforeClass public void setupEndpoints() { @@ -81,32 +81,32 @@ public void shouldLoadOpenApiServerActions() { 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")); + .method(HttpMethod.GET) + .path("/petstore/v3/pet/12345") + .version("HTTP/1.1") + .accept(APPLICATION_JSON_VALUE) + .contentType(APPLICATION_JSON_VALUE)); 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")); + { + "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_VALUE)); testLoader.load(); @@ -124,29 +124,25 @@ public void shouldLoadOpenApiServerActions() { int actionIndex = 0; ReceiveMessageAction receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(3) instanceof OpenApiMessageValidationContext); Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 0L); Assert.assertTrue(receiveMessageAction.getMessageBuilder() instanceof HttpMessageBuilder); - HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + HttpMessageBuilder httpMessageBuilder = ((HttpMessageBuilder) receiveMessageAction.getMessageBuilder()); Assert.assertNotNull(httpMessageBuilder); Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, receiveMessageAction.getMessageType()), ""); - Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 5L); + Assert.assertEquals(httpMessageBuilder.getMessage().getHeaders().size(), 2L); 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()); + httpMessageBuilder = ((HttpMessageBuilder) sendMessageAction.getMessageBuilder()); Assert.assertNotNull(httpMessageBuilder); Assert.assertTrue(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()).toString().startsWith("{\"id\": ")); @@ -155,36 +151,32 @@ public void shouldLoadOpenApiServerActions() { 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.assertEquals(httpMessageBuilder.getMessage().getHeaders().get(HttpMessageHeaders.HTTP_CONTENT_TYPE), APPLICATION_JSON_VALUE); + Assert.assertNull(sendMessageAction.getEndpoint()); Assert.assertEquals(sendMessageAction.getEndpointUri(), "httpServer"); - Assert.assertEquals(sendMessageAction.getMessageProcessors().size(), 0); + Assert.assertEquals(sendMessageAction.getMessageProcessors().size(), 1); receiveMessageAction = (ReceiveMessageAction) result.getTestAction(actionIndex++); - Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 3); + Assert.assertEquals(receiveMessageAction.getValidationContexts().size(), 4); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(0) instanceof HeaderValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(1) instanceof XmlMessageValidationContext); Assert.assertTrue(receiveMessageAction.getValidationContexts().get(2) instanceof JsonMessageValidationContext); + Assert.assertTrue(receiveMessageAction.getValidationContexts().get(3) instanceof OpenApiMessageValidationContext); Assert.assertEquals(receiveMessageAction.getReceiveTimeout(), 2000L); - httpMessageBuilder = ((HttpMessageBuilder)receiveMessageAction.getMessageBuilder()); + 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.assertEquals(requestHeaders.size(), 0L); Assert.assertNull(receiveMessageAction.getEndpoint()); Assert.assertEquals(receiveMessageAction.getEndpointUri(), "httpServer"); sendMessageAction = (SendMessageAction) result.getTestAction(actionIndex); - httpMessageBuilder = ((HttpMessageBuilder)sendMessageAction.getMessageBuilder()); + httpMessageBuilder = ((HttpMessageBuilder) sendMessageAction.getMessageBuilder()); Assert.assertNotNull(httpMessageBuilder); Assert.assertEquals(httpMessageBuilder.buildMessagePayload(context, sendMessageAction.getMessageType()), ""); Assert.assertNotNull(httpMessageBuilder.getMessage().getHeaders().get(MessageHeaders.ID)); diff --git a/connectors/citrus-openapi/src/test/resources/META-INF/citrus/openapi/processor/sampleOpenApiProcessor b/connectors/citrus-openapi/src/test/resources/META-INF/citrus/openapi/processor/sampleOpenApiProcessor new file mode 100644 index 0000000000..1092e9da72 --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/META-INF/citrus/openapi/processor/sampleOpenApiProcessor @@ -0,0 +1,2 @@ +name=sampleOpenApiProcessor +type=org.citrusframework.openapi.SampleOpenApiProcessor diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/faulty/faulty-ping-api.yaml b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/faulty/faulty-ping-api.yaml new file mode 100644 index 0000000000..25d8f64694 --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/faulty/faulty-ping-api.yaml @@ -0,0 +1,422 @@ +openapi: 3.0.1 +info: + title: Ping API + description: 'A simple OpenApi defining that contains errors according to OpenApi Specification. In this case - Responses without description.' + version: 1.0 + +servers: + - url: http://localhost:9000/services/rest/ping/v1 + - url: http://localhost:9000/ping/v1 + +paths: + /ping/{id}: + put: + tags: + - ping + summary: Do the ping + operationId: doPing + parameters: + - name: id + in: path + description: Id to ping + required: true + schema: + type: integer + format: int64 + - name: q1 + in: query + description: Some queryParameter + required: true + schema: + type: integer + format: int64 + - name: api-key + in: header + description: Some header + required: true + schema: + type: string + requestBody: + description: Ping data + content: + application/json: + schema: + $ref: '#/components/schemas/PingReqType' + required: true + responses: + 200: + headers: + ping-time: + required: false + description: response time + schema: + type: integer + format: int64 + content: + application/json: + schema: + $ref: '#/components/schemas/PingRespType' + plain/text: + schema: + type: string + 405: + content: + text/plain: + schema: + type: string +components: + schemas: + DateType: + required: + - date + type: object + properties: + date: + type: string + format: date + DateTimeType: + required: + - dateTime + type: object + properties: + dateTime: + type: string + format: date-time + AllOfType: + allOf: + - $ref: '#/components/schemas/NumbersType' + - $ref: '#/components/schemas/StringsType' + - $ref: '#/components/schemas/MultipleOfType' + - $ref: '#/components/schemas/DatesType' + discriminator: + propertyName: type + mapping: + NumbersType: '#/components/schemas/NumbersType' + StringsType: '#/components/schemas/StringsType' + MultipleOfType: '#/components/schemas/MultipleOfType' + DatesType: '#/components/schemas/DatesType' + AnyOfType: + anyOf: + - $ref: '#/components/schemas/NumbersType' + - $ref: '#/components/schemas/StringsType' + - $ref: '#/components/schemas/MultipleOfType' + - $ref: '#/components/schemas/DatesType' + discriminator: + propertyName: type + mapping: + NumbersType: '#/components/schemas/NumbersType' + StringsType: '#/components/schemas/StringsType' + MultipleOfType: '#/components/schemas/MultipleOfType' + DatesType: '#/components/schemas/DatesType' + OneOfType: + oneOf: + - $ref: '#/components/schemas/NumbersType' + - $ref: '#/components/schemas/StringsType' + - $ref: '#/components/schemas/MultipleOfType' + - $ref: '#/components/schemas/DatesType' + discriminator: + propertyName: type + mapping: + NumbersType: '#/components/schemas/NumbersType' + StringsType: '#/components/schemas/StringsType' + MultipleOfType: '#/components/schemas/MultipleOfType' + DatesType: '#/components/schemas/DatesType' + MultipleOfType: + type: object + required: + - type + - manyPi + - even + properties: + type: + type: string + enum: [ MultipleOfType ] + manyPi: + type: number + format: double + multipleOf: 3.14159 + minimum: 0 + maximum: 31459 + even: + type: integer + format: int32 + multipleOf: 2 + minimum: -2000 + maximum: 2000 + StringsType: + type: object + required: + - type + properties: + type: + type: string + enum: [ StringsType ] + smallString: + type: string + minLength: 0 + maxLength: 10 + mediumString: + type: string + minLength: 0 + maxLength: 256 + largeString: + type: string + minLength: 0 + maxLength: 1024 + nonEmptyString: + type: string + minLength: 256 + maxLength: 512 + NumbersType: + type: object + required: + - type + - integerInt32 + - integerInt64 + - numberFloat + - numberDouble + - positiveIntegerInt32 + - negativeIntegerInt64 + - positiveNumberFloat + - negativeNumberDouble + - betweenIntegerInt32 + - betweenIntegerInt64 + - betweenNumberFloat + - betweenNumberDouble + - betweenIntegerInt32Exclude + - betweenIntegerInt64Exclude + - betweenNumberFloatExclude + - betweenNumberDoubleExclude + properties: + type: + type: string + enum: [ NumbersType ] + integerInt32: + type: integer + format: int32 + integerInt64: + type: integer + format: int64 + numberFloat: + type: number + format: float + numberDouble: + type: number + format: double + positiveIntegerInt32: + type: integer + format: int32 + minimum: 0 + negativeIntegerInt64: + type: integer + format: int64 + maximum: 0 + positiveNumberFloat: + type: number + format: float + minimum: 0 + negativeNumberDouble: + type: number + format: double + maximum: 0 + betweenIntegerInt32: + type: integer + format: int32 + minimum: 2 + maximum: 8 + betweenIntegerInt64: + type: integer + format: int64 + minimum: 2 + maximum: 3 + betweenNumberFloat: + type: number + format: float + minimum: 2 + maximum: 3 + betweenNumberDouble: + type: number + format: double + minimum: 2 + maximum: 3 + betweenIntegerInt32Exclude: + type: integer + format: int32 + minimum: 2 + maximum: 4 + exclusiveMinimum: true + exclusiveMaximum: true + betweenIntegerInt64Exclude: + type: integer + format: int64 + minimum: 2 + maximum: 4 + exclusiveMinimum: true + exclusiveMaximum: true + betweenNumberFloatExclude: + type: number + format: float + minimum: 2 + maximum: 4 + exclusiveMinimum: true + exclusiveMaximum: true + betweenNumberDoubleExclude: + type: number + format: double + minimum: 2 + maximum: 4 + exclusiveMinimum: true + exclusiveMaximum: true + DatesType: + required: + - type + - date + - dateTime + type: object + properties: + type: + type: string + enum: [ DatesType ] + date: + type: string + format: date + dateTime: + type: string + format: date-time + PingReqType: + type: object + properties: + id: + type: integer + format: int64 + Detail1: + type: object + required: + - type + properties: + type: + type: string + enum: [ Detail1Type ] + allTypes: + $ref: '#/components/schemas/NumbersType' + Detail2: + type: object + required: + - type + properties: + type: + type: string + enum: [ Detail2Type ] + allString: + $ref: '#/components/schemas/StringsType' + allDates: + $ref: '#/components/schemas/DatesType' + PingRespType: + type: object + required: + - type + properties: + type: + type: string + enum: [ PingRespType ] + id: + type: integer + format: int64 + value: + type: string + other: + anyOf: + - $ref: '#/components/schemas/Detail1' + - $ref: '#/components/schemas/Detail2' + discriminator: + propertyName: type + mapping: + Detail1Type: '#/components/schemas/Detail1' + Detail2Type: '#/components/schemas/Detail2' + BooleanType: + type: object + required: + - isActive + - isVerified + properties: + isActive: + type: boolean + isVerified: + type: boolean + EnumType: + type: object + required: + - status + properties: + status: + type: string + enum: + - ACTIVE + - INACTIVE + - PENDING + NestedType: + type: object + properties: + id: + type: integer + format: int64 + details: + $ref: '#/components/schemas/Detail1' + SimpleArrayType: + type: object + properties: + stringItems: + type: array + items: + type: string + minLength: 2 + maxLength: 5 + minItems: 10 + maxItems: 20 + numberItems: + type: array + items: + type: integer + minItems: 10 + maxItems: 20 + booleanItems: + type: array + items: + type: boolean + dateItems: + type: array + items: + type: string + format: date + ComplexArrayType: + type: object + properties: + stringItems: + type: array + items: + $ref: '#/components/schemas/StringsType' + numberItems: + type: array + items: + $ref: '#/components/schemas/NumbersType' + ArrayOfArraysType: + type: object + properties: + matrix: + type: array + items: + type: array + items: + type: integer + NullableType: + type: object + properties: + nullableString: + type: string + nullable: true + DefaultValueType: + type: object + properties: + defaultValue: + type: string + default: "defaultValue" \ No newline at end of file diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/pet_invalid.json b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/pet_invalid.json new file mode 100644 index 0000000000..c265dff5be --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/pet_invalid.json @@ -0,0 +1,15 @@ +{ + "id": ${petId}, + "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-derivation-for-message-builder-test.json b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-derivation-for-message-builder-test.json index a3d3953ce1..f965f09a33 100644 --- a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-derivation-for-message-builder-test.json +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/petstore/petstore-derivation-for-message-builder-test.json @@ -49,6 +49,14 @@ "schema": { "type": "string" } + }, + { + "name": "non-required-sample-param", + "required": false, + "in": "query", + "schema": { + "type": "string" + } } ], "tags": [ 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 index 618854948f..a7e135c535 100644 --- 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 @@ -98,7 +98,8 @@ "description": "ID of pet to return", "schema": { "format": "int64", - "type": "integer" + "type": "integer", + "minimum": 1 }, "in": "path", "required": true @@ -158,7 +159,8 @@ "description": "Pet id to delete", "schema": { "format": "int64", - "type": "integer" + "type": "integer", + "minimum": 1 }, "in": "path", "required": true diff --git a/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/ping/ping-api.yaml b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/ping/ping-api.yaml new file mode 100644 index 0000000000..4d694e28e1 --- /dev/null +++ b/connectors/citrus-openapi/src/test/resources/org/citrusframework/openapi/ping/ping-api.yaml @@ -0,0 +1,467 @@ +openapi: 3.0.1 +info: + title: Ping API + description: 'A simple OpenApi defining schemas for testing purposes' + version: 1.0 + +servers: + - url: http://localhost:9000/services/rest/ping/v1 + - url: http://localhost:9000/ping/v1 + +paths: + /ping/{id}: + put: + tags: + - ping + summary: Do the ping + operationId: doPing + parameters: + - name: id + in: path + description: Id to ping + required: true + schema: + type: integer + format: int64 + - name: q1 + in: query + description: Some queryParameter + required: true + schema: + type: integer + format: int64 + - name: api-key + in: header + description: Some header + required: true + schema: + type: string + requestBody: + description: Ping data + content: + application/json: + schema: + $ref: '#/components/schemas/PingReqType' + required: true + responses: + 200: + description: successful operation + headers: + ping-time: + required: false + description: response time + schema: + type: integer + format: int64 + content: + application/json: + schema: + $ref: '#/components/schemas/PingRespType' + plain/text: + schema: + type: string + 405: + description: Some error + content: + text/plain: + schema: + type: string + /pong/{id}: + get: + tags: + - pong + summary: Do the pong + operationId: doPong + parameters: + - name: id + in: path + description: Id to pong + required: true + explode: true + schema: + type: integer + format: int64 + responses: + 200: + description: successful operation without a response + /pung/{id}: + get: + tags: + - pong + summary: Do the pung + operationId: doPung + parameters: + - name: id + in: path + description: Id to pung + required: true + explode: true + schema: + type: integer + format: int64 + responses: + 200: + description: successful pung operation with all types + content: + application/json: + schema: + $ref: '#/components/schemas/StringsType' + plain/text: + schema: + type: string +components: + schemas: + DateType: + required: + - date + type: object + properties: + date: + type: string + format: date + DateTimeType: + required: + - dateTime + type: object + properties: + dateTime: + type: string + format: date-time + AllOfType: + allOf: + - $ref: '#/components/schemas/NumbersType' + - $ref: '#/components/schemas/StringsType' + - $ref: '#/components/schemas/MultipleOfType' + - $ref: '#/components/schemas/DatesType' + discriminator: + propertyName: type + mapping: + NumbersType: '#/components/schemas/NumbersType' + StringsType: '#/components/schemas/StringsType' + MultipleOfType: '#/components/schemas/MultipleOfType' + DatesType: '#/components/schemas/DatesType' + AnyOfType: + anyOf: + - $ref: '#/components/schemas/NumbersType' + - $ref: '#/components/schemas/StringsType' + - $ref: '#/components/schemas/MultipleOfType' + - $ref: '#/components/schemas/DatesType' + discriminator: + propertyName: type + mapping: + NumbersType: '#/components/schemas/NumbersType' + StringsType: '#/components/schemas/StringsType' + MultipleOfType: '#/components/schemas/MultipleOfType' + DatesType: '#/components/schemas/DatesType' + OneOfType: + oneOf: + - $ref: '#/components/schemas/NumbersType' + - $ref: '#/components/schemas/StringsType' + - $ref: '#/components/schemas/MultipleOfType' + - $ref: '#/components/schemas/DatesType' + discriminator: + propertyName: type + mapping: + NumbersType: '#/components/schemas/NumbersType' + StringsType: '#/components/schemas/StringsType' + MultipleOfType: '#/components/schemas/MultipleOfType' + DatesType: '#/components/schemas/DatesType' + MultipleOfType: + type: object + required: + - type + - manyPi + - even + properties: + type: + type: string + enum: [ MultipleOfType ] + manyPi: + type: number + format: double + multipleOf: 3.14159 + minimum: 0 + maximum: 31459 + even: + type: integer + format: int32 + multipleOf: 2 + minimum: -2000 + maximum: 2000 + StringsType: + type: object + required: + - type + properties: + type: + type: string + enum: [ StringsType ] + smallString: + type: string + minLength: 0 + maxLength: 10 + mediumString: + type: string + minLength: 0 + maxLength: 256 + largeString: + type: string + minLength: 0 + maxLength: 1024 + nonEmptyString: + type: string + minLength: 256 + maxLength: 512 + NumbersType: + type: object + required: + - type + - integerInt32 + - integerInt64 + - numberFloat + - numberDouble + - positiveIntegerInt32 + - negativeIntegerInt64 + - positiveNumberFloat + - negativeNumberDouble + - betweenIntegerInt32 + - betweenIntegerInt64 + - betweenNumberFloat + - betweenNumberDouble + - betweenIntegerInt32Exclude + - betweenIntegerInt64Exclude + - betweenNumberFloatExclude + - betweenNumberDoubleExclude + properties: + type: + type: string + enum: [ NumbersType ] + integerInt32: + type: integer + format: int32 + integerInt64: + type: integer + format: int64 + numberFloat: + type: number + format: float + numberDouble: + type: number + format: double + positiveIntegerInt32: + type: integer + format: int32 + minimum: 0 + negativeIntegerInt64: + type: integer + format: int64 + maximum: 0 + positiveNumberFloat: + type: number + format: float + minimum: 0 + negativeNumberDouble: + type: number + format: double + maximum: 0 + betweenIntegerInt32: + type: integer + format: int32 + minimum: 2 + maximum: 8 + betweenIntegerInt64: + type: integer + format: int64 + minimum: 2 + maximum: 3 + betweenNumberFloat: + type: number + format: float + minimum: 2 + maximum: 3 + betweenNumberDouble: + type: number + format: double + minimum: 2 + maximum: 3 + betweenIntegerInt32Exclude: + type: integer + format: int32 + minimum: 2 + maximum: 4 + exclusiveMinimum: true + exclusiveMaximum: true + betweenIntegerInt64Exclude: + type: integer + format: int64 + minimum: 2 + maximum: 4 + exclusiveMinimum: true + exclusiveMaximum: true + betweenNumberFloatExclude: + type: number + format: float + minimum: 2 + maximum: 4 + exclusiveMinimum: true + exclusiveMaximum: true + betweenNumberDoubleExclude: + type: number + format: double + minimum: 2 + maximum: 4 + exclusiveMinimum: true + exclusiveMaximum: true + DatesType: + required: + - type + - date + - dateTime + type: object + properties: + type: + type: string + enum: [ DatesType ] + date: + type: string + format: date + dateTime: + type: string + format: date-time + PingReqType: + type: object + properties: + id: + type: integer + format: int64 + Detail1: + type: object + required: + - type + properties: + type: + type: string + enum: [ Detail1Type ] + allTypes: + $ref: '#/components/schemas/NumbersType' + Detail2: + type: object + required: + - type + properties: + type: + type: string + enum: [ Detail2Type ] + allString: + $ref: '#/components/schemas/StringsType' + allDates: + $ref: '#/components/schemas/DatesType' + PingRespType: + type: object + required: + - type + properties: + type: + type: string + enum: [ PingRespType ] + id: + type: integer + format: int64 + value: + type: string + other: + anyOf: + - $ref: '#/components/schemas/Detail1' + - $ref: '#/components/schemas/Detail2' + discriminator: + propertyName: type + mapping: + Detail1Type: '#/components/schemas/Detail1' + Detail2Type: '#/components/schemas/Detail2' + BooleanType: + type: object + required: + - isActive + - isVerified + properties: + isActive: + type: boolean + isVerified: + type: boolean + EnumType: + type: object + required: + - status + properties: + status: + type: string + enum: + - ACTIVE + - INACTIVE + - PENDING + NestedType: + type: object + properties: + id: + type: integer + format: int64 + details: + $ref: '#/components/schemas/Detail1' + SimpleArrayType: + type: object + properties: + stringItems: + type: array + items: + type: string + minLength: 2 + maxLength: 5 + minItems: 10 + maxItems: 20 + numberItems: + type: array + items: + type: integer + minItems: 10 + maxItems: 20 + booleanItems: + type: array + items: + type: boolean + dateItems: + type: array + items: + type: string + format: date + ComplexArrayType: + type: object + properties: + stringItems: + type: array + items: + $ref: '#/components/schemas/StringsType' + numberItems: + type: array + items: + $ref: '#/components/schemas/NumbersType' + ArrayOfArraysType: + type: object + properties: + matrix: + type: array + items: + type: array + items: + type: integer + NullableType: + type: object + properties: + nullableString: + type: string + nullable: true + DefaultValueType: + type: object + properties: + defaultValue: + type: string + default: "defaultValue" \ No newline at end of file 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 index 019c1a9e07..48837d6301 100644 --- 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 @@ -37,5 +37,17 @@ + + + + + + + + + + + + diff --git a/connectors/citrus-testcontainers/pom.xml b/connectors/citrus-testcontainers/pom.xml index 06150f9b3f..03d0175dfc 100644 --- a/connectors/citrus-testcontainers/pom.xml +++ b/connectors/citrus-testcontainers/pom.xml @@ -13,6 +13,10 @@ citrus-testcontainers Citrus :: Connectors :: Testcontainers + + false + + org.citrusframework @@ -138,4 +142,23 @@ test + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${skipContainerTests} + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${skipContainerTests} + + + + diff --git a/core/citrus-api/src/main/java/org/citrusframework/CitrusSettings.java b/core/citrus-api/src/main/java/org/citrusframework/CitrusSettings.java index 00c32339ae..d8e13ed643 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/CitrusSettings.java +++ b/core/citrus-api/src/main/java/org/citrusframework/CitrusSettings.java @@ -429,7 +429,7 @@ public static Set getTestFileNamePattern(String type) { * @param def the default value * @return first value encountered, which is not null. May return null, if default value is null. */ - private static String getPropertyEnvOrDefault(String prop, String env, String def) { + public static String getPropertyEnvOrDefault(String prop, String env, String def) { return getProperty(prop, getenv(env) != null ? getenv(env) : def); } } diff --git a/core/citrus-api/src/main/java/org/citrusframework/TestActionBuilder.java b/core/citrus-api/src/main/java/org/citrusframework/TestActionBuilder.java index b6b7db67bf..601c3926e6 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/TestActionBuilder.java +++ b/core/citrus-api/src/main/java/org/citrusframework/TestActionBuilder.java @@ -16,14 +16,14 @@ package org.citrusframework; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ResourcePathTypeResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; +import java.util.Optional; + /** * Test action builder. * @since 2.3 @@ -64,7 +64,7 @@ static Map> lookup() { Map> builders = TYPE_RESOLVER.resolveAll(); if (logger.isDebugEnabled()) { - builders.forEach((k, v) -> logger.debug(String.format("Found test action builder '%s' as %s", k, v.getClass()))); + builders.forEach((k, v) -> logger.debug("Found test action builder '{}' as {}", k, v.getClass())); } return builders; } @@ -73,7 +73,7 @@ static Map> lookup() { * Resolves test action builder from resource path lookup with given resource name. Scans classpath for test action builder meta information * with given name and returns instance of the builder. Returns optional instead of throwing exception when no test action builder * could be found. - * + *

* Given builder name is a combination of resource file name and type property separated by '.' character. * @param builder * @return @@ -82,7 +82,7 @@ static Optional> lookup(String builder) { try { return Optional.of(TYPE_RESOLVER.resolve(builder)); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve test action builder from resource '%s/%s'", RESOURCE_PATH, builder)); + logger.warn("Failed to resolve test action builder from resource '{}/{}'", RESOURCE_PATH, builder); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/annotations/CitrusEndpointAnnotations.java b/core/citrus-api/src/main/java/org/citrusframework/annotations/CitrusEndpointAnnotations.java index c14fc50dde..3a56669423 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/annotations/CitrusEndpointAnnotations.java +++ b/core/citrus-api/src/main/java/org/citrusframework/annotations/CitrusEndpointAnnotations.java @@ -16,9 +16,6 @@ package org.citrusframework.annotations; -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; - import org.citrusframework.context.TestContext; import org.citrusframework.endpoint.Endpoint; import org.citrusframework.spi.BindToRegistry; @@ -28,6 +25,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + /** * Dependency injection support for {@link CitrusEndpoint} endpoint annotations. * @@ -57,7 +57,7 @@ public static void injectEndpoints(final Object target, final TestContext contex return; } - logger.debug(String.format("Injecting Citrus endpoint on test class field '%s'", field.getName())); + logger.debug("Injecting Citrus endpoint on test class field '{}'", field.getName()); CitrusEndpoint endpointAnnotation = field.getAnnotation(CitrusEndpoint.class); for (Annotation annotation : field.getAnnotations()) { diff --git a/core/citrus-api/src/main/java/org/citrusframework/common/TestLoader.java b/core/citrus-api/src/main/java/org/citrusframework/common/TestLoader.java index 79b3156d46..abbe9d5d5e 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/common/TestLoader.java +++ b/core/citrus-api/src/main/java/org/citrusframework/common/TestLoader.java @@ -16,16 +16,16 @@ package org.citrusframework.common; -import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; - import org.citrusframework.TestCase; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ResourcePathTypeResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + /** * Test loader interface. * @since 2.1 @@ -86,7 +86,7 @@ static Map lookup() { Map loader = TYPE_RESOLVER.resolveAll(); if (logger.isDebugEnabled()) { - loader.forEach((k, v) -> logger.debug(String.format("Found test loader '%s' as %s", k, v.getClass()))); + loader.forEach((k, v) -> logger.debug("Found test loader '{}' as {}", k, v.getClass())); } return loader; } @@ -102,7 +102,7 @@ static Optional lookup(String loader) { try { return Optional.of(TYPE_RESOLVER.resolve(loader)); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve test loader from resource '%s/%s'", RESOURCE_PATH, loader)); + logger.warn("Failed to resolve test loader from resource '{}/{}'", RESOURCE_PATH, loader); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/config/annotation/AnnotationConfigParser.java b/core/citrus-api/src/main/java/org/citrusframework/config/annotation/AnnotationConfigParser.java index f497441f8b..1f3f612397 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/config/annotation/AnnotationConfigParser.java +++ b/core/citrus-api/src/main/java/org/citrusframework/config/annotation/AnnotationConfigParser.java @@ -16,11 +16,6 @@ package org.citrusframework.config.annotation; -import java.lang.annotation.Annotation; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.endpoint.Endpoint; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ReferenceResolver; @@ -29,6 +24,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + /** * @since 2.5 */ @@ -63,7 +63,7 @@ static Map lookup() { parsers.putAll(TYPE_RESOLVER.resolveAll("", TypeResolver.TYPE_PROPERTY_WILDCARD)); if (logger.isDebugEnabled()) { - parsers.forEach((k, v) -> logger.debug(String.format("Found annotation config parser '%s' as %s", k, v.getClass()))); + parsers.forEach((k, v) -> logger.debug("Found annotation config parser '{}' as {}", k, v.getClass())); } } return parsers; @@ -73,7 +73,7 @@ static Map lookup() { * Resolves annotation config parser from resource path lookup with given resource name. Scans classpath for annotation config parser meta information * with given name and returns instance of the parser. Returns optional instead of throwing exception when no annotation config parser * could be found. - * + *

* Given parser name is a combination of resource file name and type property separated by '.' character. * @param parser * @return @@ -90,7 +90,7 @@ static Optional lookup(String parser) { return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve annotation config parser from resource '%s/%s'", RESOURCE_PATH, parser)); + logger.warn("Failed to resolve annotation config parser from resource '{}/{}'", RESOURCE_PATH, parser); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/context/TestContext.java b/core/citrus-api/src/main/java/org/citrusframework/context/TestContext.java index 0bf62b1b7d..5a872a0ed0 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/context/TestContext.java +++ b/core/citrus-api/src/main/java/org/citrusframework/context/TestContext.java @@ -16,17 +16,6 @@ package org.citrusframework.context; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - import org.citrusframework.CitrusSettings; import org.citrusframework.TestAction; import org.citrusframework.TestActionBuilder; @@ -69,6 +58,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + /** * Class holding and managing test variables. The test context also provides utility methods * for replacing dynamic content(variables and functions) in message payloads and headers. @@ -255,7 +255,7 @@ public void setVariable(final String variableName, Object value) { } if (logger.isDebugEnabled()) { - logger.debug(String.format("Setting variable: %s with value: '%s'", VariableUtils.cutOffVariablesPrefix(variableName), value)); + logger.debug("Setting variable: {} with value: '{}'", VariableUtils.cutOffVariablesPrefix(variableName), value); } variables.put(VariableUtils.cutOffVariablesPrefix(variableName), value); @@ -849,7 +849,7 @@ private void logMessage(String operation, Message message, MessageDirection dire messageListeners.onInboundMessage(message, this); } } else if (logger.isDebugEnabled()) { - logger.debug(String.format("%s message:%n%s", operation, Optional.ofNullable(message).map(Message::toString).orElse(""))); + logger.debug("{} message:%n{}", operation, Optional.ofNullable(message).map(Message::toString).orElse("")); } } diff --git a/core/citrus-api/src/main/java/org/citrusframework/context/resolver/TypeAliasResolver.java b/core/citrus-api/src/main/java/org/citrusframework/context/resolver/TypeAliasResolver.java index 24ca0dc75d..2bec20ee41 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/context/resolver/TypeAliasResolver.java +++ b/core/citrus-api/src/main/java/org/citrusframework/context/resolver/TypeAliasResolver.java @@ -16,16 +16,16 @@ package org.citrusframework.context.resolver; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ResourcePathTypeResolver; import org.citrusframework.spi.TypeResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + /** * Type resolver able to adapt an alias type to a given source type. Used in {@link org.citrusframework.spi.ReferenceResolver} to * auto resolve types that can act as an alias interchangeably to a given type. @@ -54,7 +54,7 @@ public interface TypeAliasResolver { resolvers.putAll(TYPE_RESOLVER.resolveAll("", TypeResolver.DEFAULT_TYPE_PROPERTY, "name")); if (logger.isDebugEnabled()) { - resolvers.forEach((k, v) -> logger.debug(String.format("Found type alias resolver '%s' as %s", k, v.getClass()))); + resolvers.forEach((k, v) -> logger.debug("Found type alias resolver '{}' as {}", k, v.getClass())); } } return resolvers; @@ -71,7 +71,7 @@ public interface TypeAliasResolver { try { return Optional.of(TYPE_RESOLVER.resolve(resolver)); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve type alias resolver from resource '%s/%s'", RESOURCE_PATH, resolver)); + logger.warn("Failed to resolve type alias resolver from resource '{}/{}'", RESOURCE_PATH, resolver); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/endpoint/DefaultEndpointFactory.java b/core/citrus-api/src/main/java/org/citrusframework/endpoint/DefaultEndpointFactory.java index daedca5ba0..d4b6a4b388 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/endpoint/DefaultEndpointFactory.java +++ b/core/citrus-api/src/main/java/org/citrusframework/endpoint/DefaultEndpointFactory.java @@ -16,12 +16,6 @@ package org.citrusframework.endpoint; -import java.lang.annotation.Annotation; -import java.util.Map; -import java.util.Optional; -import java.util.StringTokenizer; -import java.util.concurrent.ConcurrentHashMap; - import org.citrusframework.annotations.CitrusEndpoint; import org.citrusframework.annotations.CitrusEndpointConfig; import org.citrusframework.config.annotation.AnnotationConfigParser; @@ -31,11 +25,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.Optional; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; + /** * Default endpoint factory implementation uses registered endpoint components in Spring application context to create endpoint * from given endpoint uri. If endpoint bean name is given factory directly creates from application context. If endpoint uri is given * factory tries to find proper endpoint component in application context and in default endpoint component configuration. - * + *

* Default endpoint components are listed in property file reference where key is the component name and value is the fully qualified class name * of the implementing endpoint component class. * @@ -133,7 +133,7 @@ public Endpoint create(String uri, TestContext context) { synchronized (endpointCache) { if (endpointCache.containsKey(cachedEndpointName)) { if (logger.isDebugEnabled()) { - logger.debug(String.format("Found cached endpoint for uri '%s'", cachedEndpointName)); + logger.debug("Found cached endpoint for uri '{}'", cachedEndpointName); } return endpointCache.get(cachedEndpointName); } else { diff --git a/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointBuilder.java b/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointBuilder.java index 18ce48691c..9b889f161f 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointBuilder.java +++ b/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointBuilder.java @@ -16,12 +16,6 @@ package org.citrusframework.endpoint; -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.Properties; - import org.citrusframework.annotations.CitrusEndpoint; import org.citrusframework.annotations.CitrusEndpointProperty; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -33,6 +27,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; + /** * Endpoint builder interface. All endpoint builder implementations do implement this interface * in order to build endpoints using a fluent Java API. @@ -116,7 +116,7 @@ static Map> lookup() { Map> builders = new HashMap<>(TYPE_RESOLVER.resolveAll("", TypeResolver.TYPE_PROPERTY_WILDCARD)); if (logger.isDebugEnabled()) { - builders.forEach((k, v) -> logger.debug(String.format("Found endpoint builder '%s' as %s", k, v.getClass()))); + builders.forEach((k, v) -> logger.debug("Found endpoint builder '{}' as {}", k, v.getClass())); } return builders; } @@ -125,7 +125,7 @@ static Map> lookup() { * Resolves endpoint builder from resource path lookup with given resource name. Scans classpath for endpoint builder meta information * with given name and returns instance of the builder. Returns optional instead of throwing exception when no endpoint builder * could be found. - * + *

* Given builder name is a combination of resource file name and type property separated by '.' character. * @param builder * @return @@ -142,7 +142,7 @@ static Optional> lookup(String builder) { return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve endpoint builder from resource '%s/%s'", RESOURCE_PATH, builder)); + logger.warn("Failed to resolve endpoint builder from resource '{}/{}'", RESOURCE_PATH, builder); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointComponent.java b/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointComponent.java index 7ea4e2c07a..1edb12b7e0 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointComponent.java +++ b/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointComponent.java @@ -16,15 +16,15 @@ package org.citrusframework.endpoint; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ResourcePathTypeResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; +import java.util.Optional; + /** * Endpoint component registers with bean name in Spring application context and is then responsible to create proper endpoints dynamically from * endpoint uri values. Creates endpoint instance by parsing the dynamic endpoint uri with special properties and parameters. Creates proper endpoint @@ -75,7 +75,7 @@ static Map lookup() { Map components = TYPE_RESOLVER.resolveAll(); if (logger.isDebugEnabled()) { - components.forEach((k, v) -> logger.debug(String.format("Found endpoint component '%s' as %s", k, v.getClass()))); + components.forEach((k, v) -> logger.debug("Found endpoint component '{}' as {}", k, v.getClass())); } return components; } @@ -92,7 +92,7 @@ static Optional lookup(String component) { EndpointComponent instance = TYPE_RESOLVER.resolve(component); return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve endpoint component from resource '%s/%s'", RESOURCE_PATH, component)); + logger.warn("Failed to resolve endpoint component from resource '{}/{}'", RESOURCE_PATH, component); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointFactory.java b/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointFactory.java index 99f318f9c2..f003e05359 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointFactory.java +++ b/core/citrus-api/src/main/java/org/citrusframework/endpoint/EndpointFactory.java @@ -16,15 +16,15 @@ package org.citrusframework.endpoint; -import java.lang.annotation.Annotation; - import org.citrusframework.annotations.CitrusEndpoint; import org.citrusframework.context.TestContext; +import java.lang.annotation.Annotation; + /** * Endpoint factory tries to get endpoint instance by parsing an endpoint uri. Uri can have parameters * that get passed to the endpoint configuration. - * + *

* If Spring application context is given searches for matching endpoint component bean and delegates to component for * endpoint creation. * diff --git a/core/citrus-api/src/main/java/org/citrusframework/functions/Function.java b/core/citrus-api/src/main/java/org/citrusframework/functions/Function.java index bba58d286c..d0c25caabd 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/functions/Function.java +++ b/core/citrus-api/src/main/java/org/citrusframework/functions/Function.java @@ -16,15 +16,15 @@ package org.citrusframework.functions; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import org.citrusframework.context.TestContext; import org.citrusframework.spi.ResourcePathTypeResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * General function interface. * @@ -49,7 +49,7 @@ static Map lookup() { functions.putAll(new ResourcePathTypeResolver().resolveAll(RESOURCE_PATH)); if (logger.isDebugEnabled()) { - functions.forEach((k, v) -> logger.debug(String.format("Found function '%s' as %s", k, v.getClass()))); + functions.forEach((k, v) -> logger.debug("Found function '{}' as {}", k, v.getClass())); } } diff --git a/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionRegistry.java b/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionRegistry.java index 923464c833..4cfbf452bd 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionRegistry.java +++ b/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionRegistry.java @@ -16,12 +16,12 @@ package org.citrusframework.functions; -import java.util.ArrayList; -import java.util.List; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.NoSuchFunctionLibraryException; +import java.util.ArrayList; +import java.util.List; + /** * Function registry holding all available function libraries. * @@ -38,7 +38,7 @@ public class FunctionRegistry { * @return flag (true/false) */ public boolean isFunction(final String variableExpression) { - if (variableExpression == null || variableExpression.length() == 0) { + if (variableExpression == null || variableExpression.isEmpty()) { return false; } diff --git a/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionUtils.java b/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionUtils.java index 3b59c78980..fbfd138042 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionUtils.java +++ b/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionUtils.java @@ -98,7 +98,7 @@ public static String replaceFunctionsInString(final String stringValue, TestCont strBuffer.append(newString, startIndex, searchIndex); if (enableQuoting) { - strBuffer.append("'" + value + "'"); + strBuffer.append("'").append(value).append("'"); } else { strBuffer.append(value); } diff --git a/core/citrus-api/src/main/java/org/citrusframework/main/TestEngine.java b/core/citrus-api/src/main/java/org/citrusframework/main/TestEngine.java index 0a65db111b..5c4abb8370 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/main/TestEngine.java +++ b/core/citrus-api/src/main/java/org/citrusframework/main/TestEngine.java @@ -49,7 +49,7 @@ public interface TestEngine { static TestEngine lookup(TestRunConfiguration configuration) { try { TestEngine testEngine = TYPE_RESOLVER.resolve(configuration.getEngine(), configuration); - logger.debug(String.format("Using Citrus engine '%s' as %s", configuration.getEngine(), testEngine)); + logger.debug("Using Citrus engine '{}' as {}", configuration.getEngine(), testEngine); return testEngine; } catch (CitrusRuntimeException e) { throw new CitrusRuntimeException(String.format("Failed to resolve Citrus engine from resource '%s/%s'", RESOURCE_PATH, configuration.getEngine()), e); diff --git a/core/citrus-api/src/main/java/org/citrusframework/message/AbstractMessageProcessor.java b/core/citrus-api/src/main/java/org/citrusframework/message/AbstractMessageProcessor.java index 2370937d67..8f19623c0a 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/message/AbstractMessageProcessor.java +++ b/core/citrus-api/src/main/java/org/citrusframework/message/AbstractMessageProcessor.java @@ -38,7 +38,7 @@ public void process(Message message, TestContext context) { if (supportsMessageType(message.getType())) { processMessage(message, context); } else { - logger.debug(String.format("Message processor '%s' skipped for message type: %s", getName(), message.getType())); + logger.debug("Message processor '{}' skipped for message type: {}", getName(), message.getType()); } } diff --git a/core/citrus-api/src/main/java/org/citrusframework/message/ErrorHandlingStrategy.java b/core/citrus-api/src/main/java/org/citrusframework/message/ErrorHandlingStrategy.java index c87eef7f89..df2fc30842 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/message/ErrorHandlingStrategy.java +++ b/core/citrus-api/src/main/java/org/citrusframework/message/ErrorHandlingStrategy.java @@ -27,7 +27,7 @@ public enum ErrorHandlingStrategy { PROPAGATE("propagateError"); /** Name representation */ - private String name; + private final String name; /** * Default constructor using String name representation field. diff --git a/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaderType.java b/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaderType.java index db6f87ce80..33ecadd825 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaderType.java +++ b/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaderType.java @@ -38,15 +38,15 @@ public enum MessageHeaderType { public static final String TYPE_SUFFIX = "}:"; /** Properties */ - private String name; - private Class clazz; + private final String name; + private final Class clazz; /** * Default constructor using fields. * @param name * @param clazz */ - private MessageHeaderType(String name, Class clazz) { + MessageHeaderType(String name, Class clazz) { this.name = name; this.clazz = clazz; } diff --git a/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaderUtils.java b/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaderUtils.java index 46e4121d03..436cafc8d0 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaderUtils.java +++ b/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaderUtils.java @@ -35,7 +35,7 @@ private MessageHeaderUtils() { /** * Check if given header name belongs to Spring Integration internal headers. - * + *

* This is given if header name starts with internal header prefix or * matches one of Spring's internal header names. * diff --git a/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaders.java b/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaders.java index 919ec562c0..dfe85b7cb6 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaders.java +++ b/core/citrus-api/src/main/java/org/citrusframework/message/MessageHeaders.java @@ -37,6 +37,9 @@ private MessageHeaders() { /** Unique message id */ public static final String ID = MESSAGE_PREFIX + "id"; + /** Correlated message id */ + public static final String CORRELATED_MESSAGE_ID = MESSAGE_PREFIX + "correlated_message_id"; + /** Time message was created */ public static final String TIMESTAMP = MESSAGE_PREFIX + "timestamp"; diff --git a/core/citrus-api/src/main/java/org/citrusframework/message/MessagePayloadUtils.java b/core/citrus-api/src/main/java/org/citrusframework/message/MessagePayloadUtils.java index 09145650a7..3d06776ff0 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/message/MessagePayloadUtils.java +++ b/core/citrus-api/src/main/java/org/citrusframework/message/MessagePayloadUtils.java @@ -105,7 +105,7 @@ public static String prettyPrintXml(String payload) { if (nextStartElementPos > i + 1) { String textBetweenElements = s.substring(i + 1, nextStartElementPos); - if (textBetweenElements.replaceAll("\\s", "").length() == 0) { + if (textBetweenElements.replaceAll("\\s", "").isEmpty()) { sb.append(System.lineSeparator()); } else { sb.append(textBetweenElements.trim()); @@ -212,4 +212,38 @@ public static String prettyPrintJson(String payload) { } return sb.toString(); } + + /** + * Normalizes the given text by replacing all whitespace characters (identified by {@link Character#isWhitespace) by a single space + * and replacing windows style line endings with unix style line endings. + */ + public static String normalizeWhitespace(String text, boolean normalizeWhitespace, boolean normalizeLineEndingsToUnix) { + if (text == null || text.isEmpty()) { + return text; + } + + if (normalizeWhitespace) { + StringBuilder result = new StringBuilder(); + boolean lastWasSpace = true; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (Character.isWhitespace(c)) { + if (!lastWasSpace) { + result.append(' '); + } + lastWasSpace = true; + } else { + result.append(c); + lastWasSpace = false; + } + } + return result.toString().trim(); + } + + if (normalizeLineEndingsToUnix) { + return text.replaceAll("\\r(\\n)?", "\n"); + } + + return text; + } } diff --git a/core/citrus-api/src/main/java/org/citrusframework/message/MessageProcessor.java b/core/citrus-api/src/main/java/org/citrusframework/message/MessageProcessor.java index 5b84a59161..ec8e0edcc8 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/message/MessageProcessor.java +++ b/core/citrus-api/src/main/java/org/citrusframework/message/MessageProcessor.java @@ -16,8 +16,6 @@ package org.citrusframework.message; -import java.util.Optional; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ResourcePathTypeResolver; @@ -25,6 +23,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Optional; + /** * Processor performs operations on the given message. The processor is able to change message content such as payload and headers. */ @@ -52,7 +52,7 @@ static > Optional instance = TYPE_RESOLVER.resolve(processor); return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve message processor from resource '%s/%s'", RESOURCE_PATH, processor)); + logger.warn("Failed to resolve message processor from resource '{}/{}'", RESOURCE_PATH, processor); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/message/MessageSelector.java b/core/citrus-api/src/main/java/org/citrusframework/message/MessageSelector.java index 5a14220140..ed3ee99422 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/message/MessageSelector.java +++ b/core/citrus-api/src/main/java/org/citrusframework/message/MessageSelector.java @@ -16,15 +16,15 @@ package org.citrusframework.message; -import java.util.Map; - -import java.util.concurrent.ConcurrentHashMap; import org.citrusframework.context.TestContext; import org.citrusframework.spi.ResourcePathTypeResolver; import org.citrusframework.spi.TypeResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + @FunctionalInterface public interface MessageSelector { @@ -50,7 +50,7 @@ static Map lookup() { factories.putAll(TYPE_RESOLVER.resolveAll()); if (logger.isDebugEnabled()) { - factories.forEach((k, v) -> logger.debug(String.format("Found message selector '%s' as %s", k, v.getClass()))); + factories.forEach((k, v) -> logger.debug("Found message selector '{}' as {}", k, v.getClass())); } } diff --git a/core/citrus-api/src/main/java/org/citrusframework/message/ScriptPayloadBuilder.java b/core/citrus-api/src/main/java/org/citrusframework/message/ScriptPayloadBuilder.java index a5601fa874..8f877e6d5c 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/message/ScriptPayloadBuilder.java +++ b/core/citrus-api/src/main/java/org/citrusframework/message/ScriptPayloadBuilder.java @@ -16,14 +16,14 @@ package org.citrusframework.message; -import java.util.Optional; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ResourcePathTypeResolver; import org.citrusframework.spi.TypeResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Optional; + public interface ScriptPayloadBuilder extends MessagePayloadBuilder { /** Logger */ @@ -47,7 +47,7 @@ static Optional lookup(String type) { T instance = TYPE_RESOLVER.resolve(type); return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve script payload builder from resource '%s/%s'", RESOURCE_PATH, type)); + logger.warn("Failed to resolve script payload builder from resource '{}/{}'", RESOURCE_PATH, type); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/spi/BindToRegistry.java b/core/citrus-api/src/main/java/org/citrusframework/spi/BindToRegistry.java index 7e274637b2..8d5cb77f29 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/spi/BindToRegistry.java +++ b/core/citrus-api/src/main/java/org/citrusframework/spi/BindToRegistry.java @@ -24,7 +24,7 @@ /** * Used to bind an object to the Citrus context reference registry for dependency injection reasons. - * + *

* Object is bound with given name. In case no explicit name is given the registry will auto compute the name from given * Class name, method name or field name. * diff --git a/core/citrus-api/src/main/java/org/citrusframework/spi/ReferenceResolverAware.java b/core/citrus-api/src/main/java/org/citrusframework/spi/ReferenceResolverAware.java index 0e741bbde5..b2b97fe318 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/spi/ReferenceResolverAware.java +++ b/core/citrus-api/src/main/java/org/citrusframework/spi/ReferenceResolverAware.java @@ -16,12 +16,13 @@ package org.citrusframework.spi; +import jakarta.annotation.Nullable; + @FunctionalInterface public interface ReferenceResolverAware { /** * Sets the reference resolver. - * @param referenceResolver */ - void setReferenceResolver(ReferenceResolver referenceResolver); + void setReferenceResolver(@Nullable ReferenceResolver referenceResolver); } diff --git a/core/citrus-api/src/main/java/org/citrusframework/util/ReflectionHelper.java b/core/citrus-api/src/main/java/org/citrusframework/util/ReflectionHelper.java index aa9ca41871..d0ca407b16 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/util/ReflectionHelper.java +++ b/core/citrus-api/src/main/java/org/citrusframework/util/ReflectionHelper.java @@ -16,13 +16,16 @@ package org.citrusframework.util; +import jakarta.annotation.Nonnull; +import org.citrusframework.exceptions.CitrusRuntimeException; + import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; -import org.citrusframework.exceptions.CitrusRuntimeException; +import static java.lang.String.format; /** * Helper for working with reflection on classes. @@ -224,6 +227,7 @@ public static Object invokeMethod(Method method, Object target, Object... args) } } + @SuppressWarnings("java:S3011") public static void setField(Field f, Object instance, Object value) { try { if (!Modifier.isPublic(f.getModifiers()) && !f.canAccess(instance)) { @@ -235,6 +239,7 @@ public static void setField(Field f, Object instance, Object value) { } } + @SuppressWarnings("java:S3011") public static Object getField(Field f, Object instance) { try { if ((!Modifier.isPublic(f.getModifiers()) || @@ -253,4 +258,29 @@ public static Object getField(Field f, Object instance) { return null; } } + + /** + * Copies the values of all declared fields from a source object to a target object for the specified class. + */ + @SuppressWarnings("java:S3011") + public static void copyFields(@Nonnull Class clazz, @Nonnull Object source, @Nonnull Object target) { + Class currentClass = clazz; + + while (currentClass != null) { + Field[] fields = currentClass.getDeclaredFields(); + + for (Field field : fields) { + try { + field.setAccessible(true); + field.set(target, field.get(source)); + } catch (IllegalAccessException e) { + throw new CitrusRuntimeException(format( + "Unable to reflectively copy fields from source to target. clazz=%s sourceClass=%s targetClass=%s", + clazz, source.getClass(), target.getClass())); + } + } + + currentClass = currentClass.getSuperclass(); + } + } } diff --git a/core/citrus-api/src/main/java/org/citrusframework/util/TypeConverter.java b/core/citrus-api/src/main/java/org/citrusframework/util/TypeConverter.java index 3e25a57157..4e9b0d5617 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/util/TypeConverter.java +++ b/core/citrus-api/src/main/java/org/citrusframework/util/TypeConverter.java @@ -16,14 +16,14 @@ package org.citrusframework.util; -import java.util.HashMap; -import java.util.Map; - import org.citrusframework.CitrusSettings; import org.citrusframework.spi.ResourcePathTypeResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; +import java.util.Map; + public interface TypeConverter { /** Logger */ @@ -48,12 +48,12 @@ static Map lookup() { if (converters.isEmpty()) { converters.putAll(new ResourcePathTypeResolver().resolveAll(RESOURCE_PATH)); - if (converters.size() == 0) { + if (converters.isEmpty()) { converters.put(DEFAULT, DefaultTypeConverter.INSTANCE); } if (logger.isDebugEnabled()) { - converters.forEach((k, v) -> logger.debug(String.format("Found type converter '%s' as %s", k, v.getClass()))); + converters.forEach((k, v) -> logger.debug("Found type converter '{}' as {}", k, v.getClass())); } } @@ -64,7 +64,7 @@ static Map lookup() { * Lookup default type converter specified by resource path lookup and/or environment settings. In case only a single type converter is loaded * via resource path lookup this converter is used regardless of any environment settings. If there are multiple converter implementations * on the classpath the environment settings must specify the default. - * + *

* If no converter implementation is given via resource path lookup the default implementation is returned. * @return type converter to use by default. */ @@ -76,7 +76,7 @@ static TypeConverter lookupDefault() { * Lookup default type converter specified by resource path lookup and/or environment settings. In case only a single type converter is loaded * via resource path lookup this converter is used regardless of any environment settings. If there are multiple converter implementations * on the classpath the environment settings must specify the default. - * + *

* If no converter implementation is given via resource path lookup the default implementation is returned. * * @param defaultTypeConverter the fallback default converter @@ -89,20 +89,20 @@ static TypeConverter lookupDefault(TypeConverter defaultTypeConverter) { if (converters.size() == 1) { Map.Entry converterEntry = converters.entrySet().iterator().next(); if (logger.isDebugEnabled()) { - logger.debug(String.format("Using type converter '%s'", converterEntry.getKey())); + logger.debug("Using type converter '{}'", converterEntry.getKey()); } return converterEntry.getValue(); } else if (converters.containsKey(name)) { if (logger.isDebugEnabled()) { - logger.debug(String.format("Using type converter '%s'", name)); + logger.debug("Using type converter '{}'", name); } return converters.get(name); } if (!CitrusSettings.TYPE_CONVERTER_DEFAULT.equals(name)) { - logger.warn(String.format("Missing type converter for name '%s' - using default type converter", name)); + logger.warn("Missing type converter for name '{}' - using default type converter", name); } return defaultTypeConverter; diff --git a/core/citrus-api/src/main/java/org/citrusframework/validation/HeaderValidator.java b/core/citrus-api/src/main/java/org/citrusframework/validation/HeaderValidator.java index 1adf884c9c..e5eb3fccab 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/validation/HeaderValidator.java +++ b/core/citrus-api/src/main/java/org/citrusframework/validation/HeaderValidator.java @@ -16,10 +16,6 @@ package org.citrusframework.validation; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ResourcePathTypeResolver; @@ -28,6 +24,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + /** * @since 2.7.6 */ @@ -54,7 +54,7 @@ static Map lookup() { validators.putAll(TYPE_RESOLVER.resolveAll("", TypeResolver.DEFAULT_TYPE_PROPERTY, "name")); if (logger.isDebugEnabled()) { - validators.forEach((k, v) -> logger.debug(String.format("Found header validator '%s' as %s", k, v.getClass()))); + validators.forEach((k, v) -> logger.debug("Found header validator '{}' as {}", k, v.getClass())); } } return validators; @@ -72,7 +72,7 @@ static Optional lookup(String validator) { HeaderValidator instance = TYPE_RESOLVER.resolve(validator); return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve header validator from resource '%s/%s'", RESOURCE_PATH, validator)); + logger.warn("Failed to resolve header validator from resource '{}/{}'", RESOURCE_PATH, validator); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidator.java b/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidator.java index ca436bda24..2fab5c3c9f 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidator.java +++ b/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidator.java @@ -16,10 +16,6 @@ package org.citrusframework.validation; -import java.util.List; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.ValidationException; @@ -31,6 +27,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.Map; +import java.util.Optional; + /** * Message validator interface. Message validation need specific information like * control messages or validation scripts. These validation specific information is @@ -57,7 +57,7 @@ static Map> lookup() { Map> validators = TYPE_RESOLVER.resolveAll("", TypeResolver.DEFAULT_TYPE_PROPERTY, "name"); if (logger.isDebugEnabled()) { - validators.forEach((k, v) -> logger.debug(String.format("Found message validator '%s' as %s", k, v.getClass()))); + validators.forEach((k, v) -> logger.debug("Found message validator '{}' as {}", k, v.getClass())); } return validators; @@ -75,7 +75,7 @@ static Optional> lookup(String val MessageValidator instance = TYPE_RESOLVER.resolve(validator); return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve validator from resource '%s/%s'", RESOURCE_PATH, validator)); + logger.warn("Failed to resolve validator from resource '{}/{}'", RESOURCE_PATH, validator); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidatorRegistry.java b/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidatorRegistry.java index 7ba3d3abd4..d54c371799 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidatorRegistry.java +++ b/core/citrus-api/src/main/java/org/citrusframework/validation/MessageValidatorRegistry.java @@ -16,12 +16,6 @@ package org.citrusframework.validation; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.NoSuchMessageValidatorException; import org.citrusframework.message.Message; @@ -33,10 +27,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.lang.String.format; + /** * Simple registry holding all available message validator implementations. Test context can ask this registry for * matching validator implementation according to the message type (e.g. xml, json, csv, plaintext). - * + *

* Registry tries to find a matching validator for the message. * */ @@ -105,14 +107,17 @@ public List> findMessageValidators } } - if (isEmptyOrDefault(matchingValidators) && - (message.getPayload(String.class) == null || message.getPayload(String.class).isBlank())) { + if (isEmptyOrDefault(matchingValidators) + && (message.getPayload(String.class) == null || message.getPayload(String.class).isBlank())) { matchingValidators.add(defaultEmptyMessageValidator); } if (isEmptyOrDefault(matchingValidators)) { if (mustFindValidator) { - logger.warn(String.format("Unable to find proper message validator. Message type is '%s' and message payload is '%s'", messageType, message.getPayload(String.class))); + if (logger.isWarnEnabled()) { + logger.warn("Unable to find proper message validator. Message type is '{}' and message payload is '{}'", messageType, message.getPayload(String.class)); + } + throw new CitrusRuntimeException("Failed to find proper message validator for message"); } @@ -120,9 +125,7 @@ public List> findMessageValidators matchingValidators.add(defaultTextEqualsMessageValidator); } - if (logger.isDebugEnabled()) { - logger.debug(String.format("Found %s message validators for message", matchingValidators.size())); - } + logger.debug("Found {} message validators for message", matchingValidators.size()); return matchingValidators; } @@ -197,7 +200,7 @@ public MessageValidator getMessageValidator(String return this.messageValidators.get(name); } - throw new NoSuchMessageValidatorException(String.format("Unable to find message validator with name '%s'", name)); + throw new NoSuchMessageValidatorException(format("Unable to find message validator with name '%s'", name)); } /** @@ -207,7 +210,7 @@ public MessageValidator getMessageValidator(String */ public void addMessageValidator(String name, MessageValidator messageValidator) { if (this.messageValidators.containsKey(name) && logger.isDebugEnabled()) { - logger.debug(String.format("Overwriting message validator '%s' in registry", name)); + logger.debug("Overwriting message validator '{}' in registry", name); } this.messageValidators.put(name, messageValidator); @@ -220,7 +223,7 @@ public void addMessageValidator(String name, MessageValidator schemaValidator) { if (this.schemaValidators.containsKey(name) && logger.isDebugEnabled()) { - logger.debug(String.format("Overwriting message validator '%s' in registry", name)); + logger.debug("Overwriting message validator '{}' in registry", name); } this.schemaValidators.put(name, schemaValidator); @@ -230,8 +233,7 @@ public void addSchemaValidator(String name, SchemaValidator> messageValidators) { + public void setMessageValidators(Map> messageValidators) { this.messageValidators = messageValidators; } @@ -308,4 +310,11 @@ public Optional> findSchemaVa public void setSchemaValidators(Map> schemaValidators) { this.schemaValidators = schemaValidators; } + + /** + * Return all schema validators. + */ + public Map> getSchemaValidators() { + return schemaValidators; + } } diff --git a/core/citrus-api/src/main/java/org/citrusframework/validation/SchemaValidator.java b/core/citrus-api/src/main/java/org/citrusframework/validation/SchemaValidator.java index ecc2b006df..99549bee6a 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/validation/SchemaValidator.java +++ b/core/citrus-api/src/main/java/org/citrusframework/validation/SchemaValidator.java @@ -32,7 +32,7 @@ public interface SchemaValidator { /** Logger */ - Logger logger = LoggerFactory.getLogger(MessageValidator.class); + Logger logger = LoggerFactory.getLogger(SchemaValidator.class); /** Schema validator resource lookup path */ String RESOURCE_PATH = "META-INF/citrus/message/schemaValidator"; @@ -49,7 +49,7 @@ static Map> lookup() Map> validators = TYPE_RESOLVER.resolveAll("", TypeResolver.DEFAULT_TYPE_PROPERTY, "name"); if (logger.isDebugEnabled()) { - validators.forEach((k, v) -> logger.debug(String.format("Found message validator '%s' as %s", k, v.getClass()))); + validators.forEach((k, v) -> logger.debug("Found message validator '{}' as {}", k, v.getClass())); } return validators; @@ -67,7 +67,7 @@ static Optional> lookup(Strin SchemaValidator instance = TYPE_RESOLVER.resolve(validator); return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve validator from resource '%s/%s'", RESOURCE_PATH, validator)); + logger.warn("Failed to resolve validator from resource '{}/{}'", RESOURCE_PATH, validator); } return Optional.empty(); @@ -88,4 +88,16 @@ static Optional> lookup(Strin * @return true if the message/message type can be validated by this validator */ boolean supportsMessageType(String messageType, Message message); + + /** + * @param message the message which is subject of validation + * @param schemaValidationEnabled flag to indicate whether schema validation is explicitly enabled + * @return true, if the validator can validate the given message + */ + boolean canValidate(Message message, boolean schemaValidationEnabled); + + /** + * Validate the message against the given schemaRepository and schema. + */ + void validate(Message message, TestContext context, String schemaRepository, String schema); } diff --git a/core/citrus-api/src/main/java/org/citrusframework/validation/ValueMatcher.java b/core/citrus-api/src/main/java/org/citrusframework/validation/ValueMatcher.java index c95c793394..a186fa0d81 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/validation/ValueMatcher.java +++ b/core/citrus-api/src/main/java/org/citrusframework/validation/ValueMatcher.java @@ -16,10 +16,6 @@ package org.citrusframework.validation; -import java.util.Map; -import java.util.Optional; - -import java.util.concurrent.ConcurrentHashMap; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ResourcePathTypeResolver; @@ -27,10 +23,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + public interface ValueMatcher { /** Logger */ - Logger logger = LoggerFactory.getLogger(MessageValidator.class); + Logger logger = LoggerFactory.getLogger(ValueMatcher.class); /** Message validator resource lookup path */ String RESOURCE_PATH = "META-INF/citrus/value/matcher"; @@ -50,7 +50,7 @@ static Map lookup() { validators.putAll(TYPE_RESOLVER.resolveAll()); if (logger.isDebugEnabled()) { - validators.forEach((k, v) -> logger.debug(String.format("Found validator '%s' as %s", k, v.getClass()))); + validators.forEach((k, v) -> logger.debug("Found validator '{}' as {}", k, v.getClass())); } } return validators; @@ -68,7 +68,7 @@ static Optional lookup(String validator) { ValueMatcher instance = TYPE_RESOLVER.resolve(validator); return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve value matcher from resource '%s/%s'", RESOURCE_PATH, validator)); + logger.warn("Failed to resolve value matcher from resource '{}/{}'", RESOURCE_PATH, validator); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/validation/context/ValidationContext.java b/core/citrus-api/src/main/java/org/citrusframework/validation/context/ValidationContext.java index 41f85fad0f..b90ea6b2a3 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/validation/context/ValidationContext.java +++ b/core/citrus-api/src/main/java/org/citrusframework/validation/context/ValidationContext.java @@ -22,6 +22,14 @@ */ public interface ValidationContext { + /** + * Indicates whether this validation context requires a validator. + * @return true if a validator is required; false otherwise. + */ + default boolean requiresValidator() { + return false; + } + /** * Fluent builder * @param context type diff --git a/core/citrus-api/src/main/java/org/citrusframework/validation/matcher/DefaultControlExpressionParser.java b/core/citrus-api/src/main/java/org/citrusframework/validation/matcher/DefaultControlExpressionParser.java index a967eea9ae..6c99193476 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/validation/matcher/DefaultControlExpressionParser.java +++ b/core/citrus-api/src/main/java/org/citrusframework/validation/matcher/DefaultControlExpressionParser.java @@ -16,11 +16,11 @@ package org.citrusframework.validation.matcher; +import org.citrusframework.exceptions.CitrusRuntimeException; + import java.util.ArrayList; import java.util.List; -import org.citrusframework.exceptions.CitrusRuntimeException; - /** * Default implementation of control expression parser. * @since 2.5 @@ -34,7 +34,7 @@ public List extractControlValues(String controlExpression, Character del if (controlExpression != null && !controlExpression.isBlank()) { extractParameters(controlExpression, useDelimiter, extractedParameters, 0); - if (extractedParameters.size() == 0) { + if (extractedParameters.isEmpty()) { // if the controlExpression has text but no parameters were extracted, then assume that // the controlExpression itself is the only parameter extractedParameters.add(controlExpression); diff --git a/core/citrus-api/src/main/java/org/citrusframework/validation/matcher/ValidationMatcher.java b/core/citrus-api/src/main/java/org/citrusframework/validation/matcher/ValidationMatcher.java index 19dcaecd85..731f92d91e 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/validation/matcher/ValidationMatcher.java +++ b/core/citrus-api/src/main/java/org/citrusframework/validation/matcher/ValidationMatcher.java @@ -16,16 +16,16 @@ package org.citrusframework.validation.matcher; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.ValidationException; import org.citrusframework.spi.ResourcePathTypeResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * General validation matcher interface. * @@ -51,7 +51,7 @@ static Map lookup() { matcher.putAll(new ResourcePathTypeResolver().resolveAll(RESOURCE_PATH)); if (logger.isDebugEnabled()) { - matcher.forEach((k, v) -> logger.debug(String.format("Found validation matcher '%s' as %s", k, v.getClass()))); + matcher.forEach((k, v) -> logger.debug("Found validation matcher '{}' as {}", k, v.getClass())); } } return matcher; diff --git a/core/citrus-api/src/main/java/org/citrusframework/variable/SegmentVariableExtractor.java b/core/citrus-api/src/main/java/org/citrusframework/variable/SegmentVariableExtractor.java index d1fe6c8b8c..ce32fe3c06 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/variable/SegmentVariableExtractor.java +++ b/core/citrus-api/src/main/java/org/citrusframework/variable/SegmentVariableExtractor.java @@ -20,7 +20,6 @@ /** * Class extracting values of segments of VariableExpressions. - * */ public interface SegmentVariableExtractor { diff --git a/core/citrus-api/src/main/java/org/citrusframework/variable/SegmentVariableExtractorRegistry.java b/core/citrus-api/src/main/java/org/citrusframework/variable/SegmentVariableExtractorRegistry.java index b21f149618..3bb7742ce2 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/variable/SegmentVariableExtractorRegistry.java +++ b/core/citrus-api/src/main/java/org/citrusframework/variable/SegmentVariableExtractorRegistry.java @@ -16,14 +16,6 @@ package org.citrusframework.variable; -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ResourcePathTypeResolver; @@ -32,17 +24,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + /** * Simple registry holding all available segment variable extractor implementations. Test context can ask this registry for * the extractors managed by this registry in order to access variable content from the TestContext expressed by variable expressions. *

* Registry provides all known {@link SegmentVariableExtractor}s. - * */ public class SegmentVariableExtractorRegistry { /** Logger */ - private static final Logger logger = LoggerFactory.getLogger(SegmentVariableExtractor.class); + private static final Logger logger = LoggerFactory.getLogger(SegmentVariableExtractorRegistry.class); /** Segment variable extractor resource lookup path */ private static final String RESOURCE_PATH = "META-INF/citrus/variable/extractor/segment"; @@ -61,7 +60,7 @@ static Collection lookup() { Map extractors = TYPE_RESOLVER.resolveAll(); return extractors.values(); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve segment variable extractor from resource '%s'", RESOURCE_PATH)); + logger.warn("Failed to resolve segment variable extractor from resource '{}'", RESOURCE_PATH); } return Collections.emptyList(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/variable/VariableExpressionIterator.java b/core/citrus-api/src/main/java/org/citrusframework/variable/VariableExpressionIterator.java index fe6b08b8b6..01dc2b02bd 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/variable/VariableExpressionIterator.java +++ b/core/citrus-api/src/main/java/org/citrusframework/variable/VariableExpressionIterator.java @@ -39,7 +39,6 @@ *

  • the third element of the persons property of the variable retrieved in the previous step
  • *
  • the first element of the firstnames property of the property retrieved in the previous step
  • * - * */ public class VariableExpressionIterator implements Iterator { diff --git a/core/citrus-api/src/main/java/org/citrusframework/variable/VariableExtractor.java b/core/citrus-api/src/main/java/org/citrusframework/variable/VariableExtractor.java index 67c3c50a68..ef3792bfff 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/variable/VariableExtractor.java +++ b/core/citrus-api/src/main/java/org/citrusframework/variable/VariableExtractor.java @@ -16,8 +16,6 @@ package org.citrusframework.variable; -import java.util.Optional; - import org.citrusframework.builder.WithExpressions; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -28,6 +26,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Optional; + /** * Class extracting variables form messages. Implementing classes may read * message contents and save those to test variables. @@ -56,7 +56,7 @@ static > Optional instance = TYPE_RESOLVER.resolve(extractor); return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve variable extractor from resource '%s/%s'", RESOURCE_PATH, extractor)); + logger.warn("Failed to resolve variable extractor from resource '{}/{}'", RESOURCE_PATH, extractor); } return Optional.empty(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/variable/VariableUtils.java b/core/citrus-api/src/main/java/org/citrusframework/variable/VariableUtils.java index 55a2cb329c..98fc6e8bb8 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/variable/VariableUtils.java +++ b/core/citrus-api/src/main/java/org/citrusframework/variable/VariableUtils.java @@ -16,15 +16,15 @@ package org.citrusframework.variable; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; - import org.citrusframework.CitrusSettings; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.NoSuchVariableException; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + /** * Utility class manipulating test variables. * @@ -118,15 +118,11 @@ public static String cutOffVariablesEscaping(String variable) { * @return flag true/false */ public static boolean isVariableName(final String expression) { - if (expression == null || expression.length() == 0) { + if (expression == null || expression.isEmpty()) { return false; } - if (expression.startsWith(CitrusSettings.VARIABLE_PREFIX) && expression.endsWith(CitrusSettings.VARIABLE_SUFFIX)) { - return true; - } - - return false; + return expression.startsWith(CitrusSettings.VARIABLE_PREFIX) && expression.endsWith(CitrusSettings.VARIABLE_SUFFIX); } /** @@ -176,7 +172,7 @@ public static String replaceVariablesInString(final String str, TestContext cont final String value = context.getVariable(variableNameBuf.toString()); if (value == null) { - throw new NoSuchVariableException("Variable: " + variableNameBuf.toString() + " could not be found"); + throw new NoSuchVariableException("Variable: " + variableNameBuf + " could not be found"); } newStr.append(str, startIndex, searchIndex); diff --git a/core/citrus-api/src/main/java/org/citrusframework/variable/dictionary/DataDictionary.java b/core/citrus-api/src/main/java/org/citrusframework/variable/dictionary/DataDictionary.java index 29093b6e1a..a96f52f935 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/variable/dictionary/DataDictionary.java +++ b/core/citrus-api/src/main/java/org/citrusframework/variable/dictionary/DataDictionary.java @@ -26,7 +26,7 @@ * Data dictionary interface describes a mechanism to modify message content (payload) with global dictionary elements. * Dictionary translates element values to those defined in dictionary. Message construction process is aware of dictionaries * in Spring application context so user just has to add dictionary implementation to application context. - * + *

    * Dictionary takes part in message construction for inbound and outbound messages in Citrus. * @since 1.4 */ @@ -59,6 +59,6 @@ public interface DataDictionary extends MessageProcessor, MessageDirectionAwa enum PathMappingStrategy { EXACT, ENDS_WITH, - STARTS_WITH; + STARTS_WITH } } diff --git a/core/citrus-api/src/main/java/org/citrusframework/xml/namespace/NamespaceContextBuilder.java b/core/citrus-api/src/main/java/org/citrusframework/xml/namespace/NamespaceContextBuilder.java index a4a7dae971..744023946a 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/xml/namespace/NamespaceContextBuilder.java +++ b/core/citrus-api/src/main/java/org/citrusframework/xml/namespace/NamespaceContextBuilder.java @@ -16,18 +16,18 @@ package org.citrusframework.xml.namespace; +import org.citrusframework.message.Message; + +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; -import javax.xml.XMLConstants; -import javax.xml.namespace.NamespaceContext; - -import org.citrusframework.message.Message; /** * Builds a namespace context for XPath expression evaluations. Builder supports default mappings * as well as dynamic mappings from received message. - * + *

    * Namespace mappings are defined as key value pairs where key is definded as namespace prefix and value is the * actual namespace uri. * @@ -50,7 +50,7 @@ public NamespaceContext buildContext(Message receivedMessage, Map 0) { + if (!namespaceMappings.isEmpty()) { defaultNamespaceContext.addNamespaces(namespaceMappings); } diff --git a/core/citrus-api/src/test/java/org/citrusframework/message/MessagePayloadUtilsTest.java b/core/citrus-api/src/test/java/org/citrusframework/message/MessagePayloadUtilsTest.java index 369385adc4..e6b33b73a9 100644 --- a/core/citrus-api/src/test/java/org/citrusframework/message/MessagePayloadUtilsTest.java +++ b/core/citrus-api/src/test/java/org/citrusframework/message/MessagePayloadUtilsTest.java @@ -16,56 +16,78 @@ package org.citrusframework.message; -import org.testng.Assert; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + public class MessagePayloadUtilsTest { @Test public void shouldPrettyPrintJson() { - Assert.assertEquals(MessagePayloadUtils.prettyPrint(""), ""); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("{}"), "{}"); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("[]"), "[]"); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\"}"), + assertEquals(MessagePayloadUtils.prettyPrint(""), ""); + assertEquals(MessagePayloadUtils.prettyPrint("{}"), "{}"); + assertEquals(MessagePayloadUtils.prettyPrint("[]"), "[]"); + assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\"}"), String.format("{%n \"user\": \"citrus\"%n}")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("{\"text\":\"\"}"), + assertEquals(MessagePayloadUtils.prettyPrint("{\"text\":\"\"}"), String.format("{%n \"text\": \"\"%n}")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint(String.format("%n%n { \"user\":%n%n \"citrus\" }")), + assertEquals(MessagePayloadUtils.prettyPrint(String.format("%n%n { \"user\":%n%n \"citrus\" }")), String.format("{%n \"user\": \"citrus\"%n}")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\",\"age\": 32}"), + assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\",\"age\": 32}"), String.format("{%n \"user\": \"citrus\",%n \"age\": 32%n}")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("[22, 32]"), + assertEquals(MessagePayloadUtils.prettyPrint("[22, 32]"), String.format("[%n22,%n32%n]")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("[{\"user\":\"citrus\",\"age\": 32}]"), + assertEquals(MessagePayloadUtils.prettyPrint("[{\"user\":\"citrus\",\"age\": 32}]"), String.format("[%n {%n \"user\": \"citrus\",%n \"age\": 32%n }%n]")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("[{\"user\":\"citrus\",\"age\": 32}, {\"user\":\"foo\",\"age\": 99}]"), + assertEquals(MessagePayloadUtils.prettyPrint("[{\"user\":\"citrus\",\"age\": 32}, {\"user\":\"foo\",\"age\": 99}]"), String.format("[%n {%n \"user\": \"citrus\",%n \"age\": 32%n },%n {%n \"user\": \"foo\",%n \"age\": 99%n }%n]")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\",\"age\": 32,\"pet\":{\"name\": \"fluffy\", \"age\": 4}}"), + assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\",\"age\": 32,\"pet\":{\"name\": \"fluffy\", \"age\": 4}}"), String.format("{%n \"user\": \"citrus\",%n \"age\": 32,%n \"pet\": {%n \"name\": \"fluffy\",%n \"age\": 4%n }%n}")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\",\"age\": 32,\"pets\":[\"fluffy\",\"hasso\"]}"), + assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\",\"age\": 32,\"pets\":[\"fluffy\",\"hasso\"]}"), String.format("{%n \"user\": \"citrus\",%n \"age\": 32,%n \"pets\": [%n \"fluffy\",%n \"hasso\"%n ]%n}")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\",\"pets\":[\"fluffy\",\"hasso\"],\"age\": 32}"), + assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\",\"pets\":[\"fluffy\",\"hasso\"],\"age\": 32}"), String.format("{%n \"user\": \"citrus\",%n \"pets\": [%n \"fluffy\",%n \"hasso\"%n ],%n \"age\": 32%n}")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\",\"pets\":[{\"name\": \"fluffy\", \"age\": 4},{\"name\": \"hasso\", \"age\": 2}],\"age\": 32}"), + assertEquals(MessagePayloadUtils.prettyPrint("{\"user\":\"citrus\",\"pets\":[{\"name\": \"fluffy\", \"age\": 4},{\"name\": \"hasso\", \"age\": 2}],\"age\": 32}"), String.format("{%n \"user\": \"citrus\",%n \"pets\": [%n {%n \"name\": \"fluffy\",%n \"age\": 4%n },%n {%n \"name\": \"hasso\",%n \"age\": 2%n }%n ],%n \"age\": 32%n}")); } @Test public void shouldPrettyPrintXml() { - Assert.assertEquals(MessagePayloadUtils.prettyPrint(""), ""); - Assert.assertEquals(MessagePayloadUtils.prettyPrint(""), + assertEquals(MessagePayloadUtils.prettyPrint(""), ""); + assertEquals(MessagePayloadUtils.prettyPrint(""), String.format("%n%n")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("Citrus rocks!"), + assertEquals(MessagePayloadUtils.prettyPrint("Citrus rocks!"), String.format("%n Citrus rocks!%n%n")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint("Citrus rocks!"), + assertEquals(MessagePayloadUtils.prettyPrint("Citrus rocks!"), String.format("%n%n Citrus rocks!%n%n")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint(String.format("%n%nCitrus rocks!%n%n")), + assertEquals(MessagePayloadUtils.prettyPrint(String.format("%n%nCitrus rocks!%n%n")), String.format("%n Citrus rocks!%n%n")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint(String.format("%n %nCitrus rocks!%n %n")), + assertEquals(MessagePayloadUtils.prettyPrint(String.format("%n %nCitrus rocks!%n %n")), String.format("%n Citrus rocks!%n%n")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint(String.format("%n%n ")), + assertEquals(MessagePayloadUtils.prettyPrint(String.format("%n%n ")), String.format("%n %n %n %n%n")); - Assert.assertEquals(MessagePayloadUtils.prettyPrint(String.format("")), + assertEquals(MessagePayloadUtils.prettyPrint(String.format("")), String.format("%n %n %n %n%n")); } + + @DataProvider + public Object[][] normalizeWhitespaceProvider() { + return new Object[][] { + // Test data: payload, ignoreWhitespace, ignoreNewLineType, expected result + {"Hello \t\r\nWorld\r\n", true, true, "Hello World"}, + {"Hello \t\r\nWorld\r\n", true, false, "Hello World"}, + {"Hello \t\r\nWorld\r\n", false, true, "Hello \t\nWorld\n"}, + {"Hello \t\r\nWorld\r\n", false, false, "Hello \t\r\nWorld\r\n"}, + {"", true, true, ""}, + {"", false, false, ""}, + {null, true, true, null}, + {null, false, false, null} + }; + } + + @Test(dataProvider = "normalizeWhitespaceProvider") + public void normalizeWhitespace(String text, boolean normalizeWhitespace, boolean normalizeLineEndingsToUnix, String expected) { + assertEquals(MessagePayloadUtils.normalizeWhitespace(text, normalizeWhitespace, normalizeLineEndingsToUnix), expected); + } } diff --git a/core/citrus-api/src/test/java/org/citrusframework/util/ReflectionHelperTest.java b/core/citrus-api/src/test/java/org/citrusframework/util/ReflectionHelperTest.java new file mode 100644 index 0000000000..b31387a0f1 --- /dev/null +++ b/core/citrus-api/src/test/java/org/citrusframework/util/ReflectionHelperTest.java @@ -0,0 +1,47 @@ +package org.citrusframework.util; + +import org.testng.Assert; +import org.testng.annotations.Test; + +public class ReflectionHelperTest { + + @Test + public void copyFields() { + SubClass source = new SubClass("John Doe", 30, "Super John", 60); + + SubClass target = new SubClass(null, 0, null, 0); + + ReflectionHelper.copyFields(SubClass.class, source, target); + + Assert.assertEquals(target.name, "John Doe"); + Assert.assertEquals(target.age, 30); + + Assert.assertEquals(target.superName, "Super John"); + Assert.assertEquals(target.superAge, 60); + + } + + private static class SuperClass { + + String superName; + int superAge; + + public SuperClass(String superName, int superAge) { + this.superName = superName; + this.superAge = superAge; + } + } + + private static class SubClass extends SuperClass { + + String name; + int age; + + public SubClass(String name, int age, String superName, int superAge) { + super(superName, superAge); + this.name = name; + this.age = age; + } + } + +} diff --git a/core/citrus-base/pom.xml b/core/citrus-base/pom.xml index c7e349aa0a..959f7acf01 100644 --- a/core/citrus-base/pom.xml +++ b/core/citrus-base/pom.xml @@ -28,12 +28,16 @@ commons-codec commons-codec - jakarta.xml.bind jakarta.xml.bind-api provided + + com.github.mifmif + generex + 1.0.2 + diff --git a/core/citrus-base/src/main/java/org/citrusframework/AbstractTestContainerBuilder.java b/core/citrus-base/src/main/java/org/citrusframework/AbstractTestContainerBuilder.java index 09f66d81e1..e0740fc7c1 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/AbstractTestContainerBuilder.java +++ b/core/citrus-base/src/main/java/org/citrusframework/AbstractTestContainerBuilder.java @@ -16,15 +16,15 @@ package org.citrusframework; +import org.citrusframework.container.TestActionContainer; +import org.citrusframework.spi.ReferenceResolver; +import org.citrusframework.spi.ReferenceResolverAware; + import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.citrusframework.container.TestActionContainer; -import org.citrusframework.spi.ReferenceResolver; -import org.citrusframework.spi.ReferenceResolverAware; - /** * Abstract container builder takes care on calling the container runner when actions are placed in the container. */ diff --git a/core/citrus-base/src/main/java/org/citrusframework/CitrusContextProvider.java b/core/citrus-base/src/main/java/org/citrusframework/CitrusContextProvider.java index 33c19afd8d..2a57de23b8 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/CitrusContextProvider.java +++ b/core/citrus-base/src/main/java/org/citrusframework/CitrusContextProvider.java @@ -16,15 +16,15 @@ package org.citrusframework; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ResourcePathTypeResolver; import org.citrusframework.spi.TypeResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; +import java.util.Optional; + @FunctionalInterface public interface CitrusContextProvider { @@ -85,7 +85,7 @@ static Optional lookup(String name) { CitrusContextProvider instance = TYPE_RESOLVER.resolve(name); return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve Citrus context provider from resource '%s/%s'", RESOURCE_PATH, name)); + logger.warn("Failed to resolve Citrus context provider from resource '{}/{}'", RESOURCE_PATH, name); } return Optional.empty(); diff --git a/core/citrus-base/src/main/java/org/citrusframework/TestCaseRunnerFactory.java b/core/citrus-base/src/main/java/org/citrusframework/TestCaseRunnerFactory.java index 4e9b1a9b9b..074a029f02 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/TestCaseRunnerFactory.java +++ b/core/citrus-base/src/main/java/org/citrusframework/TestCaseRunnerFactory.java @@ -25,7 +25,7 @@ * Citrus' built-in runner, but it also offers the flexibility to replace the default runner with a * custom implementation. To do this, it leverages the Citrus {@link ResourcePathTypeResolver} * mechanism. - * + *

    * To provide a custom runner, the following file needs to be added to the classpath: *

    * diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/AntRunAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/AntRunAction.java index de79a038c6..44aa241e4c 100755 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/AntRunAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/AntRunAction.java @@ -16,12 +16,6 @@ package org.citrusframework.actions; -import java.io.IOException; -import java.io.PrintStream; -import java.util.Map.Entry; -import java.util.Properties; -import java.util.Stack; - import org.apache.tools.ant.BuildException; import org.apache.tools.ant.BuildListener; import org.apache.tools.ant.DefaultLogger; @@ -38,10 +32,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Stack; + /** * Action calls Apache ANT with given build file and runs ANT targets * as separate build. User can set and overwrite properties for the build. - * + *

    * Build logging output is forwarded to test run logger. * * @since 1.3 @@ -95,7 +95,7 @@ public void doExecute(TestContext context) { for (Entry entry : properties.entrySet()) { String propertyValue = entry.getValue() != null ? context.replaceDynamicContentInString(entry.getValue().toString()) : ""; - logger.debug("Set build property: " + entry.getKey() + "=" + propertyValue); + logger.debug("Set build property: {}={}", entry.getKey(), propertyValue); project.setProperty(entry.getKey().toString(), propertyValue); } @@ -109,20 +109,20 @@ public void doExecute(TestContext context) { project.addBuildListener(consoleLogger); - logger.info("Executing ANT build: " + buildFileResource); + logger.info("Executing ANT build: {}", buildFileResource); if (StringUtils.hasText(targets)) { - logger.info("Executing ANT targets: " + targets); + logger.info("Executing ANT targets: {}", targets); project.executeTargets(parseTargets()); } else { - logger.info("Executing ANT target: " + target); + logger.info("Executing ANT target: {}", target); project.executeTarget(target); } } catch (BuildException e) { throw new CitrusRuntimeException("Failed to run ANT build file", e); } - logger.info("Executed ANT build: " + buildFileResource); + logger.info("Executed ANT build: {}", buildFileResource); } private static DefaultLogger getDefaultConsoleLogger() { @@ -166,7 +166,7 @@ private Stack parseTargets() { private void loadBuildPropertyFile(Project project, TestContext context) { if (StringUtils.hasText(propertyFilePath)) { String propertyFileResource = context.replaceDynamicContentInString(propertyFilePath); - logger.info("Reading build property file: " + propertyFileResource); + logger.info("Reading build property file: {}", propertyFileResource); Properties fileProperties = new Properties(); try { Resource propertyResource = Resources.fromClasspath(propertyFileResource); @@ -174,10 +174,7 @@ private void loadBuildPropertyFile(Project project, TestContext context) { for (Entry entry : fileProperties.entrySet()) { String propertyValue = entry.getValue() != null ? context.replaceDynamicContentInString(entry.getValue().toString()) : ""; - - if (logger.isDebugEnabled()) { - logger.debug("Set build property from file resource: " + entry.getKey() + "=" + propertyValue); - } + logger.debug("Set build property from file resource: {}={}", entry.getKey(), propertyValue); project.setProperty(entry.getKey().toString(), propertyValue); } } catch (IOException e) { diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/CreateVariablesAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/CreateVariablesAction.java index 2788d24009..8f87eefdd3 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/CreateVariablesAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/CreateVariablesAction.java @@ -16,16 +16,16 @@ package org.citrusframework.actions; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Map.Entry; - import org.citrusframework.AbstractTestActionBuilder; import org.citrusframework.context.TestContext; import org.citrusframework.variable.VariableUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + /** * Action creating new test variables during a test. Existing test variables are overwritten * by new values. @@ -64,7 +64,7 @@ public void doExecute(TestContext context) { //check if value is variable or function (and resolve it if yes) value = context.replaceDynamicContentInString(value); - logger.info("Setting variable: " + key + " to value: " + value); + logger.info("Setting variable: {} to value: {}", key, value); context.setVariable(key, value); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/EchoAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/EchoAction.java index 357b9ec178..930c7cab79 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/EchoAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/EchoAction.java @@ -16,13 +16,13 @@ package org.citrusframework.actions; -import java.util.Date; - import org.citrusframework.AbstractTestActionBuilder; import org.citrusframework.context.TestContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Date; + /** * Prints messages to the console/logger during test execution. * @@ -49,7 +49,7 @@ private EchoAction(Builder builder) { @Override public void doExecute(TestContext context) { if (message == null) { - logger.info("Citrus test " + new Date(System.currentTimeMillis())); + logger.info("Citrus test {}", new Date(System.currentTimeMillis())); } else { logger.info(context.getLogModifier().mask(context.replaceDynamicContentInString(message))); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/InputAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/InputAction.java index 24e85d8661..34228e74c4 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/InputAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/InputAction.java @@ -16,6 +16,13 @@ package org.citrusframework.actions; +import org.citrusframework.AbstractTestActionBuilder; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -24,17 +31,10 @@ import java.util.StringTokenizer; import java.util.stream.Stream; -import org.citrusframework.AbstractTestActionBuilder; -import org.citrusframework.context.TestContext; -import org.citrusframework.exceptions.CitrusRuntimeException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.citrusframework.util.StringUtils; - /** * Test action prompts user data from standard input stream. The input data is then stored as new * test variable. Test workflow stops until user input is complete. - * + *

    * Action can declare a set of valid answers, so user will be prompted until a valid * answer was returned. * @@ -77,7 +77,7 @@ public void doExecute(TestContext context) { if (context.getVariables().containsKey(variable)) { input = context.getVariable(variable); - logger.info("Variable " + variable + " is already set (='" + input + "'). Skip waiting for user input"); + logger.info("Variable {} is already set (='{}'). Skip waiting for user input", variable, input); return; } @@ -123,7 +123,7 @@ private boolean checkAnswer(String input) { } } - logger.info("User input is not valid - must be one of " + validAnswers); + logger.info("User input is not valid - must be one of {}", validAnswers); return false; } diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/JavaAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/JavaAction.java index a8009c3fb9..39c6c22617 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/JavaAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/JavaAction.java @@ -16,14 +16,6 @@ package org.citrusframework.actions; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - import org.citrusframework.AbstractTestActionBuilder; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -32,6 +24,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + /** * Action to enable class invocation through java reflection * @@ -125,7 +125,7 @@ private void invokeMethod(Object instance, Class[] methodTypes, Object[] meth Arrays.stream(methodTypes).map(Class::getSimpleName).collect(Collectors.joining(",")) + ")' for class '" + instance.getClass() + "'"); } - logger.info("Invoking method '" + methodToRun.toString() + "' on instance '" + instance.getClass() + "'"); + logger.info("Invoking method '{}' on instance '{}'", methodToRun, instance.getClass()); methodToRun.invoke(instance, methodObjects); } @@ -152,7 +152,7 @@ private Object getObjectInstanceFromClass(TestContext context) throws ClassNotFo "is set for Java reflection call"); } - logger.info("Instantiating class for name '" + className + "'"); + logger.info("Instantiating class for name '{}'", className); Class classToRun = Class.forName(className); diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/LoadPropertiesAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/LoadPropertiesAction.java index 29f989a8d3..70f340e33b 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/LoadPropertiesAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/LoadPropertiesAction.java @@ -16,11 +16,6 @@ package org.citrusframework.actions; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Properties; - import org.citrusframework.AbstractTestActionBuilder; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -29,6 +24,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + /** * Action reads property files and creates test variables for every property entry. File * resource path can define a resource located on classpath or file system. @@ -55,9 +55,7 @@ public LoadPropertiesAction(Builder builder) { public void doExecute(TestContext context) { Resource resource = FileUtils.getFileResource(filePath, context); - if (logger.isDebugEnabled()) { - logger.debug("Reading property file " + FileUtils.getFileName(resource.getLocation())); - } + logger.debug("Reading property file {}", FileUtils.getFileName(resource.getLocation())); Properties props = FileUtils.loadAsProperties(resource); @@ -65,13 +63,10 @@ public void doExecute(TestContext context) { for (Entry entry : props.entrySet()) { String key = entry.getKey().toString(); - if (logger.isDebugEnabled()) { - logger.debug("Loading property: " + key + "=" + props.getProperty(key) + " into variables"); - } + logger.debug("Loading property: {}={} into variables", key, props.getProperty(key)); if (logger.isDebugEnabled() && context.getVariables().containsKey(key)) { - logger.debug("Overwriting property " + key + " old value:" + context.getVariable(key) - + " new value:" + props.getProperty(key)); + logger.debug("Overwriting property {} old value:{} new value:{}", key, context.getVariable(key), props.getProperty(key)); } try { @@ -83,7 +78,7 @@ public void doExecute(TestContext context) { context.resolveDynamicValuesInMap(unresolved).forEach(context::setVariable); - logger.info("Loaded property file " + FileUtils.getFileName(resource.getLocation())); + logger.info("Loaded property file {}", FileUtils.getFileName(resource.getLocation())); } /** diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/PurgeEndpointAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/PurgeEndpointAction.java index b01217db4f..bb7424e0eb 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/PurgeEndpointAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/PurgeEndpointAction.java @@ -85,9 +85,7 @@ public PurgeEndpointAction(Builder builder) { @Override public void doExecute(TestContext context) { - if (logger.isDebugEnabled()) { - logger.debug("Purging message endpoints ..."); - } + logger.debug("Purging message endpoints ..."); for (Endpoint endpoint : endpoints) { purgeEndpoint(endpoint, context); @@ -108,9 +106,7 @@ public void doExecute(TestContext context) { * @param context */ private void purgeEndpoint(Endpoint endpoint, TestContext context) { - if (logger.isDebugEnabled()) { - logger.debug("Try to purge message endpoint " + endpoint.getName()); - } + logger.debug("Try to purge message endpoint {}", endpoint.getName()); int messagesPurged = 0; Consumer messageConsumer = endpoint.createConsumer(); @@ -124,14 +120,12 @@ private void purgeEndpoint(Endpoint endpoint, TestContext context) { message = (receiveTimeout >= 0) ? messageConsumer.receive(context, receiveTimeout) : messageConsumer.receive(context); } } catch (ActionTimeoutException e) { - if (logger.isDebugEnabled()) { - logger.debug("Stop purging due to timeout - " + e.getMessage()); - } + logger.debug("Stop purging due to timeout - {}", e.getMessage()); break; } if (message != null) { - logger.debug("Removed message from endpoint " + endpoint.getName()); + logger.debug("Removed message from endpoint {}", endpoint.getName()); messagesPurged++; try { @@ -142,9 +136,7 @@ private void purgeEndpoint(Endpoint endpoint, TestContext context) { } } while (message != null); - if (logger.isDebugEnabled()) { - logger.debug("Purged " + messagesPurged + " messages from endpoint"); - } + logger.debug("Purged {} messages from endpoint", messagesPurged); } /** diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/ReceiveMessageAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/ReceiveMessageAction.java index ea89780675..6f3603f5bf 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/ReceiveMessageAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/ReceiveMessageAction.java @@ -25,7 +25,6 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; - import org.citrusframework.context.TestContext; import org.citrusframework.endpoint.Endpoint; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -54,9 +53,7 @@ import org.citrusframework.validation.context.ValidationContext; import org.citrusframework.validation.json.JsonMessageValidationContext; import org.citrusframework.validation.json.JsonPathMessageValidationContext; -import org.citrusframework.validation.script.ScriptValidationContext; import org.citrusframework.validation.xml.XmlMessageValidationContext; -import org.citrusframework.validation.xml.XpathMessageValidationContext; import org.citrusframework.variable.VariableExtractor; import org.citrusframework.variable.dictionary.DataDictionary; import org.slf4j.Logger; @@ -65,7 +62,7 @@ /** * This action receives messages from a service destination. Action uses a {@link org.citrusframework.endpoint.Endpoint} * to receive the message, this means that this action is independent of any message transport. - * + *

    * The received message is validated using a {@link MessageValidator} supporting expected * control message payload and header templates. * @@ -184,9 +181,7 @@ private Message receive(TestContext context) { * @return */ private Message receiveSelected(TestContext context, String selectorString) { - if (logger.isDebugEnabled()) { - logger.debug("Setting message selector: '" + selectorString + "'"); - } + logger.debug("Setting message selector: '{}'", selectorString); Endpoint messageEndpoint = getOrCreateEndpoint(context); Consumer consumer = messageEndpoint.createConsumer(); @@ -201,7 +196,7 @@ private Message receiveSelected(TestContext context, String selectorString) { context, messageEndpoint.getEndpointConfiguration().getTimeout()); } } else { - logger.warn(String.format("Unable to receive selective with consumer implementation: '%s'", consumer.getClass())); + logger.warn("Unable to receive selective with consumer implementation: '{}'", consumer.getClass()); return receive(context); } } @@ -213,9 +208,7 @@ private Message receiveSelected(TestContext context, String selectorString) { protected void validateMessage(Message message, TestContext context) { messageProcessors.forEach(processor -> processor.process(message, context)); - if (logger.isDebugEnabled()) { - logger.debug("Received message:\n" + message.print(context)); - } + logger.debug("Received message:\n{}", message.print(context)); // extract variables from received message content for (VariableExtractor variableExtractor : variableExtractors) { @@ -232,9 +225,7 @@ protected void validateMessage(Message message, TestContext context) { if (validationProcessor != null) { validationProcessor.validate(message, context); } else { - if (logger.isDebugEnabled()) { - logger.debug("Control message:\n" + controlMessage.print(context)); - } + logger.debug("Control message:\n{}", controlMessage.print(context)); if (!validators.isEmpty()) { for (MessageValidator messageValidator : validators) { @@ -251,14 +242,12 @@ protected void validateMessage(Message message, TestContext context) { } } else { boolean mustFindValidator = validationContexts.stream() - .anyMatch(item -> JsonPathMessageValidationContext.class.isAssignableFrom(item.getClass()) || - XpathMessageValidationContext.class.isAssignableFrom(item.getClass()) || - ScriptValidationContext.class.isAssignableFrom(item.getClass())); + .anyMatch(ValidationContext::requiresValidator); - List> validators = + List> activeValidators = context.getMessageValidatorRegistry().findMessageValidators(messageType, message, mustFindValidator); - for (MessageValidator messageValidator : validators) { + for (MessageValidator messageValidator : activeValidators) { messageValidator.validateMessage(message, controlMessage, context, validationContexts); } } @@ -428,7 +417,7 @@ public MessageBuilder getMessageBuilder() { /** * Action builder. */ - public static final class Builder extends ReceiveMessageActionBuilder { + public static class Builder extends ReceiveMessageActionBuilder { /** * Fluent API action building entry method used in Java DSL. @@ -733,13 +722,13 @@ public HeaderValidationContext getHeaderValidationContext() { /** * Revisit configured validation context list and automatically add context based on message payload and path * expression contexts if any. - * + *

    * This method makes sure that validation contexts are configured. If no validation context has been set yet the method * automatically adds proper validation contexts for Json and XML message payloads. - * + *

    * In case a path expression (JsonPath, XPath) context is set but no proper message validation context (Json, Xml) the * method automatically adds the proper message validation context. - * + *

    * Only when validation contexts are set properly according to the message type and content the message validation * steps will execute later on. */ diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/ReceiveTimeoutAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/ReceiveTimeoutAction.java index 3a8da7ef60..6bc8431aee 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/ReceiveTimeoutAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/ReceiveTimeoutAction.java @@ -16,9 +16,6 @@ package org.citrusframework.actions; -import java.util.HashMap; -import java.util.Map; - import org.citrusframework.AbstractTestActionBuilder; import org.citrusframework.context.TestContext; import org.citrusframework.endpoint.Endpoint; @@ -28,9 +25,12 @@ import org.citrusframework.message.MessageSelectorBuilder; import org.citrusframework.messaging.Consumer; import org.citrusframework.messaging.SelectiveConsumer; +import org.citrusframework.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.citrusframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; /** * Action expecting a timeout on a message destination, this means that no message @@ -85,9 +85,7 @@ public void doExecute(TestContext context) { } if (receivedMessage != null) { - if (logger.isDebugEnabled()) { - logger.debug("Received message:\n" + receivedMessage.print(context)); - } + logger.debug("Received message:\n{}", receivedMessage.print(context)); throw new CitrusRuntimeException("Message timeout validation failed! " + "Received message while waiting for timeout on destination"); 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 53f6fcabd6..bf449167d7 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/SendMessageAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/SendMessageAction.java @@ -16,7 +16,6 @@ package org.citrusframework.actions; -import org.citrusframework.CitrusSettings; import org.citrusframework.Completable; import org.citrusframework.context.TestContext; import org.citrusframework.endpoint.Endpoint; @@ -25,16 +24,8 @@ import org.citrusframework.message.MessageBuilder; import org.citrusframework.message.MessageDirection; import org.citrusframework.message.MessageProcessor; -import org.citrusframework.message.MessageType; import org.citrusframework.message.builder.MessageBuilderSupport; import org.citrusframework.message.builder.SendMessageBuilderSupport; -import org.citrusframework.util.IsJsonPredicate; -import org.citrusframework.util.IsXmlPredicate; -import org.citrusframework.util.StringUtils; -import org.citrusframework.validation.SchemaValidator; -import org.citrusframework.validation.context.SchemaValidationContext; -import org.citrusframework.validation.json.JsonMessageValidationContext; -import org.citrusframework.validation.xml.XmlMessageValidationContext; import org.citrusframework.variable.VariableExtractor; import org.citrusframework.variable.dictionary.DataDictionary; import org.slf4j.Logger; @@ -45,6 +36,7 @@ import java.util.concurrent.CompletableFuture; import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static org.citrusframework.util.StringUtils.hasText; /** * This action sends a messages to a specified message endpoint. The action holds a reference to @@ -128,8 +120,8 @@ public void doExecute(final TestContext context) { finished.whenComplete((ctx, ex) -> { if (ex != null) { - logger.warn("Failure in forked send action: " + ex.getMessage()); - } else { + logger.warn("Failure in forked send action", ex); + } else if (logger.isWarnEnabled()) { for (Exception ctxEx : ctx.getExceptions()) { logger.warn(ctxEx.getMessage()); } @@ -143,10 +135,11 @@ public void doExecute(final TestContext context) { final Endpoint messageEndpoint = getOrCreateEndpoint(context); - if (StringUtils.hasText(message.getName())) { + if (hasText(message.getName())) { context.getMessageStore().storeMessage(message.getName(), message); } else { - context.getMessageStore().storeMessage(context.getMessageStore().constructMessageName(this, messageEndpoint), message); + context.getMessageStore() + .storeMessage(context.getMessageStore().constructMessageName(this, messageEndpoint), message); } if (forkMode) { @@ -179,58 +172,12 @@ public void doExecute(final TestContext context) { } /** - * Validate the message against registered schemas. - * @param message + * Validate the message against registered schema validators. */ protected void validateMessage(Message message, TestContext context) { - List> schemaValidators = null; - SchemaValidationContext validationContext = null; - String payload = message.getPayload(String.class); - - if ((isSchemaValidation() || isJsonSchemaValidationEnabled()) && IsJsonPredicate.getInstance().test(payload)) { - schemaValidators = context.getMessageValidatorRegistry() - .findSchemaValidators(MessageType.JSON.name(), message); - validationContext = JsonMessageValidationContext.Builder.json() - .schemaValidation(this.schemaValidation) - .schema(this.schema) - .schemaRepository(this.schemaRepository).build(); - } else if ((isSchemaValidation() || isXmlSchemaValidationEnabled()) && IsXmlPredicate.getInstance().test(payload)) { - schemaValidators = context.getMessageValidatorRegistry() - .findSchemaValidators(MessageType.XML.name(), message); - validationContext = XmlMessageValidationContext.Builder.xml() - .schemaValidation(this.schemaValidation) - .schema(this.schema) - .schemaRepository(this.schemaRepository).build(); - } - - if (schemaValidators != null) { - for (SchemaValidator validator : schemaValidators) { - validator.validate(message, context, validationContext); - } - } - - } - - /** - * Get setting to determine if json schema validation is enabled by default. - * @return - */ - private static boolean isJsonSchemaValidationEnabled() { - return Boolean.getBoolean(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_PROPERTY) - || Boolean.getBoolean(CitrusSettings.OUTBOUND_JSON_SCHEMA_VALIDATION_ENABLED_PROPERTY) - || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_ENV)) - || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_JSON_SCHEMA_VALIDATION_ENABLED_ENV)); - } - - /** - * Get setting to determine if xml schema validation is enabled by default. - * @return - */ - private static boolean isXmlSchemaValidationEnabled() { - return Boolean.getBoolean(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_PROPERTY) - || Boolean.getBoolean(CitrusSettings.OUTBOUND_XML_SCHEMA_VALIDATION_ENABLED_PROPERTY) - || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_ENV)) - || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_XML_SCHEMA_VALIDATION_ENABLED_ENV)); + context.getMessageValidatorRegistry().getSchemaValidators().values().stream() + .filter(validator -> validator.canValidate(message, isSchemaValidation())) + .forEach(validator -> validator.validate(message, context, this.schemaRepository, this.schema)); } /** @@ -249,12 +196,13 @@ public boolean isDisabled(TestContext context) { @Override public boolean isDone(TestContext context) { return Optional.ofNullable(finished) - .map(future -> future.isDone() || isDisabled(context)) - .orElseGet(() -> isDisabled(context)); + .map(future -> future.isDone() || isDisabled(context)) + .orElseGet(() -> isDisabled(context)); } /** * Create message to be sent. + * * @param context * @param messageType * @return @@ -264,7 +212,7 @@ protected Message createMessage(TestContext context, String messageType) { if (message.getPayload() != null) { context.getMessageProcessors(MessageDirection.OUTBOUND) - .forEach(processor -> processor.process(message, context)); + .forEach(processor -> processor.process(message, context)); if (dataDictionary != null) { dataDictionary.process(message, context); @@ -278,12 +226,13 @@ protected Message createMessage(TestContext context, String messageType) { /** * Creates or gets the message endpoint instance. + * * @return the message endpoint */ public Endpoint getOrCreateEndpoint(TestContext context) { if (endpoint != null) { return endpoint; - } else if (StringUtils.hasText(endpointUri)) { + } else if (hasText(endpointUri)) { return context.getEndpointFactory().create(endpointUri, context); } else { throw new CitrusRuntimeException("Neither endpoint nor endpoint uri is set properly!"); @@ -292,6 +241,7 @@ public Endpoint getOrCreateEndpoint(TestContext context) { /** * Gets the message endpoint. + * * @return */ public Endpoint getEndpoint() { @@ -300,6 +250,7 @@ public Endpoint getEndpoint() { /** * Get + * * @return true if schema validation is active for this message */ public boolean isSchemaValidation() { @@ -308,6 +259,7 @@ public boolean isSchemaValidation() { /** * Get the name of the schema repository used for validation + * * @return the schema repository name */ public String getSchemaRepository() { @@ -316,6 +268,7 @@ public String getSchemaRepository() { /** * Get the name of the schema used for validation + * * @return the schema */ public String getSchema() { @@ -324,6 +277,7 @@ public String getSchema() { /** * Get the variable extractors. + * * @return the variableExtractors */ public List getVariableExtractors() { @@ -332,6 +286,7 @@ public List getVariableExtractors() { /** * Obtains the message processors. + * * @return */ public List getMessageProcessors() { @@ -340,6 +295,7 @@ public List getMessageProcessors() { /** * Gets the messageBuilder. + * * @return the messageBuilder */ public MessageBuilder getMessageBuilder() { @@ -348,6 +304,7 @@ public MessageBuilder getMessageBuilder() { /** * Gets the forkMode. + * * @return the forkMode the forkMode to get. */ public boolean isForkMode() { @@ -356,6 +313,7 @@ public boolean isForkMode() { /** * Gets the message type for this receive action. + * * @return the messageType */ public String getMessageType() { @@ -364,6 +322,7 @@ public String getMessageType() { /** * Gets the data dictionary. + * * @return */ public DataDictionary getDataDictionary() { @@ -372,6 +331,7 @@ public DataDictionary getDataDictionary() { /** * Gets the endpoint uri. + * * @return */ public String getEndpointUri() { @@ -381,10 +341,12 @@ public String getEndpointUri() { /** * Action builder. */ - public static final class Builder extends SendMessageActionBuilder { + public static final class Builder extends + SendMessageActionBuilder { /** * Fluent API action building entry method used in Java DSL. + * * @return */ public static Builder send() { @@ -393,6 +355,7 @@ public static Builder send() { /** * Fluent API action building entry method used in Java DSL. + * * @param messageEndpoint * @return */ @@ -404,12 +367,14 @@ public static Builder send(Endpoint messageEndpoint) { /** * Fluent API action building entry method used in Java DSL. + * * @param messageEndpointUri * @return */ public static Builder send(String messageEndpointUri) { Builder builder = new Builder(); builder.endpoint(messageEndpointUri); + return builder; } @@ -428,7 +393,8 @@ public SendMessageAction doBuild() { } - public static class SendMessageActionBuilderSupport extends SendMessageBuilderSupport { + public static class SendMessageActionBuilderSupport extends + SendMessageBuilderSupport { public SendMessageActionBuilderSupport(SendMessageAction.Builder delegate) { super(delegate); @@ -438,14 +404,15 @@ public SendMessageActionBuilderSupport(SendMessageAction.Builder delegate) { /** * Base send message action builder also used by subclasses of base send message action. */ - public static abstract class SendMessageActionBuilder, B extends SendMessageActionBuilder> - extends MessageBuilderSupport.MessageActionBuilder { + public abstract static class SendMessageActionBuilder, B extends SendMessageActionBuilder> + extends MessageBuilderSupport.MessageActionBuilder { protected boolean forkMode = false; protected CompletableFuture finished; /** * Sets the fork mode for this send action builder. + * * @param forkMode * @return */ @@ -463,7 +430,7 @@ public final T build() { if (referenceResolver != null) { if (messageBuilderSupport.getDataDictionaryName() != null) { this.messageBuilderSupport.dictionary( - referenceResolver.resolve(messageBuilderSupport.getDataDictionaryName(), DataDictionary.class)); + referenceResolver.resolve(messageBuilderSupport.getDataDictionaryName(), DataDictionary.class)); } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/SleepAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/SleepAction.java index f97c1c6f7e..00bb64555b 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/SleepAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/SleepAction.java @@ -16,15 +16,15 @@ package org.citrusframework.actions; -import java.time.Duration; -import java.util.concurrent.TimeUnit; - import org.citrusframework.AbstractTestActionBuilder; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + /** * Stop the test execution for a given amount of time. * @@ -55,22 +55,23 @@ public void doExecute(TestContext context) { String duration = context.resolveDynamicValue(time); try { - Duration ms; + Duration parsedDuration; if (duration.indexOf(".") > 0) { - ms = switch (timeUnit) { + parsedDuration = switch (timeUnit) { case MILLISECONDS -> Duration.ofMillis(Math.round(Double.parseDouble(duration))); - case SECONDS -> Duration.ofMillis(Math.round(Double.parseDouble(duration) * 1000)); - case MINUTES -> Duration.ofMillis(Math.round(Double.parseDouble(duration) * 60 * 1000)); - default -> throw new CitrusRuntimeException("Unsupported time expression for sleep action - " + - "please use one of milliseconds, seconds, minutes"); + case SECONDS -> Duration.ofSeconds(Math.round(Double.parseDouble(duration))); + case MINUTES -> Duration.ofMinutes(Math.round(Double.parseDouble(duration))); + default -> throw new CitrusRuntimeException("Unsupported time expression for sleep action - please use one of milliseconds, seconds, minutes"); }; } else { - ms = Duration.ofMillis(TimeUnit.MILLISECONDS.convert(Long.parseLong(duration), timeUnit)); + parsedDuration = Duration.ofMillis(TimeUnit.MILLISECONDS.convert(Long.parseLong(duration), timeUnit)); } - logger.info(String.format("Sleeping %s ms", ms.toMillis())); - TimeUnit.MILLISECONDS.sleep(ms.toMillis()); - logger.info(String.format("Returning after %s ms", ms.toMillis())); + logger.info("Sleeping {} {}", duration, timeUnit); + + TimeUnit.MILLISECONDS.sleep(parsedDuration.toMillis()); + + logger.info("Returning after {} {}", duration, timeUnit); } catch (InterruptedException e) { throw new CitrusRuntimeException(e); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/StartServerAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/StartServerAction.java index 98bfb7ae9f..a1c40cd3dc 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/StartServerAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/StartServerAction.java @@ -16,11 +16,6 @@ package org.citrusframework.actions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Stream; - import org.citrusframework.AbstractTestActionBuilder; import org.citrusframework.context.TestContext; import org.citrusframework.server.Server; @@ -29,6 +24,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + /** * Action starting a {@link Server} instance. * @@ -54,7 +54,7 @@ public StartServerAction(Builder builder) { public void doExecute(TestContext context) { for (Server server : servers) { server.start(); - logger.info("Started server: " + server.getName()); + logger.info("Started server: {}", server.getName()); } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/StopServerAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/StopServerAction.java index 12cf143651..5e2b439db2 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/StopServerAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/StopServerAction.java @@ -16,11 +16,6 @@ package org.citrusframework.actions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Stream; - import org.citrusframework.AbstractTestActionBuilder; import org.citrusframework.context.TestContext; import org.citrusframework.server.Server; @@ -29,6 +24,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + /** * Action stopping {@link Server} instances. * @@ -54,7 +54,7 @@ public StopServerAction(Builder builder) { public void doExecute(TestContext context) { for (Server server : servers) { server.stop(); - logger.info("Stopped server: " + server.getName()); + logger.info("Stopped server: {}", server.getName()); } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/StopTimeAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/StopTimeAction.java index 0436a1bee1..b82d9f6a52 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/StopTimeAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/StopTimeAction.java @@ -61,12 +61,12 @@ public void doExecute(TestContext context) { context.setVariable(timeLineId + timeLineSuffix, time); if (description != null) { - logger.info("TimeWatcher " + timeLineId + " after " + time + " ms (" + description + ")"); + logger.info("TimeWatcher {} after {} ms ({})", timeLineId, time, description); } else { - logger.info("TimeWatcher " + timeLineId + " after " + time + " ms"); + logger.info("TimeWatcher {} after {} ms", timeLineId, time); } } else { - logger.info("Starting TimeWatcher: " + timeLineId); + logger.info("Starting TimeWatcher: {}", timeLineId); context.setVariable(timeLineId, System.currentTimeMillis()); context.setVariable(timeLineId + timeLineSuffix, 0L); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/StopTimerAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/StopTimerAction.java index 9c2e2a8b79..142324e29a 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/StopTimerAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/StopTimerAction.java @@ -47,7 +47,7 @@ public String getTimerId() { public void doExecute(TestContext context) { if (timerId != null) { boolean success = context.stopTimer(timerId); - logger.info(String.format("Stopping timer %s - stop successful: %s", timerId, success)); + logger.info("Stopping timer {} - stop successful: {}", timerId, success); } else { context.stopTimers(); logger.info("Stopping all timers"); diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/TraceVariablesAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/TraceVariablesAction.java index d387beb395..d3d24b9d1a 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/TraceVariablesAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/TraceVariablesAction.java @@ -16,16 +16,16 @@ package org.citrusframework.actions; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.stream.Stream; - import org.citrusframework.AbstractTestActionBuilder; import org.citrusframework.context.TestContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; + /** * Action that prints variable values to the console/logger. Action requires a list of variable * names. Tries to find the variables in the test context and print its values. @@ -53,7 +53,7 @@ public void doExecute(TestContext context) { logger.info("Trace variables"); Iterator it; - if (variableNames != null && variableNames.size() > 0) { + if (variableNames != null && !variableNames.isEmpty()) { it = variableNames.iterator(); } else { it = context.getVariables().keySet().iterator(); @@ -63,7 +63,7 @@ public void doExecute(TestContext context) { String key = it.next(); String value = context.getVariable(key); - logger.info("Variable " + context.getLogModifier().mask(key + " = " + value)); + logger.info("Variable {}", context.getLogModifier().mask(key + " = " + value)); } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/actions/TransformAction.java b/core/citrus-base/src/main/java/org/citrusframework/actions/TransformAction.java index d32c8773d5..d9e5293399 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/actions/TransformAction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/actions/TransformAction.java @@ -85,9 +85,7 @@ public TransformAction(Builder builder) { @Override public void doExecute(TestContext context) { try { - if (logger.isDebugEnabled()) { - logger.debug("Starting XSLT transformation"); - } + logger.debug("Starting XSLT transformation"); //parse XML document and define XML source for transformation Source xmlSource; diff --git a/core/citrus-base/src/main/java/org/citrusframework/annotations/CitrusAnnotations.java b/core/citrus-base/src/main/java/org/citrusframework/annotations/CitrusAnnotations.java index 88fa8578b2..e6af3437ba 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/annotations/CitrusAnnotations.java +++ b/core/citrus-base/src/main/java/org/citrusframework/annotations/CitrusAnnotations.java @@ -16,10 +16,6 @@ package org.citrusframework.annotations; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; -import java.util.Arrays; - import org.citrusframework.Citrus; import org.citrusframework.CitrusContext; import org.citrusframework.GherkinTestActionRunner; @@ -34,6 +30,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.Arrays; + /** * Dependency injection support for {@link CitrusFramework}, {@link CitrusResource} and {@link CitrusEndpoint} annotations. * @@ -105,7 +105,7 @@ public static void injectCitrusFramework(final Object testCase, final Citrus cit return; } - logger.trace(String.format("Injecting Citrus framework instance on test class field '%s'", field.getName())); + logger.trace("Injecting Citrus framework instance on test class field '{}'", field.getName()); ReflectionHelper.setField(field, testCase, citrusFramework); }); } @@ -121,7 +121,7 @@ public static void injectCitrusContext(final Object target, final CitrusContext return; } - logger.trace(String.format("Injecting Citrus context instance on test class field '%s'", field.getName())); + logger.trace("Injecting Citrus context instance on test class field '{}'", field.getName()); ReflectionHelper.setField(field, target, context); }); } @@ -139,7 +139,7 @@ public static void injectTestContext(final Object target, final TestContext cont Class type = field.getType(); if (TestContext.class.isAssignableFrom(type)) { - logger.trace(String.format("Injecting test context instance on test class field '%s'", field.getName())); + logger.trace("Injecting test context instance on test class field '{}'", field.getName()); ReflectionHelper.setField(field, target, context); } else { throw new CitrusRuntimeException("Not able to provide a Citrus resource injection for type " + type); @@ -160,7 +160,7 @@ public static void injectTestRunner(final Object target, final TestCaseRunner ru Class type = field.getType(); if (TestCaseRunner.class.isAssignableFrom(type)) { - logger.trace(String.format("Injecting test runner instance on test class field '%s'", field.getName())); + logger.trace("Injecting test runner instance on test class field '{}'", field.getName()); ReflectionHelper.setField(field, target, runner); } else { throw new CitrusRuntimeException("Not able to provide a Citrus resource injection for type " + type); @@ -184,7 +184,7 @@ private static void injectTestActionRunner(final Object target, final TestAction Class type = field.getType(); if (TestActionRunner.class.isAssignableFrom(type)) { - logger.trace(String.format("Injecting test action runner instance on test class field '%s'", field.getName())); + logger.trace("Injecting test action runner instance on test class field '{}'", field.getName()); ReflectionHelper.setField(field, target, runner); } else { throw new CitrusRuntimeException("Not able to provide a Citrus resource injection for type " + type); @@ -205,7 +205,7 @@ private static void injectGherkinTestActionRunner(final Object target, final Ghe Class type = field.getType(); if (GherkinTestActionRunner.class.isAssignableFrom(type)) { - logger.trace(String.format("Injecting test action runner instance on test class field '%s'", field.getName())); + logger.trace("Injecting test action runner instance on test class field '{}'", field.getName()); ReflectionHelper.setField(field, target, runner); } else { throw new CitrusRuntimeException("Not able to provide a Citrus resource injection for type " + type); diff --git a/core/citrus-base/src/main/java/org/citrusframework/common/DefaultTestLoader.java b/core/citrus-base/src/main/java/org/citrusframework/common/DefaultTestLoader.java index 248e836dd6..690dfa5762 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/common/DefaultTestLoader.java +++ b/core/citrus-base/src/main/java/org/citrusframework/common/DefaultTestLoader.java @@ -16,10 +16,11 @@ package org.citrusframework.common; +import static org.citrusframework.TestResult.failed; + import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; - import org.citrusframework.Citrus; import org.citrusframework.CitrusContext; import org.citrusframework.DefaultTestCase; @@ -32,8 +33,6 @@ import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.TestCaseFailedException; -import static org.citrusframework.TestResult.failed; - /** * Default test loader implementation takes case on test names/packages and initializes the test runner if applicable. * Also loads the test case and provides it to registered test handlers. This way a test case can be loaded from different sources @@ -102,6 +101,11 @@ public final void load() { // This kind of exception indicates that the error has already been handled. Just throw and end test run. throw e; } catch (Exception | Error e) { + + if (e instanceof RuntimeException runtimeException && isSkipException(runtimeException)) { + throw runtimeException; + } + if (testCase == null) { testCase = runner.getTestCase(); } @@ -113,6 +117,16 @@ public final void load() { } } + /** + * Return true if the throwable is considered a skip exception that must be passed up to the + * testing framework in order to report that this test was skipped. + * + * Sublasses may override according to the framework + */ + protected boolean isSkipException(Throwable e) { + return false; + } + /** * Subclasses are supposed to overwrite this method on order to add logic how to load the test case (e.g. from XML, Json, YAML). */ diff --git a/core/citrus-base/src/main/java/org/citrusframework/condition/ActionCondition.java b/core/citrus-base/src/main/java/org/citrusframework/condition/ActionCondition.java index fee180ff40..6f7ceff1e5 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/condition/ActionCondition.java +++ b/core/citrus-base/src/main/java/org/citrusframework/condition/ActionCondition.java @@ -16,13 +16,13 @@ package org.citrusframework.condition; -import java.util.Optional; - import org.citrusframework.TestAction; import org.citrusframework.context.TestContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Optional; + /** * @since 2.7.6 */ @@ -62,9 +62,10 @@ public boolean isSatisfied(TestContext context) { action.execute(context); } catch (Exception e) { this.caughtException = e; - logger.warn(String.format("Nested action did not perform as expected - %s", Optional.ofNullable(e.getMessage()) - .map(msg -> e.getClass().getName() + ": " + msg) - .orElseGet(() -> e.getClass().getName()))); + logger.warn("Nested action did not perform as expected - {}", + Optional.ofNullable(e.getMessage()) + .map(msg -> e.getClass().getName() + ": " + msg) + .orElseGet(() -> e.getClass().getName())); return false; } diff --git a/core/citrus-base/src/main/java/org/citrusframework/condition/FileCondition.java b/core/citrus-base/src/main/java/org/citrusframework/condition/FileCondition.java index ac2c844f11..65951b8549 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/condition/FileCondition.java +++ b/core/citrus-base/src/main/java/org/citrusframework/condition/FileCondition.java @@ -16,13 +16,13 @@ package org.citrusframework.condition; -import java.io.File; - import org.citrusframework.context.TestContext; import org.citrusframework.util.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; + /** * Tests for the presence of a file and returns true if the file exists * @@ -46,9 +46,7 @@ public FileCondition() { @Override public boolean isSatisfied(TestContext context) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Checking file path '%s'", file != null ? file.getPath() : filePath)); - } + logger.debug("Checking file path '{}'", file != null ? file.getPath() : filePath); if (file != null) { return file.exists() && file.isFile(); @@ -56,7 +54,7 @@ public boolean isSatisfied(TestContext context) { try { return FileUtils.getFileResource(context.replaceDynamicContentInString(filePath), context).getFile().isFile(); } catch (Exception e) { - logger.warn(String.format("Failed to access file resource '%s'", e.getMessage())); + logger.warn("Failed to access file resource '{}'", e.getMessage()); return false; } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/condition/HttpCondition.java b/core/citrus-base/src/main/java/org/citrusframework/condition/HttpCondition.java index 95be6db182..a252af571b 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/condition/HttpCondition.java +++ b/core/citrus-base/src/main/java/org/citrusframework/condition/HttpCondition.java @@ -16,16 +16,16 @@ package org.citrusframework.condition; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + /** * Tests if a HTTP Endpoint is reachable. The test is successful if the endpoint responds with the expected response * code. By default a HTTP 200 response code is expected. @@ -76,9 +76,7 @@ public String getErrorMessage(TestContext context) { */ private int invokeUrl(TestContext context) { URL contextUrl = getUrl(context); - if (logger.isDebugEnabled()) { - logger.debug(String.format("Probing Http request url '%s'", contextUrl.toExternalForm())); - } + logger.debug("Probing Http request url '{}'", contextUrl.toExternalForm()); int responseCode = -1; @@ -90,7 +88,7 @@ private int invokeUrl(TestContext context) { responseCode = httpURLConnection.getResponseCode(); } catch (IOException e) { - logger.warn(String.format("Could not access Http url '%s' - %s", contextUrl.toExternalForm(), e.getMessage())); + logger.warn("Could not access Http url '{}' - {}", contextUrl.toExternalForm(), e.getMessage()); } finally { if (httpURLConnection != null) { httpURLConnection.disconnect(); diff --git a/core/citrus-base/src/main/java/org/citrusframework/condition/MessageCondition.java b/core/citrus-base/src/main/java/org/citrusframework/condition/MessageCondition.java index 986762dee5..f9e0d1e228 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/condition/MessageCondition.java +++ b/core/citrus-base/src/main/java/org/citrusframework/condition/MessageCondition.java @@ -22,7 +22,7 @@ * Condition checks whether a message is present in test context message store. Messages are automatically * stored in that store when sending and receiving messages with respective test actions. So this condition * can be used to wait for a message to arrive or being sent out. - * + *

    * Message to check is identified by its name in the message store. * * @since 2.6.2 diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/AbstractActionContainer.java b/core/citrus-base/src/main/java/org/citrusframework/container/AbstractActionContainer.java index 860e7a71a0..61c049c863 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/AbstractActionContainer.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/AbstractActionContainer.java @@ -16,13 +16,6 @@ package org.citrusframework.container; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import org.citrusframework.AbstractTestContainerBuilder; import org.citrusframework.Completable; import org.citrusframework.TestAction; @@ -32,6 +25,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + /** * Abstract base class for all containers holding several embedded test actions. * @@ -106,10 +106,8 @@ public boolean isDone(TestContext context) { for (TestAction action : new ArrayList<>(executedActions)) { if (action instanceof Completable && !((Completable) action).isDone(context)) { - if (logger.isDebugEnabled()) { - logger.debug(Optional.ofNullable(action.getName()).filter(name -> !name.trim().isEmpty()) - .orElseGet(() -> action.getClass().getName()) + " not completed yet"); - } + logger.debug("{} not completed yet", Optional.ofNullable(action.getName()).filter(name -> !name.trim().isEmpty()) + .orElseGet(() -> action.getClass().getName())); return false; } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/AbstractSuiteActionContainer.java b/core/citrus-base/src/main/java/org/citrusframework/container/AbstractSuiteActionContainer.java index a9478bcb7f..cd04735330 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/AbstractSuiteActionContainer.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/AbstractSuiteActionContainer.java @@ -16,11 +16,6 @@ package org.citrusframework.container; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import org.citrusframework.AbstractTestContainerBuilder; import org.citrusframework.TestActionBuilder; import org.citrusframework.spi.ReferenceResolver; @@ -29,6 +24,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * Abstract suit container actions executed before and after test suite run. Container decides * weather to execute according to given suite name and included test groups if any. @@ -65,29 +65,30 @@ public AbstractSuiteActionContainer(String name, AbstractTestContainerBuilder envEntry : env.entrySet()) { - if (!System.getenv().containsKey(envEntry.getKey()) || - (StringUtils.hasText(envEntry.getValue()) && !System.getenv().get(envEntry.getKey()).equals(envEntry.getValue()))) { - logger.warn(String.format(baseErrorMessage, "env properties", getName())); + if (!System.getenv().containsKey(envEntry.getKey()) + || (StringUtils.hasText(envEntry.getValue()) && !System.getenv().get(envEntry.getKey()).equals(envEntry.getValue()))) { + logger.warn("{} env properties {}", baseErrorMessage, getName()); return false; } } for (Map.Entry systemProperty : systemProperties.entrySet()) { - if (!System.getProperties().containsKey(systemProperty.getKey()) || - (StringUtils.hasText(systemProperty.getValue()) && !System.getProperties().get(systemProperty.getKey()).equals(systemProperty.getValue()))) { - logger.warn(String.format(baseErrorMessage, "system properties", getName())); + if (!System.getProperties().containsKey(systemProperty.getKey()) + || (StringUtils.hasText(systemProperty.getValue()) && !System.getProperties().get(systemProperty.getKey()).equals(systemProperty.getValue()))) { + logger.warn("{} system properties {}", baseErrorMessage, getName()); return false; } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/AbstractTestBoundaryActionContainer.java b/core/citrus-base/src/main/java/org/citrusframework/container/AbstractTestBoundaryActionContainer.java index e20579d0a9..48e674e582 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/AbstractTestBoundaryActionContainer.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/AbstractTestBoundaryActionContainer.java @@ -16,12 +16,6 @@ package org.citrusframework.container; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; - import org.citrusframework.AbstractTestContainerBuilder; import org.citrusframework.TestActionBuilder; import org.citrusframework.spi.ReferenceResolver; @@ -30,6 +24,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + /** * Abstract test action container describes methods to enable/disable container execution based on given test name, package * and test groups. This action container is typically used by before and after test action containers. @@ -72,27 +72,27 @@ public boolean shouldExecute(String testName, String packageName, String[] inclu if (StringUtils.hasText(packageNamePattern)) { if (!Pattern.compile(packageNamePattern).matcher(packageName).matches()) { - logger.warn(String.format(baseErrorMessage, "test package", getName())); + logger.warn("{} test package {}", baseErrorMessage, getName()); return false; } } if (StringUtils.hasText(namePattern)) { if (!Pattern.compile(sanitizePatten(namePattern)).matcher(testName).matches()) { - logger.warn(String.format(baseErrorMessage, "test name", getName())); + logger.warn("{} test name {}", baseErrorMessage, getName()); return false; } } if (!checkTestGroups(includedGroups)) { - logger.warn(String.format(baseErrorMessage, "test groups", getName())); + logger.warn("{} test groups {}", baseErrorMessage, getName()); return false; } for (Map.Entry envEntry : env.entrySet()) { if (!System.getenv().containsKey(envEntry.getKey()) || (StringUtils.hasText(envEntry.getValue()) && !System.getenv().get(envEntry.getKey()).equals(envEntry.getValue()))) { - logger.warn(String.format(baseErrorMessage, "env properties", getName())); + logger.warn("{} env properties {}", baseErrorMessage, getName()); return false; } } @@ -100,7 +100,7 @@ public boolean shouldExecute(String testName, String packageName, String[] inclu for (Map.Entry systemProperty : systemProperties.entrySet()) { if (!System.getProperties().containsKey(systemProperty.getKey()) || (StringUtils.hasText(systemProperty.getValue()) && !System.getProperties().get(systemProperty.getKey()).equals(systemProperty.getValue()))) { - logger.warn(String.format(baseErrorMessage, "system properties", getName())); + logger.warn("{} system properties {}", baseErrorMessage, getName()); return false; } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/Assert.java b/core/citrus-base/src/main/java/org/citrusframework/container/Assert.java index 13e93aee68..a4b805ba9f 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/Assert.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/Assert.java @@ -72,9 +72,7 @@ public Assert(Builder builder) { @Override public void doExecute(TestContext context) { - if (logger.isDebugEnabled()) { - logger.debug("Assert container asserting exceptions of type {}", exception.getSimpleName()); - } + logger.debug("Assert container asserting exceptions of type {}", exception.getSimpleName()); try { executeAction(this.action.build(), context); diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/Catch.java b/core/citrus-base/src/main/java/org/citrusframework/container/Catch.java index 2730af0cee..30b7392f61 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/Catch.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/Catch.java @@ -45,16 +45,14 @@ public Catch(Builder builder) { @Override public void doExecute(TestContext context) { - if (logger.isDebugEnabled()) { - logger.debug("Catch container catching exceptions of type " + exception); - } + logger.debug("Catch container catching exceptions of type {}", exception); for (TestActionBuilder actionBuilder: actions) { try { executeAction(actionBuilder.build(), context); } catch (Exception e) { if (exception != null && exception.equals(e.getClass().getName())) { - logger.info("Caught exception " + e.getClass() + ": " + e.getLocalizedMessage()); + logger.info("Caught exception {}: {}", e.getClass(), e.getLocalizedMessage()); continue; } throw new CitrusRuntimeException(e); diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/Iterate.java b/core/citrus-base/src/main/java/org/citrusframework/container/Iterate.java index f70505ce47..c71d452244 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/Iterate.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/Iterate.java @@ -22,7 +22,7 @@ /** * Class executes nested test actions in loops. Iteration continues as long * as looping condition evaluates to true. - * + *

    * Each loop an index variable is incremented. The index variable is accessible inside the nested * test actions as normal test variable. Iteration starts with index=1 and increments with a * default step=1. diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/RepeatOnErrorUntilTrue.java b/core/citrus-base/src/main/java/org/citrusframework/container/RepeatOnErrorUntilTrue.java index dea167e97f..f9cb7cf22f 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/RepeatOnErrorUntilTrue.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/RepeatOnErrorUntilTrue.java @@ -16,11 +16,8 @@ package org.citrusframework.container; -import jakarta.annotation.Nullable; -import org.apache.commons.lang3.time.StopWatch; import org.citrusframework.AbstractIteratingContainerBuilder; import org.citrusframework.context.TestContext; -import org.citrusframework.exceptions.ActionTimeoutException; import org.citrusframework.exceptions.CitrusRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/RepeatUntilTrue.java b/core/citrus-base/src/main/java/org/citrusframework/container/RepeatUntilTrue.java index b28f7187d3..8ba4089a87 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/RepeatUntilTrue.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/RepeatUntilTrue.java @@ -22,7 +22,7 @@ /** * Typical implementation of repeat iteration loop. Nested test actions are executed until * aborting condition evaluates to true. - * + *

    * Index is incremented each iteration and stored as test variable accessible in the nested test actions * as normal variable. Index starts with 1 by default. * diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/SequenceAfterSuite.java b/core/citrus-base/src/main/java/org/citrusframework/container/SequenceAfterSuite.java index 9a03c1dc63..8352608a37 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/SequenceAfterSuite.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/SequenceAfterSuite.java @@ -50,7 +50,7 @@ public void doExecute(TestContext context) { logger.info("Entering after suite block"); if (logger.isDebugEnabled()) { - logger.debug("Executing " + actions.size() + " actions after suite"); + logger.debug("Executing {} actions after suite", actions.size()); logger.debug(""); } @@ -60,7 +60,7 @@ public void doExecute(TestContext context) { /* Executing test action and validate its success */ action.execute(context); } catch (Exception e) { - logger.error("After suite action failed " + action.getName() + "Nested exception is: ", e); + logger.error("After suite action failed {}Nested exception is: ", action.getName(), e); logger.error("Continue after suite actions"); success = false; } diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/SequenceAfterTest.java b/core/citrus-base/src/main/java/org/citrusframework/container/SequenceAfterTest.java index 2307d3a8a9..d5fc5ff458 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/SequenceAfterTest.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/SequenceAfterTest.java @@ -51,7 +51,7 @@ public void doExecute(TestContext context) { logger.info("Entering after test block"); if (logger.isDebugEnabled()) { - logger.debug("Executing " + actions.size() + " actions after test"); + logger.debug("Executing {} actions after test", actions.size()); logger.debug(""); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/SequenceBeforeSuite.java b/core/citrus-base/src/main/java/org/citrusframework/container/SequenceBeforeSuite.java index 00218d41fb..b63d320a48 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/SequenceBeforeSuite.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/SequenceBeforeSuite.java @@ -48,7 +48,7 @@ public void doExecute(TestContext context) { logger.info("Entering before suite block"); if (logger.isDebugEnabled()) { - logger.debug("Executing " + actions.size() + " actions before suite"); + logger.debug("Executing {} actions before suite", actions.size()); logger.debug(""); } @@ -58,7 +58,7 @@ public void doExecute(TestContext context) { /* Executing test action and validate its success */ action.execute(context); } catch (Exception e) { - logger.error("Task failed " + action.getName() + "Nested exception is: ", e); + logger.error("Task failed {}Nested exception is: ", action.getName(), e); throw new CitrusRuntimeException(e); } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/SequenceBeforeTest.java b/core/citrus-base/src/main/java/org/citrusframework/container/SequenceBeforeTest.java index c675859ce7..7bba10edb6 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/SequenceBeforeTest.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/SequenceBeforeTest.java @@ -51,7 +51,7 @@ public void doExecute(TestContext context) { logger.info("Entering before test block"); if (logger.isDebugEnabled()) { - logger.debug("Executing " + actions.size() + " actions before test"); + logger.debug("Executing {} actions before test", actions.size()); logger.debug(""); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/Template.java b/core/citrus-base/src/main/java/org/citrusframework/container/Template.java index 72738ed515..20aef139da 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/Template.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/Template.java @@ -44,12 +44,12 @@ /** * This class represents a previously defined block of test actions. Test cases can call * templates and reuse their functionality. - * + *

    * Templates operate on test variables. While calling, the template caller can set these * variables as parameters. - * + *

    * Nested test actions are executed in sequence. - * + *

    * The template execution may affect existing variable values in the calling test case. So * variables may have different values in the test case after template execution. Therefore, * users can create a local test context by setting globalContext to false. Templates then will diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/TemplateLoader.java b/core/citrus-base/src/main/java/org/citrusframework/container/TemplateLoader.java index 0f0dc251e3..3add3c2a57 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/TemplateLoader.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/TemplateLoader.java @@ -16,10 +16,6 @@ package org.citrusframework.container; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.functions.Function; import org.citrusframework.spi.ReferenceResolverAware; @@ -28,10 +24,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + public interface TemplateLoader extends ReferenceResolverAware { /** Logger */ - Logger logger = LoggerFactory.getLogger(Function.class); + Logger logger = LoggerFactory.getLogger(TemplateLoader.class); /** Function resource lookup path */ String RESOURCE_PATH = "META-INF/citrus/template/loader"; @@ -53,7 +53,7 @@ static Optional lookup(String name) { TemplateLoader instance = TYPE_RESOLVER.resolve(name); return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve template loader from resource '%s/%s'", RESOURCE_PATH, name)); + logger.warn("Failed to resolve template loader from resource '{}/{}'", RESOURCE_PATH, name); } return Optional.empty(); diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/Timer.java b/core/citrus-base/src/main/java/org/citrusframework/container/Timer.java index 7cd9dfe843..3e47bed468 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/Timer.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/Timer.java @@ -89,13 +89,13 @@ public void run() { try { indexCount++; updateIndexCountInTestContext(context); - logger.debug(String.format("Timer event fired #%s - executing nested actions", indexCount)); + logger.debug("Timer event fired #{} - executing nested actions", indexCount); for (TestActionBuilder actionBuilder : actions) { executeAction(actionBuilder.build(), context); } if (indexCount >= repeatCount) { - logger.debug(String.format("Timer complete: %s iterations reached", repeatCount)); + logger.debug("Timer complete: {} iterations reached", repeatCount); stopTimer(); } } catch (Exception e) { @@ -113,7 +113,7 @@ private void handleException(Exception e) { } else { timerException = new CitrusRuntimeException(e); } - logger.error(String.format("Timer stopped as a result of nested action error (%s)", e.getMessage())); + logger.error("Timer stopped as a result of nested action error ({})", e.getMessage()); stopTimer(); if (fork) { diff --git a/core/citrus-base/src/main/java/org/citrusframework/container/Wait.java b/core/citrus-base/src/main/java/org/citrusframework/container/Wait.java index b02056d8f9..c4b17baa0e 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/container/Wait.java +++ b/core/citrus-base/src/main/java/org/citrusframework/container/Wait.java @@ -83,9 +83,7 @@ public void doExecute(final TestContext context) { while (timeLeft > 0) { timeLeft -= intervalMs; - if (logger.isDebugEnabled()) { - logger.debug(String.format("Waiting for condition %s", condition.getName())); - } + logger.debug("Waiting for condition {}", condition.getName()); var executor = newSingleThreadExecutor(); long checkStartTime = System.currentTimeMillis(); diff --git a/core/citrus-base/src/main/java/org/citrusframework/endpoint/AbstractEndpointComponent.java b/core/citrus-base/src/main/java/org/citrusframework/endpoint/AbstractEndpointComponent.java index 29c4f398d1..477abd9a54 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/endpoint/AbstractEndpointComponent.java +++ b/core/citrus-base/src/main/java/org/citrusframework/endpoint/AbstractEndpointComponent.java @@ -16,6 +16,13 @@ package org.citrusframework.endpoint; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.ReferenceResolverAware; +import org.citrusframework.util.ReflectionHelper; +import org.citrusframework.util.StringUtils; +import org.citrusframework.util.TypeConversionUtils; + import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URI; @@ -28,17 +35,10 @@ import java.util.Map; import java.util.StringTokenizer; -import org.citrusframework.context.TestContext; -import org.citrusframework.exceptions.CitrusRuntimeException; -import org.citrusframework.spi.ReferenceResolverAware; -import org.citrusframework.util.ReflectionHelper; -import org.citrusframework.util.StringUtils; -import org.citrusframework.util.TypeConversionUtils; - /** * Default endpoint component reads component name from endpoint uri and parses parameters from uri using * the HTTP uri pattern. - * + *

    * http://localhost:8080?param1=value1¶m2=value2¶m3=value3 * jms:queue.name?connectionFactory=specialConnectionFactory * soap:localhost:8080?soapAction=sayHello @@ -187,16 +187,13 @@ protected String getParameterString(Map parameters, Field field = ReflectionHelper.findField(endpointConfigurationType, parameterEntry.getKey()); if (field == null) { - if (paramString.length() == 0) { + if (paramString.isEmpty()) { paramString.append("?").append(parameterEntry.getKey()); - if (parameterEntry.getValue() != null) { - paramString.append("=").append(parameterEntry.getValue()); - } } else { paramString.append("&").append(parameterEntry.getKey()); - if (parameterEntry.getValue() != null) { - paramString.append("=").append(parameterEntry.getValue()); - } + } + if (parameterEntry.getValue() != null) { + paramString.append("=").append(parameterEntry.getValue()); } } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectConsumer.java b/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectConsumer.java index 0f6ccd4468..78237afb6b 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectConsumer.java +++ b/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectConsumer.java @@ -24,9 +24,9 @@ import org.citrusframework.message.MessageSelector; import org.citrusframework.message.selector.DelegatingMessageSelector; import org.citrusframework.messaging.AbstractSelectiveMessageConsumer; +import org.citrusframework.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.citrusframework.util.StringUtils; public class DirectConsumer extends AbstractSelectiveMessageConsumer { @@ -57,9 +57,7 @@ public Message receive(String selector, TestContext context, long timeout) { destinationQueueName = getDestinationQueueName(); } - if (logger.isDebugEnabled()) { - logger.debug(String.format("Receiving message from queue: '%s'", destinationQueueName)); - } + logger.debug("Receiving message from queue: '{}'", destinationQueueName); Message message; if (StringUtils.hasText(selector)) { @@ -82,7 +80,7 @@ public Message receive(String selector, TestContext context, long timeout) { throw new MessageTimeoutException(timeout, destinationQueueName); } - logger.info(String.format("Received message from queue: '%s'", destinationQueueName)); + logger.info("Received message from queue: '{}'", destinationQueueName); return message; } diff --git a/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncConsumer.java b/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncConsumer.java index 5b25d7597a..26308b3850 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncConsumer.java +++ b/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncConsumer.java @@ -67,12 +67,12 @@ public void send(Message message, TestContext context) { ObjectHelper.assertNotNull(replyQueue, "Failed to find reply channel for message correlation key: " + correlationKey); if (logger.isDebugEnabled()) { - logger.debug("Sending message to reply channel: '" + replyQueue + "'"); - logger.debug("Message to send is:\n" + message.toString()); + logger.debug("Sending message to reply channel: '{}'", replyQueue); + logger.debug("Message to send is:\n{}", message); } replyQueue.send(message); - logger.info("Message was sent to reply channel: '" + replyQueue + "'"); + logger.info("Message was sent to reply channel: '{}'", replyQueue); } /** @@ -94,8 +94,7 @@ public void saveReplyMessageQueue(Message receivedMessage, TestContext context) correlationManager.saveCorrelationKey(correlationKeyName, correlationKey, context); correlationManager.store(correlationKey, replyQueue); } else { - logger.warn("Unable to retrieve reply message channel for message \n" + - receivedMessage + "\n - no reply channel found in message headers!"); + logger.warn("Unable to retrieve reply message channel for message \n{}\n - no reply channel found in message headers!", receivedMessage); } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncProducer.java b/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncProducer.java index c5788fdb67..9dad1395e7 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncProducer.java +++ b/core/citrus-base/src/main/java/org/citrusframework/endpoint/direct/DirectSyncProducer.java @@ -16,8 +16,6 @@ package org.citrusframework.endpoint.direct; -import java.util.UUID; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.MessageTimeoutException; import org.citrusframework.exceptions.ReplyMessageTimeoutException; @@ -30,6 +28,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.UUID; + public class DirectSyncProducer extends DirectProducer implements ReplyConsumer { /** Logger */ private static final Logger logger = LoggerFactory.getLogger(DirectSyncProducer.class); @@ -62,11 +62,11 @@ public void send(Message message, TestContext context) { String destinationQueueName = getDestinationQueueName(); if (logger.isDebugEnabled()) { - logger.debug("Sending message to queue: '" + destinationQueueName + "'"); - logger.debug("Message to send is:\n" + message.toString()); + logger.debug("Sending message to queue: '{}'", destinationQueueName); + logger.debug("Message to send is:\n{}", message.toString()); } - logger.info("Message was sent to queue: '" + destinationQueueName + "'"); + logger.info("Message was sent to queue: '{}'", destinationQueueName); MessageQueue replyQueue = getReplyQueue(message, context); getDestinationQueue(context).send(message); @@ -89,7 +89,7 @@ public void send(Message message, TestContext context) { */ private MessageQueue getReplyQueue(Message message, TestContext context) { if (message.getHeader(DirectMessageHeaders.REPLY_QUEUE) == null) { - MessageQueue temporaryQueue = new DefaultMessageQueue(getName() + "." + UUID.randomUUID().toString()); + MessageQueue temporaryQueue = new DefaultMessageQueue(getName() + "." + UUID.randomUUID()); message.setHeader(DirectMessageHeaders.REPLY_QUEUE, temporaryQueue); return temporaryQueue; } diff --git a/core/citrus-base/src/main/java/org/citrusframework/endpoint/resolver/DynamicEndpointUriResolver.java b/core/citrus-base/src/main/java/org/citrusframework/endpoint/resolver/DynamicEndpointUriResolver.java index 6d2cb8d20c..0b1305590f 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/endpoint/resolver/DynamicEndpointUriResolver.java +++ b/core/citrus-base/src/main/java/org/citrusframework/endpoint/resolver/DynamicEndpointUriResolver.java @@ -16,13 +16,13 @@ package org.citrusframework.endpoint.resolver; -import java.util.Map; -import java.util.StringTokenizer; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.message.Message; import org.citrusframework.util.StringUtils; +import java.util.Map; +import java.util.StringTokenizer; + /** * Endpoint uri resolver working on message headers. Resolver is searching for a specific header entry which holds the actual * target endpoint uri. @@ -78,7 +78,7 @@ private String appendRequestPath(String uri, Map headers) { requestUri = requestUri.substring(0, requestUri.length() - 1); } - while (path.startsWith("/") && path.length() > 0) { + while (path.startsWith("/") && !path.isEmpty()) { path = path.length() == 1 ? "" : path.substring(1); } @@ -114,7 +114,7 @@ private String appendQueryParams(String uri, Map headers) { queryParamBuilder.append("&").append(tok.nextToken()); } - return requestUri + queryParamBuilder.toString(); + return requestUri + queryParamBuilder; } /** diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/DefaultFunctionLibrary.java b/core/citrus-base/src/main/java/org/citrusframework/functions/DefaultFunctionLibrary.java index be7fc8e0f5..4d3b211e9c 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/DefaultFunctionLibrary.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/DefaultFunctionLibrary.java @@ -17,6 +17,7 @@ package org.citrusframework.functions; import org.citrusframework.functions.core.AbsoluteFunction; +import org.citrusframework.functions.core.AdvancedRandomNumberFunction; import org.citrusframework.functions.core.AvgFunction; import org.citrusframework.functions.core.CeilingFunction; import org.citrusframework.functions.core.ChangeDateFunction; @@ -34,6 +35,7 @@ import org.citrusframework.functions.core.MinFunction; import org.citrusframework.functions.core.RandomEnumValueFunction; import org.citrusframework.functions.core.RandomNumberFunction; +import org.citrusframework.functions.core.RandomPatternFunction; import org.citrusframework.functions.core.RandomStringFunction; import org.citrusframework.functions.core.RandomUUIDFunction; import org.citrusframework.functions.core.ReadFileResourceFunction; @@ -45,10 +47,10 @@ import org.citrusframework.functions.core.SumFunction; import org.citrusframework.functions.core.SystemPropertyFunction; import org.citrusframework.functions.core.TranslateFunction; +import org.citrusframework.functions.core.UnixTimestampFunction; import org.citrusframework.functions.core.UpperCaseFunction; import org.citrusframework.functions.core.UrlDecodeFunction; import org.citrusframework.functions.core.UrlEncodeFunction; -import org.citrusframework.functions.core.UnixTimestampFunction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,7 +66,9 @@ public DefaultFunctionLibrary() { setName("citrusFunctionLibrary"); getMembers().put("randomNumber", new RandomNumberFunction()); + getMembers().put("randomNumberGenerator", new AdvancedRandomNumberFunction()); getMembers().put("randomString", new RandomStringFunction()); + getMembers().put("randomPattern", new RandomPatternFunction()); getMembers().put("concat", new ConcatFunction()); getMembers().put("currentDate", new CurrentDateFunction()); getMembers().put("substring", new SubstringFunction()); @@ -106,9 +110,7 @@ public DefaultFunctionLibrary() { private void lookupFunctions() { Function.lookup().forEach((k, m) -> { getMembers().put(k, m); - if (logger.isDebugEnabled()) { - logger.debug(String.format("Register function '%s' as %s", k, m.getClass())); - } + logger.debug("Register function '{}' as {}", k, m.getClass()); }); } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/AdvancedRandomNumberFunction.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/AdvancedRandomNumberFunction.java new file mode 100644 index 0000000000..a3e0fb4f73 --- /dev/null +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/AdvancedRandomNumberFunction.java @@ -0,0 +1,216 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.functions.core; + +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.exceptions.InvalidFunctionUsageException; +import org.citrusframework.functions.Function; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import static java.lang.String.format; + +/** + * A function for generating random double values with specified decimal places and range. This + * function includes options to specify the number of decimal places, minimum and maximum values, + * and whether to include or exclude the minimum and maximum values. + *

    + * Parameters: + *

      + *
    1. Decimal places: The number of decimal places in the generated random number (optional, default: 0). Note that definition of 0 results in an integer.
    2. + *
    3. Min value: The minimum value for the generated random number (optional, default: Double.MIN_VALUE).
    4. + *
    5. Max value: The maximum value for the generated random number (optional, default: Double.MAX_VALUE).
    6. + *
    7. Exclude min: Whether to exclude the minimum value (optional, default: false).
    8. + *
    9. Exclude max: Whether to exclude the maximum value (optional, default: false).
    10. + *
    11. Multiple of: The generated number will be a multiple of this value (optional).
    12. + *
    + *

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

      + *
    • It allows to specify several aspects of a number (see above).
    • + *
    • The length of the number is restricted to the range and precision of a double, whereas RandomNumberFunction can create arbitrarily long integer values.
    • + *
    + */ +public class AdvancedRandomNumberFunction implements Function { + + public static final BigDecimal DEFAULT_MAX_VALUE = new BigDecimal(1000000); + public static final BigDecimal DEFAULT_MIN_VALUE = DEFAULT_MAX_VALUE.negate(); + + public String execute(List parameterList, TestContext context) { + if (parameterList == null) { + throw new InvalidFunctionUsageException("Function parameters must not be null."); + } + + int decimalPlaces = getParameter(parameterList, 0, Integer.class, Integer::parseInt, 2); + if (decimalPlaces < 0) { + throw new InvalidFunctionUsageException( + "Decimal places must be a non-negative integer value."); + } + + BigDecimal minValue = getParameter(parameterList, 1, BigDecimal.class, BigDecimal::new, DEFAULT_MIN_VALUE); + BigDecimal maxValue = getParameter(parameterList, 2, BigDecimal.class, BigDecimal::new, DEFAULT_MAX_VALUE); + if (minValue.compareTo(maxValue) > 0) { + throw new InvalidFunctionUsageException("Min value must be less than max value."); + } + + boolean excludeMin = getParameter(parameterList, 3, Boolean.class, Boolean::parseBoolean, false); + boolean excludeMax = getParameter(parameterList, 4, Boolean.class, Boolean::parseBoolean, false); + BigDecimal multiple = getParameter(parameterList, 5, BigDecimal.class, BigDecimal::new, null); + + return getRandomNumber(decimalPlaces, minValue, maxValue, excludeMin, excludeMax, multiple); + } + + private T getParameter(List params, int index, Class type, + java.util.function.Function parser, T defaultValue) { + if (index < params.size()) { + String param = params.get(index); + return "null".equals(param) ? defaultValue + : parseParameter(index + 1, param, type, parser); + } + return defaultValue; + } + + private T parseParameter(int index, String text, Class type, + java.util.function.Function parseFunction) { + T value; + try { + value = parseFunction.apply(text); + if (value == null) { + throw new CitrusRuntimeException( + "Text '%s' could not be parsed to '%s'. Resulting value is null".formatted(text, type.getSimpleName())); + } + return value; + } catch (Exception e) { + throw new InvalidFunctionUsageException( + format("Invalid parameter at index %d. %s must be parsable to %s.", index, text, type.getSimpleName())); + } + } + + /** + * Static number generator method. + */ + private String getRandomNumber(int decimalPlaces, + BigDecimal minValue, + BigDecimal maxValue, + boolean excludeMin, + boolean excludeMax, + BigDecimal multiple) { + minValue = excludeMin ? incrementToExclude(minValue) : minValue; + maxValue = excludeMax ? decrementToExclude(maxValue) : maxValue; + + BigDecimal range = maxValue.subtract(minValue); + + BigDecimal randomValue; + if (multiple != null) { + randomValue = createMultipleOf(minValue, maxValue, multiple); + } else { + randomValue = createRandomValue(minValue, range, + ThreadLocalRandom.current().nextDouble()); + randomValue = randomValue.setScale(decimalPlaces, RoundingMode.HALF_UP); + } + + if (randomValue == null) { + // May only happen if multiple is out of range of min/max + return format("%s", Double.POSITIVE_INFINITY); + } + + return decimalPlaces == 0 ? + format("%s", randomValue.longValue()) : + format("%." + decimalPlaces + "f", randomValue.doubleValue()); + } + + // Pass in random for testing + BigDecimal createRandomValue(BigDecimal minValue, BigDecimal range, double random) { + BigDecimal offset = range.multiply(BigDecimal.valueOf(random)); + BigDecimal value = minValue.add(offset); + return value.compareTo(BigDecimal.valueOf(Double.MAX_VALUE)) > 0 ? BigDecimal.valueOf(Double.MAX_VALUE) + : value; + } + + private BigDecimal largestMultipleOf(BigDecimal highest, BigDecimal multipleOf) { + RoundingMode roundingMode = highest.compareTo(BigDecimal.ZERO) < 0 ? RoundingMode.UP : RoundingMode.DOWN; + BigDecimal factor = highest.divide(multipleOf, 0, roundingMode); + return multipleOf.multiply(factor); + } + + private BigDecimal lowestMultipleOf(BigDecimal lowest, BigDecimal multipleOf) { + RoundingMode roundingMode = lowest.compareTo(java.math.BigDecimal.ZERO) < 0 ? RoundingMode.DOWN : RoundingMode.UP; + BigDecimal factor = lowest.divide(multipleOf, 0, roundingMode); + return multipleOf.multiply(factor); + } + + private BigDecimal incrementToExclude(BigDecimal val) { + return val.add(determineIncrement(val)) + .setScale(findLeastSignificantDecimalPlace(val), RoundingMode.HALF_DOWN); + } + + private BigDecimal decrementToExclude(BigDecimal val) { + return val.subtract(determineIncrement(val)) + .setScale(findLeastSignificantDecimalPlace(val), RoundingMode.HALF_DOWN); + } + + private BigDecimal determineIncrement(BigDecimal number) { + return BigDecimal.valueOf(1.0d / (Math.pow(10d, findLeastSignificantDecimalPlace(number)))); + } + + private int findLeastSignificantDecimalPlace(BigDecimal number) { + number = number.stripTrailingZeros(); + + String[] parts = number.toPlainString().split("\\."); + + if (parts.length == 1) { + return 0; + } + + return parts[1].length(); + } + + private BigDecimal createMultipleOf(BigDecimal minimum, + BigDecimal maximum, + BigDecimal multipleOf) { + BigDecimal lowestMultiple = lowestMultipleOf(minimum, multipleOf); + BigDecimal largestMultiple = largestMultipleOf(maximum, multipleOf); + + // Check if there are no valid multiples in the range + if (lowestMultiple.compareTo(largestMultiple) > 0) { + return null; + } + + BigDecimal range = largestMultiple.subtract(lowestMultiple) + .divide(multipleOf, RoundingMode.DOWN); + + // Don't go for incredible large numbers + if (range.compareTo(BigDecimal.valueOf(11)) > 0) { + range = BigDecimal.valueOf(10); + } + + long factor = 0; + if (range.compareTo(BigDecimal.ZERO) != 0) { + factor = ThreadLocalRandom.current().nextLong(1, range.longValue() + 1); + } + BigDecimal randomMultiple = lowestMultiple.add( + multipleOf.multiply(BigDecimal.valueOf(factor))); + randomMultiple = randomMultiple.setScale(findLeastSignificantDecimalPlace(multipleOf), + RoundingMode.HALF_UP); + + return randomMultiple; + } +} diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/DecodeBase64Function.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/DecodeBase64Function.java index 35fc0d6c96..2c7c074c7e 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/core/DecodeBase64Function.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/DecodeBase64Function.java @@ -16,15 +16,15 @@ package org.citrusframework.functions.core; -import java.io.UnsupportedEncodingException; -import java.util.List; - import org.apache.commons.codec.binary.Base64; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.InvalidFunctionUsageException; import org.citrusframework.functions.Function; +import java.io.UnsupportedEncodingException; +import java.util.List; + /** * Decodes base64 binary data to a character sequence. * @@ -33,7 +33,7 @@ public class DecodeBase64Function implements Function { @Override public String execute(List parameterList, TestContext context) { - if (parameterList == null || parameterList.size() < 1) { + if (parameterList == null || parameterList.isEmpty()) { throw new InvalidFunctionUsageException("Invalid function parameter usage! Missing parameters!"); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/EncodeBase64Function.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/EncodeBase64Function.java index 2465903570..494c4b7b64 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/core/EncodeBase64Function.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/EncodeBase64Function.java @@ -16,15 +16,15 @@ package org.citrusframework.functions.core; -import java.io.UnsupportedEncodingException; -import java.util.List; - import org.apache.commons.codec.binary.Base64; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.InvalidFunctionUsageException; import org.citrusframework.functions.Function; +import java.io.UnsupportedEncodingException; +import java.util.List; + /** * Encodes a character sequence to base64 binary using given charset. * @@ -33,7 +33,7 @@ public class EncodeBase64Function implements Function { @Override public String execute(List parameterList, TestContext context) { - if (parameterList == null || parameterList.size() < 1) { + if (parameterList == null || parameterList.isEmpty()) { throw new InvalidFunctionUsageException("Invalid function parameter usage! Missing parameters!"); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomNumberFunction.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomNumberFunction.java index a3fa489e5a..aacd485b9d 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomNumberFunction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomNumberFunction.java @@ -16,13 +16,13 @@ package org.citrusframework.functions.core; -import java.util.List; -import java.util.Random; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.InvalidFunctionUsageException; import org.citrusframework.functions.Function; +import java.util.List; +import java.util.Random; + import static java.lang.Boolean.parseBoolean; import static java.lang.Integer.parseInt; @@ -113,7 +113,7 @@ private static String removeLeadingZeros(String generated) { } } - if (builder.length() == 0) { + if (builder.isEmpty()) { // very unlikely to happen, ensures that empty string is not returned builder.append('0'); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomPatternFunction.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomPatternFunction.java new file mode 100644 index 0000000000..7fd8ec7b94 --- /dev/null +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomPatternFunction.java @@ -0,0 +1,56 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.functions.core; + +import com.mifmif.common.regex.Generex; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.InvalidFunctionUsageException; +import org.citrusframework.functions.Function; + +import java.util.List; + +import static org.citrusframework.util.StringUtils.hasText; + +/** + * The RandomPatternFunction generates a random string based on a provided regular expression pattern. + * It uses the Generex library to generate the random string. + *

    + * Note: The Generex library has limitations in its ability to generate all possible expressions + * from a given regular expression. It may not support certain complex regex features or produce all + * possible variations. + */ +public class RandomPatternFunction implements Function { + + public String execute(List parameterList, TestContext context) { + if (parameterList == null) { + throw new InvalidFunctionUsageException("Function parameters must not be null."); + } + + String pattern = parameterList.get(0); + + if (!hasText(pattern)) { + throw new InvalidFunctionUsageException("Pattern must not be empty."); + } + + if (!Generex.isValidPattern(pattern)) { + throw new IllegalArgumentException( + "Function called with a pattern, the algorithm is not able to create a string for."); + } + + return new Generex(pattern).random(); + } +} diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomStringFunction.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomStringFunction.java index 099c13df55..75064ae56b 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomStringFunction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/RandomStringFunction.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.InvalidFunctionUsageException; import org.citrusframework.functions.Function; @@ -32,7 +33,8 @@ * */ public class RandomStringFunction implements Function { - private static Random generator = new Random(System.currentTimeMillis()); + + private static final Random generator = new Random(System.currentTimeMillis()); private static final char[] ALPHABET_UPPER = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', @@ -67,12 +69,13 @@ public String execute(List parameterList, TestContext context) { int numberOfLetters; String notationMethod = MIXED; boolean includeNumbers = false; + int minNumberOfLetters = -1; if (parameterList == null || parameterList.isEmpty()) { throw new InvalidFunctionUsageException("Function parameters must not be empty"); } - if (parameterList.size() > 3) { + if (parameterList.size() > 4) { throw new InvalidFunctionUsageException("Too many parameters for function"); } @@ -89,12 +92,16 @@ public String execute(List parameterList, TestContext context) { includeNumbers = parseBoolean(parameterList.get(2)); } + if (parameterList.size() > 3) { + minNumberOfLetters = parseInt(parameterList.get(3)); + } + if (notationMethod.equals(UPPERCASE)) { - return getRandomString(numberOfLetters, ALPHABET_UPPER, includeNumbers); + return getRandomString(numberOfLetters, ALPHABET_UPPER, includeNumbers, minNumberOfLetters); } else if (notationMethod.equals(LOWERCASE)) { - return getRandomString(numberOfLetters, ALPHABET_LOWER, includeNumbers); + return getRandomString(numberOfLetters, ALPHABET_LOWER, includeNumbers, minNumberOfLetters); } else { - return getRandomString(numberOfLetters, ALPHABET_MIXED, includeNumbers); + return getRandomString(numberOfLetters, ALPHABET_MIXED, includeNumbers, minNumberOfLetters); } } @@ -105,7 +112,7 @@ public String execute(List parameterList, TestContext context) { * @param includeNumbers * @return */ - public static String getRandomString(int numberOfLetters, char[] alphabet, boolean includeNumbers) { + public static String getRandomString(int numberOfLetters, char[] alphabet, boolean includeNumbers, int minNumberOfLetters) { StringBuilder builder = new StringBuilder(); int upperRange = alphabet.length - 1; @@ -117,6 +124,11 @@ public static String getRandomString(int numberOfLetters, char[] alphabet, boole upperRange += NUMBERS.length; } + if (minNumberOfLetters > -1) { + numberOfLetters = ThreadLocalRandom.current() + .nextInt(minNumberOfLetters, numberOfLetters + 1); + } + for (int i = 1; i < numberOfLetters; i++) { int letterIndex = generator.nextInt(upperRange); diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/ReadFileResourceFunction.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/ReadFileResourceFunction.java index 710600b8c1..2d6a554039 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/core/ReadFileResourceFunction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/ReadFileResourceFunction.java @@ -16,9 +16,6 @@ package org.citrusframework.functions.core; -import java.io.IOException; -import java.util.List; - import org.apache.commons.codec.binary.Base64; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -26,12 +23,15 @@ import org.citrusframework.functions.Function; import org.citrusframework.util.FileUtils; +import java.io.IOException; +import java.util.List; + /** * Function reads file from given file path and returns the complete file content as function result. * File content is automatically parsed for test variables. - * + *

    * File path can also have test variables as part of the file name or path. - * + *

    * The function accepts the following parameters: * *

      diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/SubstringFunction.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/SubstringFunction.java index a5f23b5973..f6270f56cc 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/core/SubstringFunction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/SubstringFunction.java @@ -16,18 +16,18 @@ package org.citrusframework.functions.core; -import java.util.List; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.InvalidFunctionUsageException; import org.citrusframework.functions.Function; +import java.util.List; + import static java.lang.Integer.parseInt; import static org.citrusframework.util.StringUtils.hasText; /** * Function implements simple substring functionality. - * + *

      * Function requires at least a target string and a beginIndex as function parameters. A * optional endIndex may be given as function parameter, too. The parameter usage looks * like this: substring(targetString, beginIndex, [endIndex]). diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/UrlDecodeFunction.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/UrlDecodeFunction.java index 0f3abd5c5c..13bb58756c 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/core/UrlDecodeFunction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/UrlDecodeFunction.java @@ -16,15 +16,15 @@ package org.citrusframework.functions.core; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.List; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.InvalidFunctionUsageException; import org.citrusframework.functions.Function; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.List; + /** * Decodes URL encoded string to a character sequence. * @@ -33,7 +33,7 @@ public class UrlDecodeFunction implements Function { @Override public String execute(List parameterList, TestContext context) { - if (parameterList == null || parameterList.size() < 1) { + if (parameterList == null || parameterList.isEmpty()) { throw new InvalidFunctionUsageException("Invalid function parameter usage! Missing parameters!"); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/functions/core/UrlEncodeFunction.java b/core/citrus-base/src/main/java/org/citrusframework/functions/core/UrlEncodeFunction.java index 293cebb0e6..aad80ba6b3 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/functions/core/UrlEncodeFunction.java +++ b/core/citrus-base/src/main/java/org/citrusframework/functions/core/UrlEncodeFunction.java @@ -16,15 +16,15 @@ package org.citrusframework.functions.core; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.List; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.InvalidFunctionUsageException; import org.citrusframework.functions.Function; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.List; + /** * Encodes a character sequence to URL encoded string using given charset. * @@ -33,7 +33,7 @@ public class UrlEncodeFunction implements Function { @Override public String execute(List parameterList, TestContext context) { - if (parameterList == null || parameterList.size() < 1) { + if (parameterList == null || parameterList.isEmpty()) { throw new InvalidFunctionUsageException("Invalid function parameter usage! Missing parameters!"); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/log/DefaultLogModifier.java b/core/citrus-base/src/main/java/org/citrusframework/log/DefaultLogModifier.java index d99d2fd5ca..d14fc2c58e 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/log/DefaultLogModifier.java +++ b/core/citrus-base/src/main/java/org/citrusframework/log/DefaultLogModifier.java @@ -16,12 +16,12 @@ package org.citrusframework.log; +import org.citrusframework.CitrusSettings; + import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.citrusframework.CitrusSettings; - /** * Default modifier implementation uses regular expressions to mask logger output. * Regular expressions match on default keywords. @@ -44,7 +44,7 @@ public class DefaultLogModifier implements LogMessageModifier { @Override public String mask(String source) { - if (!CitrusSettings.isLogModifierEnabled() || source == null || source.length() == 0) { + if (!CitrusSettings.isLogModifierEnabled() || source == null || source.isEmpty()) { return source; } diff --git a/core/citrus-base/src/main/java/org/citrusframework/main/scan/ClassPathTestScanner.java b/core/citrus-base/src/main/java/org/citrusframework/main/scan/ClassPathTestScanner.java index 2bccded319..f6bc7fc907 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/main/scan/ClassPathTestScanner.java +++ b/core/citrus-base/src/main/java/org/citrusframework/main/scan/ClassPathTestScanner.java @@ -16,6 +16,14 @@ package org.citrusframework.main.scan; +import org.citrusframework.TestClass; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.ClasspathResourceResolver; +import org.citrusframework.util.FileUtils; +import org.citrusframework.util.ReflectionHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.lang.annotation.Annotation; import java.nio.file.Path; @@ -25,14 +33,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; -import org.citrusframework.TestClass; -import org.citrusframework.exceptions.CitrusRuntimeException; -import org.citrusframework.spi.ClasspathResourceResolver; -import org.citrusframework.util.FileUtils; -import org.citrusframework.util.ReflectionHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * @since 2.7.4 */ @@ -97,7 +97,7 @@ protected boolean isIncluded(String className) { }); return hasTestMethod.get(); } catch (NoClassDefFoundError | ClassNotFoundException e) { - logger.warn("Unable to access class: " + className); + logger.warn("Unable to access class: {}", className); return false; } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/message/DefaultMessageQueue.java b/core/citrus-base/src/main/java/org/citrusframework/message/DefaultMessageQueue.java index 607990f568..091d60ea13 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/message/DefaultMessageQueue.java +++ b/core/citrus-base/src/main/java/org/citrusframework/message/DefaultMessageQueue.java @@ -15,12 +15,12 @@ */ package org.citrusframework.message; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + /** * Default message queue implementation. Holds queued messages in memory and adds selective consumption of messages * according to a message selector implementation. @@ -76,8 +76,7 @@ public Message receive(MessageSelector selector, long timeout) { timeLeft -= pollingInterval; if (RETRY_LOG.isDebugEnabled()) { - RETRY_LOG.debug("No message received with message selector - retrying in " + - (timeLeft > 0 ? pollingInterval : pollingInterval + timeLeft) + "ms"); + RETRY_LOG.debug("No message received with message selector - retrying in {}ms", timeLeft > 0 ? pollingInterval : pollingInterval + timeLeft); } try { @@ -99,11 +98,9 @@ public void purge(MessageSelector selector) { Message message = (Message) o; if (selector.accept(message)) { if (this.queue.remove(message)) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Purged message '%s' from in memory queue", message.getId())); - } + logger.debug("Purged message '{}' from in memory queue", message.getId()); } else { - logger.warn(String.format("Failed to purge message '%s' from in memory queue", message.getId())); + logger.warn("Failed to purge message '{}' from in memory queue", message.getId()); } } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/message/ZipMessage.java b/core/citrus-base/src/main/java/org/citrusframework/message/ZipMessage.java index f2a1ac6f31..f63361d4dc 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/message/ZipMessage.java +++ b/core/citrus-base/src/main/java/org/citrusframework/message/ZipMessage.java @@ -16,6 +16,13 @@ package org.citrusframework.message; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.Resource; +import org.citrusframework.util.FileUtils; +import org.citrusframework.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -26,13 +33,6 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import org.citrusframework.exceptions.CitrusRuntimeException; -import org.citrusframework.spi.Resource; -import org.citrusframework.util.FileUtils; -import org.citrusframework.util.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * @since 2.7.5 */ @@ -139,7 +139,7 @@ public ZipMessage addEntry(Entry entry) { private void addToZip(String path, Entry entry, ZipOutputStream zos) throws IOException { String name = (path.endsWith("/") ? path : path + "/") + entry.getName(); if (entry.isDirectory()) { - logger.debug("Adding directory to zip: " + name); + logger.debug("Adding directory to zip: {}", name); zos.putNextEntry(new ZipEntry(name.endsWith("/") ? name : name + "/")); for (Entry child : entry.getEntries()) { @@ -151,7 +151,7 @@ private void addToZip(String path, Entry entry, ZipOutputStream zos) throws IOEx } zos.closeEntry(); } else { - logger.debug("Adding file to zip: " + name); + logger.debug("Adding file to zip: {}", name); zos.putNextEntry(new ZipEntry(name)); zos.write(entry.getContent()); diff --git a/core/citrus-base/src/main/java/org/citrusframework/message/correlation/DefaultCorrelationManager.java b/core/citrus-base/src/main/java/org/citrusframework/message/correlation/DefaultCorrelationManager.java index ffea23f0a1..29773dc60d 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/message/correlation/DefaultCorrelationManager.java +++ b/core/citrus-base/src/main/java/org/citrusframework/message/correlation/DefaultCorrelationManager.java @@ -37,18 +37,13 @@ public class DefaultCorrelationManager implements CorrelationManager { @Override public void saveCorrelationKey(String correlationKeyName, String correlationKey, TestContext context) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Saving correlation key for '%s'", correlationKeyName)); - } - + logger.debug("Saving correlation key for '{}'", correlationKeyName); context.setVariable(correlationKeyName, correlationKey); } @Override public String getCorrelationKey(String correlationKeyName, TestContext context) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Get correlation key for '%s'", correlationKeyName)); - } + logger.debug("Get correlation key for '{}'", correlationKeyName); if (context.getVariables().containsKey(correlationKeyName)) { return context.getVariable(correlationKeyName); @@ -60,23 +55,18 @@ public String getCorrelationKey(String correlationKeyName, TestContext context) @Override public void store(String correlationKey, T object) { if (object == null) { - logger.warn(String.format("Ignore correlated null object for '%s'", correlationKey)); + logger.warn("Ignore correlated null object for '{}'", correlationKey); return; } - if (logger.isDebugEnabled()) { - logger.debug(String.format("Saving correlated object for '%s'", correlationKey)); - } + logger.debug("Saving correlated object for '{}'", correlationKey); objectStore.add(correlationKey, object); } @Override public T find(String correlationKey, long timeout) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Finding correlated object for '%s'", correlationKey)); - } - + logger.debug("Finding correlated object for '{}'", correlationKey); return objectStore.remove(correlationKey); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/message/correlation/PollingCorrelationManager.java b/core/citrus-base/src/main/java/org/citrusframework/message/correlation/PollingCorrelationManager.java index 4c445b7c1d..991d9bf6c6 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/message/correlation/PollingCorrelationManager.java +++ b/core/citrus-base/src/main/java/org/citrusframework/message/correlation/PollingCorrelationManager.java @@ -62,9 +62,7 @@ public T find(String correlationKey) { @Override public String getCorrelationKey(String correlationKeyName, TestContext context) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Get correlation key for '%s'", correlationKeyName)); - } + logger.debug("Get correlation key for '{}'", correlationKeyName); String correlationKey = null; if (context.getVariables().containsKey(correlationKeyName)) { @@ -77,7 +75,7 @@ public String getCorrelationKey(String correlationKeyName, TestContext context) timeLeft -= pollingInterval; if (RETRY_LOG.isDebugEnabled()) { - RETRY_LOG.debug("Correlation key not available yet - retrying in " + (timeLeft > 0 ? pollingInterval : pollingInterval + timeLeft) + "ms"); + RETRY_LOG.debug("Correlation key not available yet - retrying in {}ms", timeLeft > 0 ? pollingInterval : pollingInterval + timeLeft); } try { @@ -109,7 +107,7 @@ public T find(String correlationKey, long timeout) { timeLeft -= pollingInterval; if (RETRY_LOG.isDebugEnabled()) { - RETRY_LOG.debug(retryLogMessage + " - retrying in " + (timeLeft > 0 ? pollingInterval : pollingInterval + timeLeft) + "ms"); + RETRY_LOG.debug("{} - retrying in {}ms", retryLogMessage, timeLeft > 0 ? pollingInterval : pollingInterval + timeLeft); } try { diff --git a/core/citrus-base/src/main/java/org/citrusframework/report/AbstractOutputFileReporter.java b/core/citrus-base/src/main/java/org/citrusframework/report/AbstractOutputFileReporter.java index 72f3265347..4a056888ff 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/report/AbstractOutputFileReporter.java +++ b/core/citrus-base/src/main/java/org/citrusframework/report/AbstractOutputFileReporter.java @@ -16,15 +16,15 @@ package org.citrusframework.report; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; -import org.citrusframework.exceptions.CitrusRuntimeException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * @since 2.7.4 */ @@ -64,7 +64,7 @@ private void createReportFile(String reportFileName, String content) { try (Writer fileWriter = new FileWriter(new File(targetDirectory, reportFileName))) { fileWriter.append(content); fileWriter.flush(); - logger.info("Generated test report: " + targetDirectory + File.separator + reportFileName); + logger.info("Generated test report: {}{}{}", targetDirectory, File.separator, reportFileName); } catch (IOException e) { logger.error("Failed to create test report", e); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/report/HtmlReporter.java b/core/citrus-base/src/main/java/org/citrusframework/report/HtmlReporter.java index e093ce1e96..84ae87edef 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/report/HtmlReporter.java +++ b/core/citrus-base/src/main/java/org/citrusframework/report/HtmlReporter.java @@ -16,16 +16,6 @@ package org.citrusframework.report; -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.text.DateFormat; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.Properties; - import org.apache.commons.codec.binary.Base64; import org.citrusframework.TestCase; import org.citrusframework.TestCaseMetaInfo; @@ -37,6 +27,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.text.DateFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; + /** * Basic logging reporter generating a HTML report with detailed test results. * @@ -243,7 +243,7 @@ private String getStackTraceHtml(Throwable cause) { } return "" + - "

      " + stackTraceBuilder.toString() +
      +        		"
      " + stackTraceBuilder +
               		"
      " + getCodeSnippetHtml(cause) + "
      "; } diff --git a/core/citrus-base/src/main/java/org/citrusframework/report/MessageTracingTestListener.java b/core/citrus-base/src/main/java/org/citrusframework/report/MessageTracingTestListener.java index 771973daa4..65ab37fd2d 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/report/MessageTracingTestListener.java +++ b/core/citrus-base/src/main/java/org/citrusframework/report/MessageTracingTestListener.java @@ -16,15 +16,6 @@ package org.citrusframework.report; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - import org.citrusframework.CitrusSettings; import org.citrusframework.TestCase; import org.citrusframework.context.TestContext; @@ -34,10 +25,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + /** * Test listener collects all messages sent and received by Citrus during test execution. Listener * writes a trace file with all message content per test case to a output directory. - * + *

      * Note: This class is not thread safe! Parallel test execution leads to behaviour that messages get mixed. * Proper correlation to test case is not possible here. * @@ -129,7 +129,7 @@ private String separator() { * @return */ private String newLine() { - return System.getProperty("line.separator"); + return System.lineSeparator(); } /** @@ -151,7 +151,7 @@ protected File getTraceFile(String testName) { File traceFile = new File(targetDirectory, filename); if (traceFile.exists()) { - logger.warn(String.format("Trace file '%s' already exists. Normally a new file is created on each test execution ", traceFile.getName())); + logger.warn("Trace file '{}' already exists. Normally a new file is created on each test execution ", traceFile.getName()); } return traceFile; } diff --git a/core/citrus-base/src/main/java/org/citrusframework/repository/BaseRepository.java b/core/citrus-base/src/main/java/org/citrusframework/repository/BaseRepository.java new file mode 100644 index 0000000000..eae38431bd --- /dev/null +++ b/core/citrus-base/src/main/java/org/citrusframework/repository/BaseRepository.java @@ -0,0 +1,107 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.repository; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.citrusframework.common.InitializingPhase; +import org.citrusframework.common.Named; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.ClasspathResourceResolver; +import org.citrusframework.spi.Resource; +import org.citrusframework.spi.Resources; +import org.citrusframework.util.FileUtils; +import org.citrusframework.util.StringUtils; + +/** + * Base class for repositories providing common functionality for initializing and managing resources. + * Implementations must provide the logic for loading and adding resources to the repository. + */ +public abstract class BaseRepository implements Named, InitializingPhase { + + private String name; + + /** List of location patterns that will be translated to schema resources */ + private List locations = new ArrayList<>(); + + protected BaseRepository(String name) { + this.name = name; + } + + @Override + public void initialize() { + try { + ClasspathResourceResolver resourceResolver = new ClasspathResourceResolver(); + for (String location : locations) { + Resource found = Resources.create(location); + if (found.exists()) { + addRepository(found); + } else { + Set findings; + if (StringUtils.hasText(FileUtils.getFileExtension(location))) { + String fileNamePattern = FileUtils.getFileName(location).replace(".", "\\.").replace("*", ".*"); + String basePath = FileUtils.getBasePath(location); + findings = resourceResolver.getResources(basePath, fileNamePattern); + } else { + findings = resourceResolver.getResources(location); + } + + for (Path resource : findings) { + addRepository(Resources.fromClasspath(resource.toString())); + } + } + } + } catch (IOException e) { + throw new CitrusRuntimeException("Failed to initialize repository", e); + } + } + + protected abstract void addRepository(Resource resource); + + @Override + public void setName(String name) { + this.name = name; + } + + /** + * Gets the name. + * @return the name to get. + */ + public String getName() { + return name; + } + + /** + * Gets the locations. + * @return the locations to get. + */ + public List getLocations() { + return locations; + } + + /** + * Sets the locations. + * @param locations the locations to set + */ + public void setLocations(List locations) { + this.locations = locations; + } + +} diff --git a/core/citrus-base/src/main/java/org/citrusframework/server/AbstractServer.java b/core/citrus-base/src/main/java/org/citrusframework/server/AbstractServer.java index d9d1d14b3f..a5b7b8adff 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/server/AbstractServer.java +++ b/core/citrus-base/src/main/java/org/citrusframework/server/AbstractServer.java @@ -16,9 +16,6 @@ package org.citrusframework.server; -import java.util.ArrayList; -import java.util.List; - import org.citrusframework.common.InitializingPhase; import org.citrusframework.common.ShutdownPhase; import org.citrusframework.context.TestContextFactory; @@ -36,6 +33,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; + /** * Abstract base class for {@link Server} implementations. * @@ -85,9 +85,7 @@ public AbstractServer() { @Override public void start() { - if (logger.isDebugEnabled()) { - logger.debug("Starting server: " + getName() + " ..."); - } + logger.debug("Starting server: {} ...", getName()); startup(); @@ -99,15 +97,13 @@ public void start() { thread.setDaemon(false); thread.start(); - logger.info("Started server: " + getName()); + logger.info("Started server: {}", getName()); } @Override public void stop() { if (isRunning()) { - if (logger.isDebugEnabled()) { - logger.debug("Stopping server: " + getName() + " ..."); - } + logger.debug("Stopping server: {} ...", getName()); shutdown(); @@ -117,7 +113,7 @@ public void stop() { thread = null; - logger.info("Stopped server: " + getName()); + logger.info("Stopped server: {}", getName()); } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/sharding/ShardingConfiguration.java b/core/citrus-base/src/main/java/org/citrusframework/sharding/ShardingConfiguration.java index b203f4e4c0..85ccd5963b 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/sharding/ShardingConfiguration.java +++ b/core/citrus-base/src/main/java/org/citrusframework/sharding/ShardingConfiguration.java @@ -109,7 +109,7 @@ public ShardingConfiguration() { * * @param systemProvider a provider for system environment variables and properties. */ - protected ShardingConfiguration(SystemProvider systemProvider) { + ShardingConfiguration(SystemProvider systemProvider) { this(getTotalNumberOfShards(systemProvider), getShardNumber(systemProvider), systemProvider); } @@ -131,7 +131,7 @@ public ShardingConfiguration(int totalNumberOfShards, int shardNumber) { * @param shardNumber the shard number for this loader, zero-based. * @param systemProvider a provider for system environment variables and properties. */ - protected ShardingConfiguration(int totalNumberOfShards, int shardNumber, SystemProvider systemProvider) { + private ShardingConfiguration(int totalNumberOfShards, int shardNumber, SystemProvider systemProvider) { this.totalNumberOfShards = totalNumberOfShards; this.shardNumber = shardNumber; diff --git a/core/citrus-base/src/main/java/org/citrusframework/util/BooleanExpressionParser.java b/core/citrus-base/src/main/java/org/citrusframework/util/BooleanExpressionParser.java index c33f14288f..09e91ef776 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/util/BooleanExpressionParser.java +++ b/core/citrus-base/src/main/java/org/citrusframework/util/BooleanExpressionParser.java @@ -127,9 +127,7 @@ public static boolean evaluate(final String expression) { result = parseBoolean(evaluateExpressionStack(operators, values)); - if (logger.isDebugEnabled()) { - logger.debug("Boolean expression {} evaluates to {}", expression, result); - } + logger.debug("Boolean expression {} evaluates to {}", expression, result); } catch (final NoSuchElementException e) { throw new CitrusRuntimeException("Unable to parse boolean expression '" + expression + "'. Maybe expression is incomplete!", e); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/util/FileUtils.java b/core/citrus-base/src/main/java/org/citrusframework/util/FileUtils.java index d17c2a805d..b70b2b4e7a 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/util/FileUtils.java +++ b/core/citrus-base/src/main/java/org/citrusframework/util/FileUtils.java @@ -16,6 +16,15 @@ package org.citrusframework.util; +import org.citrusframework.CitrusSettings; +import org.citrusframework.TestSource; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.Resource; +import org.citrusframework.spi.Resources; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; @@ -32,15 +41,6 @@ import java.util.Set; import java.util.Stack; -import org.citrusframework.CitrusSettings; -import org.citrusframework.TestSource; -import org.citrusframework.context.TestContext; -import org.citrusframework.exceptions.CitrusRuntimeException; -import org.citrusframework.spi.Resource; -import org.citrusframework.spi.Resources; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Class to provide general file utilities, such as listing all XML files in a directory, * or finding certain tests in a directory. @@ -121,9 +121,8 @@ public static String readToString(Resource resource, Charset charset) throws IOE throw new CitrusRuntimeException("Failed to read resource %s - does not exist".formatted(resource.getLocation())); } - if (logger.isDebugEnabled()) { - logger.debug(String.format("Reading file resource: '%s' (encoding is '%s')", resource.getLocation(), charset.displayName())); - } + logger.debug("Reading file resource: '{}' (encoding is '{}')", resource.getLocation(), charset.displayName()); + return readToString(resource.getInputStream(), charset); } @@ -148,9 +147,7 @@ public static String readToString(InputStream inputStream, Charset charset) thro * @param file */ public static void writeToFile(InputStream inputStream, File file) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Writing file resource: '%s'", file.getName())); - } + logger.debug("Writing file resource: '{}'", file.getName()); if (!file.getParentFile().exists()) { if (!file.getParentFile().mkdirs()) { @@ -180,9 +177,7 @@ public static void writeToFile(String content, File file) { * @param file */ public static void writeToFile(String content, File file, Charset charset) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Writing file resource: '%s' (encoding is '%s')", file.getName(), charset.displayName())); - } + logger.debug("Writing file resource: '{}' (encoding is '{}')", file.getName(), charset.displayName()); if (!file.getParentFile().exists()) { if (!file.getParentFile().mkdirs()) { @@ -224,7 +219,7 @@ public static List findFiles(final String startDir, final Set file } /* walk through the directories */ - while (dirs.size() > 0) { + while (!dirs.isEmpty()) { final File file = dirs.pop(); File[] foundFiles = file.listFiles((dir, name) -> { File tmp = new File(dir.getPath() + File.separator + name); diff --git a/core/citrus-base/src/main/java/org/citrusframework/util/PropertyUtils.java b/core/citrus-base/src/main/java/org/citrusframework/util/PropertyUtils.java index 43788a1abf..588616bfb5 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/util/PropertyUtils.java +++ b/core/citrus-base/src/main/java/org/citrusframework/util/PropertyUtils.java @@ -16,12 +16,12 @@ package org.citrusframework.util; -import java.io.IOException; -import java.util.Properties; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.Resource; +import java.io.IOException; +import java.util.Properties; + /** * Utility class supporting property replacement in template files. * For usage see doc generators and test case creator. @@ -99,7 +99,7 @@ public static String replacePropertiesInString(final String line, Properties pro if (!properties.containsKey(propertyName.toString())) { throw new CitrusRuntimeException("No such property '" - + PROPERTY_MARKER + propertyName.toString() + PROPERTY_MARKER + "'"); + + PROPERTY_MARKER + propertyName + PROPERTY_MARKER + "'"); } newStr.append(line, startIndex, searchIndex); diff --git a/core/citrus-base/src/main/java/org/citrusframework/util/StringUtils.java b/core/citrus-base/src/main/java/org/citrusframework/util/StringUtils.java index cfb32699c2..c3f0d1f383 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/util/StringUtils.java +++ b/core/citrus-base/src/main/java/org/citrusframework/util/StringUtils.java @@ -16,11 +16,15 @@ package org.citrusframework.util; +import java.util.Locale; + /** * Utility helper class for Strings. */ public class StringUtils { + public static final String URL_PATH_SEPARATOR = "/"; + private StringUtils() { //prevent instantiation of utility class } @@ -32,10 +36,100 @@ public static boolean hasText(String str) { return str != null && !str.isBlank(); } + /** + * Helper method checks for null or blank String. + */ + public static boolean hasNoText(String str) { + return !hasText(str); + } + /** * String helper checking for isEmpty String and adds null check on given parameter. */ public static boolean isEmpty(String str) { return str == null || str.isEmpty(); } + + /** + * String helper checking for isEmpty String and adds null check on given parameter. + */ + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * Appends a URL segment to an existing URL path. + *

      + * This method ensures that the provided segment is appended correctly to the base path, + * handling cases where there may or may not be existing separators (`/`). It avoids duplicate + * separators and ensures proper formatting of the resulting URL. + *

      + * + * @param path the base URL path to which the segment will be appended. If {@code null}, the segment will be returned. + * @param segment the segment to append to the base path. If {@code null}, the original path will be returned. + * @return the combined URL path with the appended segment, properly formatted. + */ + public static String appendSegmentToUrlPath(String path, String segment) { + if (path == null) { + return segment; + } + + if (isEmpty(segment)) { + return path; + } + + if (!path.endsWith(URL_PATH_SEPARATOR)) { + path = path + URL_PATH_SEPARATOR; + } + + if (segment.startsWith(URL_PATH_SEPARATOR)) { + segment = segment.substring(1); + } + + return path + segment; + } + + public static String quote(String text, boolean quote) { + return quote ? "\"" + text + "\"" : text; + } + + /** + * Trims trailing whitespace characters and the first trailing comma from the end of the given StringBuilder. + *

      + * This method removes all trailing whitespace characters (such as spaces, tabs, and newline characters) + * and the first trailing comma found from the end of the content in the provided StringBuilder. + * Any additional commas or whitespace characters after the first trailing comma are not removed. + * + * @param builder the StringBuilder whose trailing whitespace characters and first comma are to be removed + */ + public static void trimTrailingComma(StringBuilder builder) { + int length = builder.length(); + while (length > 0 && (builder.charAt(length - 1) == ',' || Character.isWhitespace(builder.charAt(length - 1)))) { + char c = builder.charAt(length - 1); + builder.deleteCharAt(length - 1); + + if (c == ',') { + return; + } + + length = builder.length(); + } + } + + /** + * Converts the first letter of the given input string to uppercase while leaving + * the rest of the string unchanged. If the input string is empty or null, + * an empty string is returned. + * + * @param input The string to be converted. It can be null or empty. + * @return the string in with upper case first letter + */ + public static String convertFirstChartToUpperCase(String input) { + if (input != null && !input.isEmpty()) { + String firstLetter = input.substring(0, 1).toUpperCase(Locale.ROOT); + return input.length() == 1 ? firstLetter : firstLetter + input.substring(1); + } else { + return ""; + } + } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/util/SystemProvider.java b/core/citrus-base/src/main/java/org/citrusframework/util/SystemProvider.java index 73483d824f..c8713a53ab 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/util/SystemProvider.java +++ b/core/citrus-base/src/main/java/org/citrusframework/util/SystemProvider.java @@ -29,4 +29,8 @@ public Optional getEnv(String envVarName) { public Optional getProperty(String propertyName) { return ofNullable(System.getProperty(propertyName)); } + + public void setProperty(String propertyName, String propertyValue) { + System.setProperty(propertyName, propertyValue); + } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/util/TestUtils.java b/core/citrus-base/src/main/java/org/citrusframework/util/TestUtils.java index 67ba7afd8a..8299a0db3d 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/util/TestUtils.java +++ b/core/citrus-base/src/main/java/org/citrusframework/util/TestUtils.java @@ -16,13 +16,9 @@ package org.citrusframework.util; -import org.citrusframework.CitrusSettings; -import org.citrusframework.Completable; -import org.citrusframework.context.TestContext; -import org.citrusframework.exceptions.CitrusRuntimeException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -30,6 +26,12 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.citrusframework.CitrusSettings; +import org.citrusframework.Completable; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Utility class for test cases providing several utility @@ -38,6 +40,8 @@ */ public abstract class TestUtils { + public static final String HTTPS_CITRUSFRAMEWORK_ORG = "https://citrusframework.org"; + /** Used to identify waiting task threads pool */ public static final String WAIT_THREAD_PREFIX = "citrus-waiting-"; @@ -93,7 +97,7 @@ public static void waitForCompletion(final ScheduledExecutorService scheduledExe if (logger.isDebugEnabled()) { logger.debug("Failed to wait for completion of nested test actions", e); } else { - logger.warn(String.format("Failed to wait for completion of nested test actions because of %s", e.getMessage())); + logger.warn("Failed to wait for completion of nested test actions because of {}", e.getMessage()); } } }, 100L, timeout / 10, TimeUnit.MILLISECONDS); @@ -110,7 +114,7 @@ public static void waitForCompletion(final ScheduledExecutorService scheduledExe scheduledExecutor.shutdown(); scheduledExecutor.awaitTermination((timeout / 10) / 2, TimeUnit.MICROSECONDS); } catch (InterruptedException e) { - logger.warn(String.format("Failed to await orderly termination of waiting tasks to complete, caused by %s", e.getMessage())); + logger.warn("Failed to await orderly termination of waiting tasks to complete, caused by {}", e.getMessage()); } if (!scheduledExecutor.isTerminated()) { @@ -137,4 +141,18 @@ private static Thread createWaitingThread(final Runnable runnable, TestContext c } return waitThread; } + + public static boolean isNetworkReachable() { + try { + URL url = new URL(HTTPS_CITRUSFRAMEWORK_ORG); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + int responseCode = connection.getResponseCode(); + return responseCode == HttpURLConnection.HTTP_OK; + } catch (IOException e) { + return false; + } + } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultHeaderValidator.java b/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultHeaderValidator.java index 85e223f2fe..d0233cfec9 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultHeaderValidator.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultHeaderValidator.java @@ -16,10 +16,6 @@ package org.citrusframework.validation; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.ValidationException; import org.citrusframework.util.StringUtils; @@ -28,6 +24,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + /** * @since 2.7.6 */ @@ -73,9 +77,98 @@ public void validateHeader(String headerName, Object receivedValue, Object contr + null + "'"); } - if (logger.isDebugEnabled()) { - logger.debug("Validating header element: " + headerName + "='" + expectedValue + "': OK"); + logger.debug("Validating header element: {}='{}' : OK", headerName, expectedValue); + } + + public void validateHeaderArray(String headerName, Object receivedValue, Object controlValue, TestContext context, HeaderValidationContext validationContext) { + Optional validator = getHeaderValidator(headerName, controlValue, context); + if (validator.isPresent()) { + validator.get().validateHeader(headerName, receivedValue, controlValue, context, validationContext); + return; } + + List receivedValues = toList(receivedValue); + List controlValues = toList(controlValue); + + // Convert and replace dynamic content for controlValue + List expectedValues = controlValues.stream() + .map(value -> context.getTypeConverter().convertIfNecessary(value, String.class)) + .map(context::replaceDynamicContentInString) + .toList(); + + // Process received values + if (receivedValue != null) { + List receivedValueStrings = receivedValues.stream() + .map(value -> context.getTypeConverter().convertIfNecessary(value, String.class)) + .toList(); + + List expectedValuesCopy = new ArrayList<>(expectedValues); + + // Iterate over received values and try to match with expected values + for (String receivedValueString : receivedValueStrings) { + + Iterator expectedIterator = expectedValuesCopy.iterator(); + boolean validated = validateExpected(headerName, context, receivedValueString, expectedIterator); + + if (!validated) { + throw new ValidationException(String.format("Values not equal for header element '%s', expected '%s' but was '%s'", + headerName, String.join(", ", expectedValues), receivedValueString)); + } + } + + if (!expectedValuesCopy.isEmpty()) { + throw new ValidationException(String.format("Values not equal for header element '%s', expected '%s' but was '%s'", + headerName, String.join(", ", expectedValues), String.join(", ", receivedValues))); + } + } else if (!expectedValues.isEmpty()) { + throw new ValidationException(String.format("Values not equal for header element '%s', expected '%s' but was 'null'", + headerName, String.join(", ", expectedValues))); + } + + logger.debug("Validating header element: {}='{}' : OK", headerName, String.join(", ", expectedValues)); + } + + private static boolean validateExpected(String headerName, TestContext context, + String receivedValueString, Iterator expectedIterator) { + boolean validated = false; + while (expectedIterator.hasNext()) { + String expectedValue = expectedIterator.next(); + + if (ValidationMatcherUtils.isValidationMatcherExpression(expectedValue)) { + try { + ValidationMatcherUtils.resolveValidationMatcher(headerName, receivedValueString, expectedValue, + context); + validated = true; + expectedIterator.remove(); // Remove matched value + break; + } catch (ValidationException e) { + // Ignore this exception and try other expected values + } + } else { + if (receivedValueString.equals(expectedValue)) { + validated = true; + expectedIterator.remove(); // Remove matched value + break; + } + } + } + + return validated; + } + + private static List toList(Object value) { + List receivedValuesList; + if (value == null) { + receivedValuesList = Collections.emptyList(); + } else if (!(value instanceof List)) { + receivedValuesList = new ArrayList<>(); + receivedValuesList.add(value.toString()); + } else { + //noinspection unchecked + receivedValuesList = (List) value; + } + + return receivedValuesList; } @Override @@ -87,11 +180,7 @@ public boolean supports(String headerName, Class type) { * Combines header validators from multiple sources. Includes validators coming from reference resolver * and resource path lookup are added. * - * Then pick validator that explicitly supports the given header name or control value and return as optional. - * @param headerName - * @param controlValue - * @param context - * @return + *

      Then pick validator that explicitly supports the given header name or control value and return as optional. */ private static Optional getHeaderValidator(String headerName, Object controlValue, TestContext context) { // add validators from resource path lookup diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultMessageHeaderValidator.java b/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultMessageHeaderValidator.java index f6bcc30068..fed3d9171b 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultMessageHeaderValidator.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/DefaultMessageHeaderValidator.java @@ -16,15 +16,6 @@ package org.citrusframework.validation; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - import org.citrusframework.context.TestContext; import org.citrusframework.endpoint.resolver.EndpointUriResolver; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -36,6 +27,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + /** * Basic header message validator provides message header validation. Subclasses only have to add * specific logic for message payload validation. This validator is based on a control message. @@ -112,7 +112,7 @@ public void validateMessage(Message receivedMessage, Message controlMessage, Tes /** * Combines header validators from multiple sources. First the manual added validators in this class are added. Then * validators coming from reference resolver and resource path lookup are added. - * + *

      * At the end a distinct combination of all validators is returned. * @param context * @return diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/DelegatingPayloadVariableExtractor.java b/core/citrus-base/src/main/java/org/citrusframework/validation/DelegatingPayloadVariableExtractor.java index a7d59ab176..d86d7d3b61 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/DelegatingPayloadVariableExtractor.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/DelegatingPayloadVariableExtractor.java @@ -60,9 +60,7 @@ public void extractVariables(Message message, TestContext context) { return; } - if (logger.isDebugEnabled()) { - logger.debug("Reading path elements."); - } + logger.debug("Reading path elements."); Map jsonPathExpressions = new LinkedHashMap<>(); Map xpathExpressions = new LinkedHashMap<>(); diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/ValidationUtils.java b/core/citrus-base/src/main/java/org/citrusframework/validation/ValidationUtils.java index d18083c144..600f8a29cd 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/ValidationUtils.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/ValidationUtils.java @@ -16,18 +16,18 @@ package org.citrusframework.validation; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - import org.apache.commons.codec.binary.Base64; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.ValidationException; import org.citrusframework.validation.matcher.ValidationMatcherUtils; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + /** * Utility class provides helper methods for validation work in Citrus. * @@ -155,7 +155,7 @@ public static void validateValues(Object actualValue, Object expectedValue, Stri /** * Combines value matchers from multiple sources. Includes validators coming from reference resolver * and resource path lookup are added. - * + *

      * Then pick matcher that explicitly supports the given expected value type. * @param expectedValue * @param context diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/json/JsonMessageValidationContext.java b/core/citrus-base/src/main/java/org/citrusframework/validation/json/JsonMessageValidationContext.java index 3c4faaaec0..39d1e09768 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/json/JsonMessageValidationContext.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/json/JsonMessageValidationContext.java @@ -16,14 +16,14 @@ package org.citrusframework.validation.json; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - import org.citrusframework.validation.context.DefaultValidationContext; import org.citrusframework.validation.context.SchemaValidationContext; import org.citrusframework.validation.context.ValidationContext; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + /** * Validation context holding JSON specific validation information. * @since 2.3 @@ -35,7 +35,7 @@ public class JsonMessageValidationContext extends DefaultValidationContext imple /** * Should message be validated with its schema definition - * + *

      * This is currently disabled by default, because old json tests would fail with a validation exception * as soon as a json schema repository is specified and the schema validation is activated. */ diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/json/JsonPathMessageValidationContext.java b/core/citrus-base/src/main/java/org/citrusframework/validation/json/JsonPathMessageValidationContext.java index dbd7d8e39d..b7a90eb184 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/json/JsonPathMessageValidationContext.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/json/JsonPathMessageValidationContext.java @@ -54,6 +54,11 @@ public JsonPathMessageValidationContext() { this(new Builder()); } + @Override + public boolean requiresValidator() { + return true; + } + /** * Fluent builder. */ @@ -119,4 +124,6 @@ public Map getJsonPathExpressions() { public static boolean isJsonPathExpression(String pathExpression) { return StringUtils.hasText(pathExpression) && (pathExpression.startsWith("$")); } + + } diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/DefaultValidationMatcherLibrary.java b/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/DefaultValidationMatcherLibrary.java index 875a6f4733..124e30da38 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/DefaultValidationMatcherLibrary.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/DefaultValidationMatcherLibrary.java @@ -88,9 +88,7 @@ public DefaultValidationMatcherLibrary() { private void lookupValidationMatchers() { ValidationMatcher.lookup().forEach((k, m) -> { getMembers().put(k, m); - if (logger.isDebugEnabled()) { - logger.debug(String.format("Register message matcher '%s' as %s", k, m.getClass())); - } + logger.debug("Register message matcher '{}' as {}", k, m.getClass()); }); } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/CreateVariableValidationMatcher.java b/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/CreateVariableValidationMatcher.java index a55813f0b1..fe51f77e92 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/CreateVariableValidationMatcher.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/CreateVariableValidationMatcher.java @@ -16,14 +16,14 @@ package org.citrusframework.validation.matcher.core; -import java.util.List; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.ValidationException; import org.citrusframework.validation.matcher.ValidationMatcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; + /** * Creates new variables from given field. Either uses field name or control value as variable name. * @@ -42,7 +42,7 @@ public void validate(String fieldName, String value, List controlParamet name = controlParameters.get(0); } - logger.debug("Setting variable: " + name + " to value: " + value); + logger.debug("Setting variable: {} to value: {}", name, value); context.setVariable(name, value); } diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/DateRangeValidationMatcher.java b/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/DateRangeValidationMatcher.java index 658fc84cfd..94e790ce12 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/DateRangeValidationMatcher.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/DateRangeValidationMatcher.java @@ -47,10 +47,7 @@ public class DateRangeValidationMatcher implements ValidationMatcher { @Override public void validate(String fieldName, String value, List params, TestContext context) throws ValidationException { - logger.debug(String.format( - "Validating date range for date '%s' using control data: %s", - value, - ValidationMatcherUtils.getParameterListAsString(params))); + logger.debug("Validating date range for date '{}' using control data: {}", value, ValidationMatcherUtils.getParameterListAsString(params)); try { String dateFromParam = params.get(0); diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/IgnoreValidationMatcher.java b/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/IgnoreValidationMatcher.java index b5deaa66af..568892eca2 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/IgnoreValidationMatcher.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/IgnoreValidationMatcher.java @@ -34,8 +34,6 @@ public class IgnoreValidationMatcher implements ValidationMatcher { @Override public void validate(String fieldName, String value, List controlParameters, TestContext context) throws ValidationException { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Ignoring value for field '%s'", fieldName)); - } + logger.debug("Ignoring value for field '{}'", fieldName); } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/WeekdayValidationMatcher.java b/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/WeekdayValidationMatcher.java index e896d42255..6f181c537c 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/WeekdayValidationMatcher.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/matcher/core/WeekdayValidationMatcher.java @@ -107,7 +107,7 @@ private enum Weekday { SATURDAY(Calendar.SATURDAY), SUNDAY(Calendar.SUNDAY); - private int constantValue; + private final int constantValue; Weekday(int constant) { this.constantValue = constant; diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/script/ScriptValidationContext.java b/core/citrus-base/src/main/java/org/citrusframework/validation/script/ScriptValidationContext.java index 5e4ebe3448..4ab437bb57 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/script/ScriptValidationContext.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/script/ScriptValidationContext.java @@ -70,6 +70,11 @@ public ScriptValidationContext(Builder builder) { this.scriptType = builder.scriptType; } + @Override + public boolean requiresValidator() { + return true; + } + /** * Constructs the actual validation script either from data or external resource. * @param context the current TestContext. diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/script/TemplateBasedScriptBuilder.java b/core/citrus-base/src/main/java/org/citrusframework/validation/script/TemplateBasedScriptBuilder.java index b9c7c1ba66..38fefeca30 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/script/TemplateBasedScriptBuilder.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/script/TemplateBasedScriptBuilder.java @@ -16,14 +16,14 @@ package org.citrusframework.validation.script; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.Resource; import org.citrusframework.util.FileUtils; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; + /** * Script builder builds a script with custom code body. Script header and tail come from static * script template. @@ -71,7 +71,7 @@ public String build() { scriptBuilder.append(line); scriptBuilder.append("\n"); } else { - scriptBody.append((scriptBody.length() == 0 ? "" : "\n")); + scriptBody.append((scriptBody.isEmpty() ? "" : "\n")); scriptBody.append(line); } } @@ -83,7 +83,7 @@ public String build() { } scriptBuilder.append(scriptHead); - scriptBuilder.append(scriptBody.toString()); + scriptBuilder.append(scriptBody); scriptBuilder.append(scriptTail); return scriptBuilder.toString(); diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/script/sql/SqlResultSetScriptValidator.java b/core/citrus-base/src/main/java/org/citrusframework/validation/script/sql/SqlResultSetScriptValidator.java index 65ac950ec6..da62d2c682 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/script/sql/SqlResultSetScriptValidator.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/script/sql/SqlResultSetScriptValidator.java @@ -16,9 +16,6 @@ package org.citrusframework.validation.script.sql; -import java.util.List; -import java.util.Map; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.ValidationException; import org.citrusframework.spi.ResourcePathTypeResolver; @@ -27,6 +24,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.Map; + /** * Validator working on SQL result sets. Scripts get the actual test context * and a SQL result set representation for validation. @@ -52,7 +52,7 @@ static Map lookup() { Map validators = TYPE_RESOLVER.resolveAll("", TypeResolver.DEFAULT_TYPE_PROPERTY, "name"); if (logger.isDebugEnabled()) { - validators.forEach((k, v) -> logger.debug(String.format("Found SQL result set validator '%s' as %s", k, v.getClass()))); + validators.forEach((k, v) -> logger.debug("Found SQL result set validator '{}' as {}", k, v.getClass())); } return validators; } diff --git a/core/citrus-base/src/main/java/org/citrusframework/validation/xml/XpathMessageValidationContext.java b/core/citrus-base/src/main/java/org/citrusframework/validation/xml/XpathMessageValidationContext.java index 5ffddac1ef..3402757f7b 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/validation/xml/XpathMessageValidationContext.java +++ b/core/citrus-base/src/main/java/org/citrusframework/validation/xml/XpathMessageValidationContext.java @@ -53,6 +53,11 @@ public XpathMessageValidationContext(Builder builder) { this.xPathExpressions = builder.expressions; } + @Override + public boolean requiresValidator() { + return true; + } + /** * Fluent builder. */ diff --git a/core/citrus-base/src/main/java/org/citrusframework/xml/Jaxb2Marshaller.java b/core/citrus-base/src/main/java/org/citrusframework/xml/Jaxb2Marshaller.java index 9baf24fa25..bbc8e9f76a 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/xml/Jaxb2Marshaller.java +++ b/core/citrus-base/src/main/java/org/citrusframework/xml/Jaxb2Marshaller.java @@ -16,21 +16,6 @@ package org.citrusframework.xml; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.xml.XMLConstants; -import javax.xml.transform.Result; -import javax.xml.transform.Source; -import javax.xml.transform.sax.SAXSource; -import javax.xml.validation.Schema; -import javax.xml.validation.SchemaFactory; - import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.PropertyException; @@ -43,6 +28,21 @@ import org.xml.sax.SAXException; import org.xml.sax.XMLReader; +import javax.xml.XMLConstants; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.sax.SAXSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + /** * Marshaller uses Jaxb to marshal/unmarshal data. */ @@ -119,7 +119,7 @@ private jakarta.xml.bind.Marshaller createMarshaller() throws JAXBException { try { marshaller.setProperty(k, v); } catch (PropertyException e) { - logger.warn(String.format("Unable to set marshaller property %s=%s", k, v)); + logger.warn("Unable to set marshaller property {}={}", k, v); } }); @@ -139,9 +139,7 @@ private jakarta.xml.bind.Unmarshaller createUnmarshaller() throws JAXBException private JAXBContext getOrCreateContext() throws JAXBException { if (jaxbContext == null) { synchronized (this) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Creating JAXBContext with bound classes %s", Arrays.toString(classesToBeBound))); - } + logger.debug("Creating JAXBContext with bound classes {}", Arrays.toString(classesToBeBound)); if (classesToBeBound != null) { jaxbContext = JAXBContext.newInstance(classesToBeBound); @@ -161,10 +159,7 @@ public void setProperty(String key, Object value) { } private Schema loadSchema(Resource... schemas) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Using marshaller validation schemas '%s'", - Stream.of(schemas).map(Object::toString).collect(Collectors.joining(",")))); - } + logger.debug("Using marshaller validation schemas '{}'", Stream.of(schemas).map(Object::toString).collect(Collectors.joining(","))); try { List schemaSources = new ArrayList<>(); diff --git a/core/citrus-base/src/test/java/org/citrusframework/TestCaseTest.java b/core/citrus-base/src/test/java/org/citrusframework/TestCaseTest.java index 638474f31c..fcfec80b37 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/TestCaseTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/TestCaseTest.java @@ -16,13 +16,6 @@ package org.citrusframework; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.actions.AbstractAsyncTestAction; import org.citrusframework.actions.EchoAction; import org.citrusframework.container.Async; @@ -35,6 +28,13 @@ import org.testng.Assert; import org.testng.annotations.Test; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + import static org.citrusframework.DefaultTestActionBuilder.action; public class TestCaseTest extends UnitTestSupport { @@ -121,7 +121,7 @@ public void doExecuteAsync(final TestContext context) { .findAny(); waitingThread.ifPresent(thread -> LoggerFactory.getLogger("TestWaitForFinishAsync").warn(Arrays.toString(threads.get(thread)))); - waitingThread.ifPresent(thread -> Assert.fail(String.format("Waiting thread still alive: %s", thread.toString()))); + waitingThread.ifPresent(thread -> Assert.fail(String.format("Waiting thread still alive: %s", thread))); } @Test @@ -220,6 +220,6 @@ public void testThreadLeak() { .filter(Thread::isAlive) .findAny(); - waitingThread.ifPresent(thread -> Assert.fail(String.format("Waiting thread still alive: %s", thread.toString()))); + waitingThread.ifPresent(thread -> Assert.fail(String.format("Waiting thread still alive: %s", thread))); } } diff --git a/core/citrus-base/src/test/java/org/citrusframework/actions/PurgeEndpointActionTest.java b/core/citrus-base/src/test/java/org/citrusframework/actions/PurgeEndpointActionTest.java index b77472e1e5..24c7981518 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/actions/PurgeEndpointActionTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/actions/PurgeEndpointActionTest.java @@ -16,8 +16,6 @@ package org.citrusframework.actions; -import java.util.Collections; - import org.citrusframework.UnitTestSupport; import org.citrusframework.context.TestContextFactory; import org.citrusframework.endpoint.Endpoint; @@ -28,6 +26,8 @@ import org.mockito.Mockito; import org.testng.annotations.Test; +import java.util.Collections; + import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; @@ -85,7 +85,7 @@ public void testPurgeWithEndpointObjects() { } @Test - public void testPurgeWithMessageSelector() throws Exception { + public void testPurgeWithMessageSelector() { reset(mockEndpoint, consumer, selectiveConsumer); when(mockEndpoint.getName()).thenReturn("mockEndpoint"); @@ -102,7 +102,7 @@ public void testPurgeWithMessageSelector() throws Exception { } @Test - public void testPurgeWithMessageSelectorMap() throws Exception { + public void testPurgeWithMessageSelectorMap() { reset(mockEndpoint, consumer, selectiveConsumer); when(mockEndpoint.getName()).thenReturn("mockEndpoint"); diff --git a/core/citrus-base/src/test/java/org/citrusframework/actions/ReceiveMessageBuilderTest.java b/core/citrus-base/src/test/java/org/citrusframework/actions/ReceiveMessageBuilderTest.java index 9b19eeba19..60cc62070c 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/actions/ReceiveMessageBuilderTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/actions/ReceiveMessageBuilderTest.java @@ -16,14 +16,6 @@ package org.citrusframework.actions; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; import org.citrusframework.endpoint.Endpoint; @@ -52,11 +44,24 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import static org.citrusframework.validation.json.JsonMessageValidationContext.Builder.json; import static org.citrusframework.validation.json.JsonPathMessageValidationContext.Builder.jsonPath; import static org.citrusframework.validation.xml.XmlMessageValidationContext.Builder.xml; import static org.citrusframework.validation.xml.XpathMessageValidationContext.Builder.xpath; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -206,7 +211,7 @@ void payload_asResourceWithCharset() { } @Test - void testSetPayloadWithResourceIoExceptionsIsWrapped() throws IOException { + void testSetPayloadWithResourceIoExceptionsIsWrapped() { //GIVEN final ReceiveMessageAction.Builder builder = new ReceiveMessageAction.Builder(); diff --git a/core/citrus-base/src/test/java/org/citrusframework/actions/dsl/ReceiveMessageActionBuilderTest.java b/core/citrus-base/src/test/java/org/citrusframework/actions/dsl/ReceiveMessageActionBuilderTest.java index cdba297247..1888eff21f 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/actions/dsl/ReceiveMessageActionBuilderTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/actions/dsl/ReceiveMessageActionBuilderTest.java @@ -16,12 +16,6 @@ package org.citrusframework.actions.dsl; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - import org.citrusframework.DefaultTestCaseRunner; import org.citrusframework.TestCase; import org.citrusframework.UnitTestSupport; @@ -59,11 +53,22 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import java.io.ByteArrayInputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + import static org.citrusframework.actions.ReceiveMessageAction.Builder.receive; import static org.citrusframework.dsl.MessageSupport.MessageHeaderSupport.fromHeaders; import static org.citrusframework.dsl.MessageSupport.message; import static org.citrusframework.validation.xml.XmlMessageValidationContext.Builder.xml; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class ReceiveMessageActionBuilderTest extends UnitTestSupport { @@ -220,7 +225,7 @@ public void testReceiveBuilderWithPayloadString() { } @Test - public void testReceiveBuilderWithPayloadResource() throws IOException { + public void testReceiveBuilderWithPayloadResource() { reset(resource, messageEndpoint, messageConsumer, configuration); when(messageEndpoint.createConsumer()).thenReturn(messageConsumer); when(messageEndpoint.getEndpointConfiguration()).thenReturn(configuration); @@ -523,7 +528,7 @@ public void testReceiveBuilderWithHeaderDataBuilder() { } @Test - public void testReceiveBuilderWithHeaderResource() throws IOException { + public void testReceiveBuilderWithHeaderResource() { reset(resource, messageEndpoint, messageConsumer, configuration); when(messageEndpoint.createConsumer()).thenReturn(messageConsumer); when(messageEndpoint.getEndpointConfiguration()).thenReturn(configuration); @@ -581,7 +586,7 @@ public void testReceiveBuilderWithHeaderResource() throws IOException { } @Test - public void testReceiveBuilderWithMultipleHeaderResource() throws IOException { + public void testReceiveBuilderWithMultipleHeaderResource() { reset(resource, messageEndpoint, messageConsumer, configuration); when(messageEndpoint.createConsumer()).thenReturn(messageConsumer); when(messageEndpoint.getEndpointConfiguration()).thenReturn(configuration); @@ -1051,7 +1056,7 @@ public void testReceiveBuilderWithValidationProcessor() { } @Test - public void testDeactivateSchemaValidation() throws IOException { + public void testDeactivateSchemaValidation() { reset(messageEndpoint, messageConsumer, configuration); when(messageEndpoint.createConsumer()).thenReturn(messageConsumer); diff --git a/core/citrus-base/src/test/java/org/citrusframework/actions/dsl/SendMessageActionBuilderTest.java b/core/citrus-base/src/test/java/org/citrusframework/actions/dsl/SendMessageActionBuilderTest.java index 7bfe51471f..d31b38669c 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/actions/dsl/SendMessageActionBuilderTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/actions/dsl/SendMessageActionBuilderTest.java @@ -16,11 +16,6 @@ package org.citrusframework.actions.dsl; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; - import org.citrusframework.DefaultTestCaseRunner; import org.citrusframework.TestCase; import org.citrusframework.UnitTestSupport; @@ -52,6 +47,10 @@ import org.testng.Assert; import org.testng.annotations.Test; +import java.io.ByteArrayInputStream; +import java.util.Collections; +import java.util.HashMap; + import static org.citrusframework.actions.SendMessageAction.Builder.send; import static org.citrusframework.dsl.MessageSupport.message; import static org.mockito.Mockito.any; @@ -273,7 +272,7 @@ public void testSendBuilderWithPayloadData() { } @Test - public void testSendBuilderWithPayloadResource() throws IOException { + public void testSendBuilderWithPayloadResource() { reset(resource, messageEndpoint, messageProducer); when(messageEndpoint.createProducer()).thenReturn(messageProducer); when(messageEndpoint.getActor()).thenReturn(null); @@ -497,7 +496,7 @@ public void testSendBuilderWithMultipleHeaderData() { } @Test - public void testSendBuilderWithHeaderDataResource() throws IOException { + public void testSendBuilderWithHeaderDataResource() { reset(resource, messageEndpoint, messageProducer); when(messageEndpoint.createProducer()).thenReturn(messageProducer); when(messageEndpoint.getActor()).thenReturn(null); diff --git a/core/citrus-base/src/test/java/org/citrusframework/endpoint/AbstractEndpointBuilderTest.java b/core/citrus-base/src/test/java/org/citrusframework/endpoint/AbstractEndpointBuilderTest.java index 0296245da3..195083fe57 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/endpoint/AbstractEndpointBuilderTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/endpoint/AbstractEndpointBuilderTest.java @@ -22,18 +22,19 @@ import org.citrusframework.annotations.CitrusEndpointProperty; import org.citrusframework.context.TestContextFactory; import org.mockito.Mockito; -import org.testng.Assert; import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + public class AbstractEndpointBuilderTest extends UnitTestSupport { @CitrusEndpoint( - name = "fooEndpoint", - properties = { - @CitrusEndpointProperty(name = "message", value = "Hello from Citrus!"), - @CitrusEndpointProperty(name = "number", value = "1", type = int.class), - @CitrusEndpointProperty(name = "person", value = "testPerson", type = TestEndpointBuilder.Person.class) - } + name = "fooEndpoint", + properties = { + @CitrusEndpointProperty(name = "message", value = "Hello from Citrus!"), + @CitrusEndpointProperty(name = "number", value = "1", type = int.class), + @CitrusEndpointProperty(name = "person", value = "testPerson", type = TestEndpointBuilder.Person.class) + } ) private Endpoint injected; @@ -52,10 +53,10 @@ protected TestContextFactory createTestContextFactory() { public void buildFromEndpointProperties() { CitrusEndpointAnnotations.injectEndpoints(this, context); - Assert.assertEquals(injected, endpointBuilder.mockEndpoint); - Assert.assertEquals(endpointBuilder.message, "Hello from Citrus!"); - Assert.assertEquals(endpointBuilder.number, 1); - Assert.assertEquals(endpointBuilder.person, person); + assertEquals(injected, endpointBuilder.mockEndpoint); + assertEquals(endpointBuilder.message, "Hello from Citrus!"); + assertEquals(endpointBuilder.number, 1); + assertEquals(endpointBuilder.person, person); } public static final class TestEndpointBuilder extends AbstractEndpointBuilder { diff --git a/core/citrus-base/src/test/java/org/citrusframework/endpoint/adapter/mapping/HeaderMappingKeyExtractorTest.java b/core/citrus-base/src/test/java/org/citrusframework/endpoint/adapter/mapping/HeaderMappingKeyExtractorTest.java index cf93cc2e75..806dda55fa 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/endpoint/adapter/mapping/HeaderMappingKeyExtractorTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/endpoint/adapter/mapping/HeaderMappingKeyExtractorTest.java @@ -24,7 +24,7 @@ public class HeaderMappingKeyExtractorTest { @Test - public void testExtractMappingKey() throws Exception { + public void testExtractMappingKey() { HeaderMappingKeyExtractor extractor = new HeaderMappingKeyExtractor(); extractor.setHeaderName("Foo"); @@ -34,7 +34,7 @@ public void testExtractMappingKey() throws Exception { } @Test - public void testExtractMappingKeyWithoutHeaderNameSet() throws Exception { + public void testExtractMappingKeyWithoutHeaderNameSet() { HeaderMappingKeyExtractor extractor = new HeaderMappingKeyExtractor(); try { @@ -48,7 +48,7 @@ public void testExtractMappingKeyWithoutHeaderNameSet() throws Exception { } @Test - public void testExtractMappingKeyWithUnknownHeaderName() throws Exception { + public void testExtractMappingKeyWithUnknownHeaderName() { HeaderMappingKeyExtractor extractor = new HeaderMappingKeyExtractor("UNKNOWN"); try { diff --git a/core/citrus-base/src/test/java/org/citrusframework/endpoint/adapter/mapping/SoapActionMappingKeyExtractorTest.java b/core/citrus-base/src/test/java/org/citrusframework/endpoint/adapter/mapping/SoapActionMappingKeyExtractorTest.java index 2285c3c752..f7ef07644d 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/endpoint/adapter/mapping/SoapActionMappingKeyExtractorTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/endpoint/adapter/mapping/SoapActionMappingKeyExtractorTest.java @@ -24,7 +24,7 @@ public class SoapActionMappingKeyExtractorTest { @Test - public void testExtractMappingKey() throws Exception { + public void testExtractMappingKey() { SoapActionMappingKeyExtractor extractor = new SoapActionMappingKeyExtractor(); Assert.assertEquals(extractor.extractMappingKey(new DefaultMessage("Foo") @@ -33,7 +33,7 @@ public void testExtractMappingKey() throws Exception { } @Test - public void testExtractNoMappingFound() throws Exception { + public void testExtractNoMappingFound() { SoapActionMappingKeyExtractor extractor = new SoapActionMappingKeyExtractor(); try { diff --git a/core/citrus-base/src/test/java/org/citrusframework/endpoint/direct/DirectEndpointComponentTest.java b/core/citrus-base/src/test/java/org/citrusframework/endpoint/direct/DirectEndpointComponentTest.java index 04a94c6091..d28152e42b 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/endpoint/direct/DirectEndpointComponentTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/endpoint/direct/DirectEndpointComponentTest.java @@ -16,8 +16,6 @@ package org.citrusframework.endpoint.direct; -import java.util.Map; - import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; import org.citrusframework.endpoint.Endpoint; @@ -26,6 +24,8 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import java.util.Map; + public class DirectEndpointComponentTest { private TestContext context; @@ -36,7 +36,7 @@ public void setupMocks() { } @Test - public void testCreateDirectEndpoint() throws Exception { + public void testCreateDirectEndpoint() { DirectEndpointComponent component = new DirectEndpointComponent(); Assert.assertFalse(context.getReferenceResolver().isResolvable("queueName")); @@ -50,7 +50,7 @@ public void testCreateDirectEndpoint() throws Exception { } @Test - public void testCreateSyncDirectEndpoint() throws Exception { + public void testCreateSyncDirectEndpoint() { DirectEndpointComponent component = new DirectEndpointComponent(); Assert.assertFalse(context.getReferenceResolver().isResolvable("queueName")); @@ -63,7 +63,7 @@ public void testCreateSyncDirectEndpoint() throws Exception { } @Test - public void testCreateDirectEndpointWithParameters() throws Exception { + public void testCreateDirectEndpointWithParameters() { DirectEndpointComponent component = new DirectEndpointComponent(); Endpoint endpoint = component.createEndpoint("direct:queueName?timeout=10000", context); diff --git a/core/citrus-base/src/test/java/org/citrusframework/endpoint/direct/annotation/DirectEndpointConfigParserTest.java b/core/citrus-base/src/test/java/org/citrusframework/endpoint/direct/annotation/DirectEndpointConfigParserTest.java index eefc5fbb3b..dc309940b9 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/endpoint/direct/annotation/DirectEndpointConfigParserTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/endpoint/direct/annotation/DirectEndpointConfigParserTest.java @@ -16,8 +16,6 @@ package org.citrusframework.endpoint.direct.annotation; -import java.util.Map; - import org.citrusframework.TestActor; import org.citrusframework.annotations.CitrusEndpoint; import org.citrusframework.annotations.CitrusEndpointAnnotations; @@ -35,6 +33,8 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import java.util.Map; + import static org.mockito.Mockito.when; public class DirectEndpointConfigParserTest { diff --git a/core/citrus-base/src/test/java/org/citrusframework/functions/FunctionsTest.java b/core/citrus-base/src/test/java/org/citrusframework/functions/FunctionsTest.java index 975946ca91..c4433e3ace 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/functions/FunctionsTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/functions/FunctionsTest.java @@ -16,15 +16,23 @@ package org.citrusframework.functions; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; - import org.citrusframework.UnitTestSupport; import org.citrusframework.functions.core.RandomStringFunction; import org.testng.Assert; import org.testng.annotations.Test; -import static org.citrusframework.functions.Functions.*; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; + +import static org.citrusframework.functions.Functions.changeDate; +import static org.citrusframework.functions.Functions.currentDate; +import static org.citrusframework.functions.Functions.decodeBase64; +import static org.citrusframework.functions.Functions.digestAuthHeader; +import static org.citrusframework.functions.Functions.encodeBase64; +import static org.citrusframework.functions.Functions.randomNumber; +import static org.citrusframework.functions.Functions.randomString; +import static org.citrusframework.functions.Functions.randomUUID; +import static org.citrusframework.functions.Functions.unixTimestamp; public class FunctionsTest extends UnitTestSupport { @@ -39,65 +47,65 @@ public void testCurrentDateFormat() throws Exception { } @Test - public void testChangeDate() throws Exception { + public void testChangeDate() { Assert.assertEquals(changeDate("01.01.2014", "+1y", context), "01.01.2015"); Assert.assertEquals(changeDate("2014-01-01T12:00:00", "+1y", "yyyy-MM-dd'T'HH:mm:ss", context), "2015-01-01T12:00:00"); } @Test - public void testEncodeBase64() throws Exception { + public void testEncodeBase64() { Assert.assertEquals(encodeBase64("Foo", context), "Rm9v"); } @Test - public void testEncodeBase64WithCharset() throws Exception { + public void testEncodeBase64WithCharset() { Assert.assertEquals(encodeBase64("Foo", StandardCharsets.UTF_8, context), "Rm9v"); } @Test - public void testDecodeBase64() throws Exception { + public void testDecodeBase64() { Assert.assertEquals(decodeBase64("Rm9v", context), "Foo"); } @Test - public void testDecodeBase64WithCharset() throws Exception { + public void testDecodeBase64WithCharset() { Assert.assertEquals(decodeBase64("Rm9v", StandardCharsets.UTF_8, context), "Foo"); } @Test - public void testDigestAuthHeader() throws Exception { + public void testDigestAuthHeader() { digestAuthHeader("username", "password", "authRealm", "acegi", "POST", "http://localhost:8080", "citrus", "md5", context); } @Test - public void testRandomUUID() throws Exception { + public void testRandomUUID() { Assert.assertNotNull(randomUUID(context)); } @Test - public void testRandomNumber() throws Exception { + public void testRandomNumber() { Assert.assertTrue(randomNumber(10L, context).length() > 9); } @Test - public void testRandomNumberWithParams() throws Exception { + public void testRandomNumberWithParams() { Assert.assertTrue(randomNumber(10L, true, context).length() > 9); } @Test - public void testRandomString() throws Exception { + public void testRandomString() { Assert.assertEquals(randomString(10L, context).length(), 10); } @Test - public void testRandomStringWithParams() throws Exception { + public void testRandomStringWithParams() { Assert.assertEquals(randomString(10L, false, context).length(), 10); Assert.assertEquals(randomString(10L, RandomStringFunction.LOWERCASE, context).length(), 10); Assert.assertEquals(randomString(10L, RandomStringFunction.UPPERCASE, false, context).length(), 10); } @Test - public void testUnixTimestamp() throws Exception { + public void testUnixTimestamp() { Assert.assertEquals(String.valueOf(System.currentTimeMillis() / 1000L), unixTimestamp(context)); } } diff --git a/core/citrus-base/src/test/java/org/citrusframework/functions/core/AdvancedRandomNumberFunctionTest.java b/core/citrus-base/src/test/java/org/citrusframework/functions/core/AdvancedRandomNumberFunctionTest.java new file mode 100644 index 0000000000..5e30fdcbca --- /dev/null +++ b/core/citrus-base/src/test/java/org/citrusframework/functions/core/AdvancedRandomNumberFunctionTest.java @@ -0,0 +1,410 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.functions.core; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.math.BigDecimal; +import java.util.List; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.InvalidFunctionUsageException; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class AdvancedRandomNumberFunctionTest { + + private AdvancedRandomNumberFunction function; + private TestContext context; + + @BeforeMethod + public void setUp() { + function = new AdvancedRandomNumberFunction(); + context = new TestContext(); + } + + @Test + public void testRandomNumberWithNullParameter() { + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(null, context)); + assertEquals(exception.getMessage(), + "Function parameters must not be null."); + } + + @Test + public void testRandomNumberWithDefaultValues() { + List params = List.of(); + String result = function.execute(params, context); + assertNotNull(result); + assertTrue(result.matches("-?\\d*\\.\\d{2}")); + } + + @Test + public void testRandomNumberWithDecimalPlaces() { + List params = List.of("2"); + String result = function.execute(params, context); + assertNotNull(result); + assertTrue(result.matches("-?\\d*\\.\\d{2}"), "result does not match pattern: " + result); + } + + @Test + public void testRandomNumberWithinRange() { + List params = List.of("2", "10.5", "20.5"); + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= 10.5 && randomValue <= 20.5); + } + + @Test + public void testRandomNumberIncludesMin() { + List params = List.of("1", "10.5", "20.5"); + function = new AdvancedRandomNumberFunction() { + @Override + BigDecimal createRandomValue(BigDecimal minValue, BigDecimal range, double random) { + random = 0.0; + return super.createRandomValue(minValue, range, random); + } + }; + String result = function.execute(params, context); + assertEquals(result, "10.5"); + } + + @Test + public void testRandomNumberIncludesMax() { + List params = List.of("1", "10.5", "20.5"); + function = new AdvancedRandomNumberFunction() { + @Override + BigDecimal createRandomValue(BigDecimal minValue, BigDecimal range, double random) { + random = 1.0; + return super.createRandomValue(minValue, range, random); + } + }; + String result = function.execute(params, context); + assertEquals(result, "20.5"); + } + + @Test + public void testRandomNumberExcludeMin() { + List params = List.of("1", "10.5", "20.5", "true", "false"); + function = new AdvancedRandomNumberFunction() { + @Override + BigDecimal createRandomValue(BigDecimal minValue, BigDecimal range, double random) { + random = 0.0; + return super.createRandomValue(minValue, range, random); + } + }; + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue > 10.5 && randomValue <= 20.5); + } + + @Test + public void testRandomNumberExcludeMax() { + List params = List.of("2", "10.5", "20.5", "false", "true"); + function = new AdvancedRandomNumberFunction() { + @Override + BigDecimal createRandomValue(BigDecimal minValue, BigDecimal range, double random) { + random = 1.0; + return super.createRandomValue(minValue, range, random); + } + }; + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= 10.5 && randomValue < 20.5); + } + + @Test + public void testRandomInteger32EdgeCase() { + List params = List.of("0", "-2147483648", "2147483647", "false", "false"); + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= -Integer.MAX_VALUE && randomValue < Integer.MAX_VALUE); + } + + @Test + public void testRandomInteger32MinEqualsMaxEdgeCase() { + List params = List.of("0", "3", "3", "false", "false"); + for (int i = 0; i < 100; i++) { + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertEquals(randomValue, 3); + } + } + + @Test + public void testRandomDouble32MinEqualsMaxEdgeCase() { + List params = List.of("2", "3.0", "3.0", "false", "false"); + for (int i = 0; i < 100; i++) { + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertEquals(randomValue, 3); + } + } + + @Test + public void testRandomInteger64EdgeCase() { + List params = List.of("0", "-9223372036854775808", "9223372036854775807", "false", "false"); + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= -Long.MAX_VALUE && randomValue < Long.MAX_VALUE); + } + + @Test + public void testRandomNumberFloatEdgeCase() { + List params = List.of("0", "-3.4028235E38", "3.4028235E38", "false", "false"); + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= -Float.MAX_VALUE && randomValue < Float.MAX_VALUE); + } + + @Test + public void testRandomNumberDoubleEdgeCase() { + List params = List.of("0", "-1.7976931348623157E308", "1.7976931348623157E308", "false", "false"); + String result = function.execute(params, context); + assertNotNull(result); + double randomValue = Double.parseDouble(result); + assertTrue(randomValue >= -Double.MAX_VALUE && randomValue < Double.MAX_VALUE); + } + + @Test + public void testInvalidDecimalPlaces() { + List params = List.of("-1"); // invalid decimalPlaces + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(params, context)); + assertEquals(exception.getMessage(), + "Decimal places must be a non-negative integer value."); + } + + @Test + public void testInvalidRange() { + List params = List.of("2", "20.5", "10.5"); // invalid range + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(params, context)); + assertEquals(exception.getMessage(), + "Min value must be less than max value."); + } + + @Test + public void testInvalidDecimalPlacesFormat() { + List params = List.of("xxx"); // invalid decimalPlaces + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(params, context)); + assertEquals(exception.getMessage(), + "Invalid parameter at index 1. xxx must be parsable to Integer."); + } + + @Test + public void testInvalidMinValueFormat() { + List params = List.of("1", "xxx"); // invalid min value + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(params, context)); + assertEquals(exception.getMessage(), + "Invalid parameter at index 2. xxx must be parsable to BigDecimal."); + } + + @Test + public void testInvalidMaxValueFormat() { + List params = List.of("1", "1.1", "xxx"); // invalid max value + InvalidFunctionUsageException exception = expectThrows(InvalidFunctionUsageException.class, + () -> function.execute(params, context)); + assertEquals(exception.getMessage(), + "Invalid parameter at index 3. xxx must be parsable to BigDecimal."); + } + + @DataProvider(name = "testRandomNumber") + public static Object[][] testRandomNumber() { + return new Object[][]{ + {0, 12, null, null, false, false}, + {0, null, 0, 2, true, true}, + {0, null, null, null, false, false}, + {0, null, 0, 100, false, false}, + {0, null, 0, 2, false, false}, + {0, null, -100, 0, false, false}, + {0, null, -2, 0, false, false}, + {0, null, 0, 100, true, true}, + {0, null, -100, 0, true, true}, + {0, null, -2, 0, true, true}, + {0, null, 0, null, false, false}, + {0, null, 0, 0, false, false}, + {0, 11, 0, 12, true, true}, + + {0, 13, 0, 100, false, false}, + {0, 14, 0, 14, false, false}, + {0, 15, -100, 0, false, false}, + {0, 16, -16, 0, false, false}, + {0, 17, 0, 100, true, true}, + {0, 18, -100, 0, true, true}, + {0, 19, -20, 0, true, true}, + {0, 20, 0, null, false, false}, + {0, 21, 21, 21, false, false}, + + {0, null, 0, 2, true, true}, + {0, null, null, null, false, false}, + {0, null, 0, 100, false, false}, + {0, null, 0, 2, false, false}, + {0, null, -100, 0, false, false}, + {0, null, -2, 0, false, false}, + {0, null, 0, 100, true, true}, + {0, null, -100, 0, true, true}, + {0, null, -2, 0, true, true}, + {0, null, 0, null, false, false}, + {0, null, 0, 0, false, false}, + {0, 11, 0, 12, true, true}, + {0, 12, null, null, false, false}, + {0, 13, 0, 100, false, false}, + {0, 14, 0, 14, false, false}, + {0, 15, -100, 0, false, false}, + {0, 16, -16, 0, false, false}, + {0, 17, 0, 100, true, true}, + {0, 18, -100, 0, true, true}, + {0, 19, -20, 0, true, true}, + {0, 20, 0, null, false, false}, + {0, 21, 21, 21, false, false}, + + {3, null, 0, 2, true, true}, + {3, null, null, null, false, false}, + {3, null, 0, 100, false, false}, + {3, null, 0, 2, false, false}, + {3, null, -100, 0, false, false}, + {3, null, -2, 0, false, false}, + {3, null, 0, 100, true, true}, + {3, null, -100, 0, true, true}, + {3, null, -2, 0, true, true}, + {3, null, 0, null, false, false}, + {3, null, 0, 0, false, false}, + {3, 11.123f, 0, 13, true, true}, + {3, 12.123f, null, null, false, false}, + {3, 13.123f, 0, 100, false, false}, + {3, 14.123f, 0, 14, false, false}, + {3, 15.123f, -100, 0, false, false}, + {3, 16.123f, -16, 0, false, false}, + {3, 17.123f, 0, 100, true, true}, + {3, 18.123f, -100, 0, true, true}, + {3, 19.123f, -21, 0, true, true}, + {3, 20.123f, 0, null, false, false}, + {3, 21.123f, 21.122f, 21.124f, false, false}, + + {5, null, 0, 2, true, true}, + {5, null, null, null, false, false}, + {5, null, 0, 100, false, false}, + {5, null, 0, 2, false, false}, + {5, null, -100, 0, false, false}, + {5, null, -2, 0, false, false}, + {5, null, 0, 100, true, true}, + {5, null, -100, 0, true, true}, + {5, null, -2, 0, true, true}, + {5, null, 0, null, false, false}, + {5, null, 0, 0, false, false}, + {5, 11.123d, 0, 13, true, true}, + {5, 12.123d, null, null, false, false}, + {5, 13.123d, 0, 100, false, false}, + {5, 14.123d, 0, 14, false, false}, + {5, 15.123d, -100, 0, false, false}, + {5, 16.123d, -16, 0, false, false}, + {5, 17.123d, 0, 100, true, true}, + {5, 18.123d, -100, 0, true, true}, + {5, 19.123d, -21, 0, true, true}, + {5, 20.123d, 0, null, false, false}, + {5, 21.123d, 21.122d, 21.124d, false, false}, + }; + } + + @Test(dataProvider = "testRandomNumber") + void testRandomNumber(Number decimalPlaces, Number multipleOf, Number minimum, Number maximum, + boolean exclusiveMinimum, boolean exclusiveMaximum) { + + TestContext testContext = new TestContext(); + AdvancedRandomNumberFunction advancedRandomNumberFunction = new AdvancedRandomNumberFunction(); + try { + for (int i = 0; i < 1000; i++) { + + BigDecimal value = new BigDecimal(advancedRandomNumberFunction.execute( + List.of(toString(decimalPlaces), toString(minimum), toString(maximum), + toString(exclusiveMinimum), toString(exclusiveMaximum), toString(multipleOf)), testContext)); + + if (multipleOf != null) { + BigDecimal remainder = value.remainder(new BigDecimal(multipleOf.toString())); + + assertEquals( + remainder.compareTo(BigDecimal.ZERO), 0, + "Expected %s to be a multiple of %s! Remainder is %s".formatted( + value, multipleOf, + remainder)); + } + + if (maximum != null) { + if (exclusiveMaximum) { + assertTrue(value.doubleValue() < maximum.doubleValue(), + "Expected %s to be lower than %s!".formatted( + value, maximum)); + } else { + assertTrue(value.doubleValue() <= maximum.doubleValue(), + "Expected %s to be lower or equal than %s!".formatted( + value, maximum)); + } + } + + if (minimum != null) { + if (exclusiveMinimum) { + assertTrue(value.doubleValue() > minimum.doubleValue(), + "Expected %s to be larger than %s!".formatted( + value, minimum)); + } else { + assertTrue(value.doubleValue() >= minimum.doubleValue(), + "Expected %s to be larger or equal than %s!".formatted( + value, minimum)); + } + } + } + } catch (Exception e) { + Assert.fail("Creation of multiple float threw an exception: " + e.getMessage(), e); + } + } + + private String toString(Object obj) { + if (obj == null) { + return "null"; + } + + return obj.toString(); + } + + private T expectThrows(Class exceptionClass, Runnable runnable) { + try { + runnable.run(); + } catch (Throwable throwable) { + if (exceptionClass.isInstance(throwable)) { + return exceptionClass.cast(throwable); + } else { + throw new AssertionError("Unexpected exception type", throwable); + } + } + + throw new AssertionError("Expected exception not thrown"); + } +} diff --git a/core/citrus-base/src/test/java/org/citrusframework/functions/core/LoadMessageFunctionTest.java b/core/citrus-base/src/test/java/org/citrusframework/functions/core/LoadMessageFunctionTest.java index 9f1d341c5b..47ad8a9db0 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/functions/core/LoadMessageFunctionTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/functions/core/LoadMessageFunctionTest.java @@ -16,8 +16,6 @@ package org.citrusframework.functions.core; -import java.util.Collections; - import org.citrusframework.UnitTestSupport; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.message.DefaultMessage; @@ -25,6 +23,8 @@ import org.testng.Assert; import org.testng.annotations.Test; +import java.util.Collections; + /** * @since 2.6.2 */ @@ -36,27 +36,27 @@ public class LoadMessageFunctionTest extends UnitTestSupport { .setHeader("operation", "sampleOperation"); @Test - public void testLoadMessagePayload() throws Exception { + public void testLoadMessagePayload() { context.getMessageStore().storeMessage("request", message); Assert.assertEquals(function.execute(Collections.singletonList("request"), context), "This is a sample message"); Assert.assertEquals(function.execute(Collections.singletonList("request.body()"), context), "This is a sample message"); } @Test - public void testLoadMessageHeader() throws Exception { + public void testLoadMessageHeader() { context.getMessageStore().storeMessage("request", message); Assert.assertEquals(function.execute(Collections.singletonList("request.header(operation)"), context), "sampleOperation"); Assert.assertEquals(function.execute(Collections.singletonList("request.header('operation')"), context), "sampleOperation"); } @Test(expectedExceptions = CitrusRuntimeException.class, expectedExceptionsMessageRegExp = "Missing header name.*") - public void testLoadMessageHeaderEmpty() throws Exception { + public void testLoadMessageHeaderEmpty() { context.getMessageStore().storeMessage("request", message); function.execute(Collections.singletonList("request.header()"), context); } @Test(expectedExceptions = CitrusRuntimeException.class, expectedExceptionsMessageRegExp = "Failed to find header 'wrong'.*") - public void testLoadMessageHeaderUnknown() throws Exception { + public void testLoadMessageHeaderUnknown() { context.getMessageStore().storeMessage("request", message); function.execute(Collections.singletonList("request.header('wrong')"), context); } diff --git a/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomPatternFunctionTest.java b/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomPatternFunctionTest.java new file mode 100644 index 0000000000..ce1c67c8ac --- /dev/null +++ b/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomPatternFunctionTest.java @@ -0,0 +1,74 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.functions.core; + +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.InvalidFunctionUsageException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.List; + +import static org.testng.Assert.assertTrue; + +public class RandomPatternFunctionTest { + + private final RandomPatternFunction function = new RandomPatternFunction(); + private final TestContext context = new TestContext(); + + @Test(expectedExceptions = InvalidFunctionUsageException.class) + public void testExecuteWithNullParameterList() { + function.execute(null, context); + } + + @Test(expectedExceptions = InvalidFunctionUsageException.class) + public void testExecuteWithEmptyPattern() { + function.execute(List.of(""), context); + } + + @Test + public void testExecuteWithValidPattern() { + String pattern = "[a-zA-Z0-9]{10}"; + String result = function.execute(List.of(pattern), context); + assertTrue(result.matches(pattern), "Generated string does not match the pattern"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testExecuteWithInvalidPattern() { + String pattern = "[0-3]([a-c]|[e-g]{1"; // Invalid regex pattern with "Character range is out of order" + function.execute(List.of(pattern), context); + } + + @DataProvider(name = "patternProvider") + public Object[][] patternProvider() { + return new Object[][]{ + {"testExecuteWithComplexPattern", "(foo|bar)[0-9]{2,4}"}, + {"testIpv6", "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"}, + {"testIpv4", "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}"}, + {"testEmail", "[a-z]{5,15}\\.?[a-z]{5,15}\\@[a-z]{5,15}\\.[a-z]{2}"}, + {"testUri", "((http|https)://[a-zA-Z0-9-]+(\\.[a-zA-Z]{2,})+(/[a-zA-Z0-9-]+){1,6})|(file:///[a-zA-Z0-9-]+(/[a-zA-Z0-9-]+){1,6})"} + }; + } + + @Test(dataProvider = "patternProvider") + public void testPatterns(String description, String pattern) { + for (int i = 0; i < 100; i++) { + String result = function.execute(List.of(pattern), context); + assertTrue(result.matches(pattern), "Generated string does not match the pattern: " + description); + } + } +} diff --git a/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomStringFunctionTest.java b/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomStringFunctionTest.java index 34b982a486..042b54f42d 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomStringFunctionTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/functions/core/RandomStringFunctionTest.java @@ -16,19 +16,22 @@ package org.citrusframework.functions.core; -import java.util.ArrayList; -import java.util.List; - import org.citrusframework.UnitTestSupport; import org.citrusframework.exceptions.InvalidFunctionUsageException; import org.testng.Assert; import org.testng.annotations.Test; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; public class RandomStringFunctionTest extends UnitTestSupport { - private RandomStringFunction function = new RandomStringFunction(); + + private final RandomStringFunction function = new RandomStringFunction(); @Test public void testFunction() { @@ -110,8 +113,30 @@ public void testTooManyParameters() { params.add("3"); params.add("UPPERCASE"); params.add("true"); - params.add("too much"); + params.add("0"); + params.add("too many"); function.execute(params, context); } + + @Test + public void testRandomSize() { + List params; + params = new ArrayList<>(); + params.add("10"); + params.add("UPPERCASE"); + params.add("true"); + params.add("8"); + + Set sizes = new HashSet<>(); + + for (int i = 0; i < 1000; i++) { + String text = function.execute(params, context); + sizes.add(text.length()); + } + + Assert.assertTrue(sizes.contains(8)); + Assert.assertTrue(sizes.contains(9)); + Assert.assertTrue(sizes.contains(10)); + } } diff --git a/core/citrus-base/src/test/java/org/citrusframework/message/DefaultMessageStoreTest.java b/core/citrus-base/src/test/java/org/citrusframework/message/DefaultMessageStoreTest.java index b0faa45f93..3ffee9bf36 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/message/DefaultMessageStoreTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/message/DefaultMessageStoreTest.java @@ -31,14 +31,14 @@ public class DefaultMessageStoreTest extends UnitTestSupport { private final MessageStore messageStore = new DefaultMessageStore(); @Test - public void testStoreAndGetMessage() throws Exception { + public void testStoreAndGetMessage() { messageStore.storeMessage("request", new DefaultMessage("RequestMessage")); Assert.assertEquals(messageStore.getMessage("request").getPayload(String.class), "RequestMessage"); Assert.assertNull(messageStore.getMessage("unknown")); } @Test - public void testConstructMessageName() throws Exception { + public void testConstructMessageName() { Endpoint endpoint = new DirectEndpoint(); endpoint.setName("testEndpoint"); Assert.assertEquals(messageStore.constructMessageName(new SendMessageAction.Builder().build(), endpoint), "send(testEndpoint)"); diff --git a/core/citrus-base/src/test/java/org/citrusframework/message/correlation/PollingCorrelationManagerTest.java b/core/citrus-base/src/test/java/org/citrusframework/message/correlation/PollingCorrelationManagerTest.java index f816e321e6..d0d10e2e0a 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/message/correlation/PollingCorrelationManagerTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/message/correlation/PollingCorrelationManagerTest.java @@ -29,7 +29,7 @@ public class PollingCorrelationManagerTest { private ObjectStore objectStore = Mockito.mock(ObjectStore.class); @Test - public void testFind() throws Exception { + public void testFind() { DirectSyncEndpointConfiguration pollableEndpointConfiguration = new DirectSyncEndpointConfiguration(); pollableEndpointConfiguration.setPollingInterval(100L); pollableEndpointConfiguration.setTimeout(500L); diff --git a/core/citrus-base/src/test/java/org/citrusframework/report/LoggingReporterTest.java b/core/citrus-base/src/test/java/org/citrusframework/report/LoggingReporterTest.java index 1813104117..a05471ac92 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/report/LoggingReporterTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/report/LoggingReporterTest.java @@ -16,9 +16,6 @@ package org.citrusframework.report; -import java.time.Duration; -import java.util.Locale; - import org.citrusframework.DefaultTestCase; import org.citrusframework.actions.EchoAction; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -29,6 +26,9 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import java.time.Duration; +import java.util.Locale; + import static java.lang.String.format; import static org.citrusframework.TestResult.failed; import static org.citrusframework.TestResult.skipped; diff --git a/core/citrus-base/src/test/java/org/citrusframework/report/MessageTracingTestListenerTest.java b/core/citrus-base/src/test/java/org/citrusframework/report/MessageTracingTestListenerTest.java index 9239cc659a..798aba387d 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/report/MessageTracingTestListenerTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/report/MessageTracingTestListenerTest.java @@ -16,10 +16,6 @@ package org.citrusframework.report; -import java.io.File; -import java.io.IOException; -import java.util.Scanner; - import org.citrusframework.TestCase; import org.citrusframework.UnitTestSupport; import org.citrusframework.message.RawMessage; @@ -27,6 +23,10 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.io.File; +import java.io.IOException; +import java.util.Scanner; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -43,13 +43,13 @@ public void setupTestling() { } @Test - public void shouldReturnTheSameTraceFile() throws Exception { + public void shouldReturnTheSameTraceFile() { String testname = "SomeDummyTest"; Assert.assertEquals(testling.getTraceFile(testname).getAbsolutePath(), testling.getTraceFile(testname).getAbsolutePath()); } @Test - public void shouldContainMessages() throws Exception { + public void shouldContainMessages() { String testname = "SomeDummyTest"; String inboundPayload = "Inbound Message"; String outboundPayload = "Outbound Message"; diff --git a/core/citrus-base/src/test/java/org/citrusframework/util/FileUtilsTest.java b/core/citrus-base/src/test/java/org/citrusframework/util/FileUtilsTest.java index 2b4da6f619..6c52d67422 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/util/FileUtilsTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/util/FileUtilsTest.java @@ -16,20 +16,20 @@ package org.citrusframework.util; -import java.nio.charset.StandardCharsets; - import org.citrusframework.CitrusSettings; import org.citrusframework.UnitTestSupport; import org.citrusframework.spi.Resource; import org.testng.Assert; import org.testng.annotations.Test; +import java.nio.charset.StandardCharsets; + /** * @since 2.7 */ public class FileUtilsTest extends UnitTestSupport { @Test - public void testGetFileResource() throws Exception { + public void testGetFileResource() { Resource resource = FileUtils.getFileResource("classpath:citrus-context.xml", context); Assert.assertNotNull(resource); @@ -37,7 +37,7 @@ public void testGetFileResource() throws Exception { } @Test - public void testGetFileResourceExplicitCharset() throws Exception { + public void testGetFileResourceExplicitCharset() { Resource resource = FileUtils.getFileResource("classpath:citrus-context.xml" + FileUtils.FILE_PATH_CHARSET_PARAMETER + "ISO-8859-1", context); Assert.assertNotNull(resource); @@ -45,13 +45,13 @@ public void testGetFileResourceExplicitCharset() throws Exception { } @Test - public void testGetCharset() throws Exception { + public void testGetCharset() { Assert.assertEquals(FileUtils.getCharset("/path/to/some/file.txt").displayName(), CitrusSettings.CITRUS_FILE_ENCODING); Assert.assertEquals(FileUtils.getCharset("/path/to/some/file.txt" + FileUtils.FILE_PATH_CHARSET_PARAMETER + "ISO-8859-1"), StandardCharsets.ISO_8859_1); } @Test - public void testGetBaseName() throws Exception { + public void testGetBaseName() { Assert.assertNull(FileUtils.getBaseName(null)); Assert.assertEquals(FileUtils.getBaseName(""), ""); Assert.assertEquals(FileUtils.getBaseName("foo"), "foo"); @@ -61,7 +61,7 @@ public void testGetBaseName() throws Exception { } @Test - public void testGetFileName() throws Exception { + public void testGetFileName() { Assert.assertEquals(FileUtils.getFileName(null), ""); Assert.assertEquals(FileUtils.getFileName(""), ""); Assert.assertEquals(FileUtils.getFileName("foo"), "foo"); diff --git a/core/citrus-base/src/test/java/org/citrusframework/util/InvocationDummy.java b/core/citrus-base/src/test/java/org/citrusframework/util/InvocationDummy.java index 8cf2d5e467..be758c3205 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/util/InvocationDummy.java +++ b/core/citrus-base/src/test/java/org/citrusframework/util/InvocationDummy.java @@ -27,44 +27,34 @@ public class InvocationDummy { private static final Logger logger = LoggerFactory.getLogger(InvocationDummy.class); public InvocationDummy() { - if (logger.isDebugEnabled()) { - logger.debug("Constructor without argument"); - } + logger.debug("Constructor without argument"); } public InvocationDummy(String arg) { - if (logger.isDebugEnabled()) { - logger.debug("Constructor with argument: " + arg); - } + logger.debug("Constructor with argument: {}", arg); } public InvocationDummy(Integer arg1, String arg2, Boolean arg3) { if (logger.isDebugEnabled()) { - if (logger.isDebugEnabled()) { - logger.debug("Constructor with arguments:"); - logger.debug("arg1: " + arg1); - logger.debug("arg2: " + arg2); - logger.debug("arg3: " + arg3); - } + logger.debug("Constructor with arguments:"); + logger.debug("arg1: {}", arg1); + logger.debug("arg2: {}", arg2); + logger.debug("arg3: {}", arg3); } } public void invoke() { - if (logger.isDebugEnabled()) { - logger.debug("Methode invoke no arguments"); - } + logger.debug("Methode invoke no arguments"); } public void invoke(String text) { - if (logger.isDebugEnabled()) { - logger.debug("Methode invoke with string argument: '" + text + "'"); - } + logger.debug("Methode invoke with string argument: '{}'", text); } public void invoke(String[] args) { - for (var arg : args) { - if (logger.isDebugEnabled()) { - logger.debug("Methode invoke with argument: " + arg); + if (logger.isDebugEnabled()) { + for (var arg : args) { + logger.debug("Methode invoke with argument: {}", arg); } } } @@ -72,16 +62,16 @@ public void invoke(String[] args) { public void invoke(Integer arg1, String arg2, Boolean arg3) { if (logger.isDebugEnabled()) { logger.debug("Method invoke with arguments:"); - logger.debug("arg1: " + arg1); - logger.debug("arg2: " + arg2); - logger.debug("arg3: " + arg3); + logger.debug("arg1: {}", arg1); + logger.debug("arg2: {}", arg2); + logger.debug("arg3: {}", arg3); } } public static void main(String[] args) { - for (int i = 0; i < args.length; i++) { - if (logger.isDebugEnabled()) { - logger.debug("arg" + i + ": " + args[i]); + if (logger.isDebugEnabled()) { + for (String arg : args) { + logger.debug("arg{}: ", arg); } } } diff --git a/core/citrus-base/src/test/java/org/citrusframework/util/StringUtilsTest.java b/core/citrus-base/src/test/java/org/citrusframework/util/StringUtilsTest.java index dbed312239..8f067a0bc3 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/util/StringUtilsTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/util/StringUtilsTest.java @@ -1,13 +1,17 @@ package org.citrusframework.util; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + import static org.citrusframework.util.StringUtils.hasText; import static org.citrusframework.util.StringUtils.isEmpty; +import static org.citrusframework.util.StringUtils.quote; +import static org.citrusframework.util.StringUtils.trimTrailingComma; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - public class StringUtilsTest { @DataProvider @@ -34,6 +38,7 @@ public void hasText_returnsTrue(String str) { public void hasText_returnsFalse_forBlankText(String str) { assertFalse(hasText(str)); } + @Test(dataProvider = "emptyText") public void hasText_returnsFalse_forEmptyText(String str) { assertFalse(hasText(str)); @@ -53,4 +58,132 @@ public void isEmpty_returnsFalse_forText(String str) { public void isEmpty_returnsFalse_forBlankText(String str) { assertFalse(isEmpty(str)); } + + @Test + public void appendSegmentToPath() { + assertEquals(StringUtils.appendSegmentToUrlPath("s1", "s2"), "s1/s2"); + assertEquals(StringUtils.appendSegmentToUrlPath("s1", ""), "s1"); + assertEquals(StringUtils.appendSegmentToUrlPath("s1/", "s2"), "s1/s2"); + assertEquals(StringUtils.appendSegmentToUrlPath("s1/", ""), "s1/"); + assertEquals(StringUtils.appendSegmentToUrlPath("s1/", "/s2"), "s1/s2"); + assertEquals(StringUtils.appendSegmentToUrlPath("/s1", "/s2"), "/s1/s2"); + assertEquals(StringUtils.appendSegmentToUrlPath("/s1/", "/s2"), "/s1/s2"); + assertEquals(StringUtils.appendSegmentToUrlPath("/s1/", "/s2/"), "/s1/s2/"); + assertEquals(StringUtils.appendSegmentToUrlPath("/s1/", null), "/s1/"); + assertEquals(StringUtils.appendSegmentToUrlPath(null, "/s2/"), "/s2/"); + assertNull(StringUtils.appendSegmentToUrlPath(null, null)); + } + + @Test + public void testQuoteTrue() { + String input = "Hello, World!"; + String expected = "\"Hello, World!\""; + String result = quote(input, true); + + assertEquals(result, expected, "The text should be quoted."); + } + + @Test + public void testQuoteFalse() { + String input = "Hello, World!"; + String expected = "Hello, World!"; + String result = quote(input, false); + + assertEquals(result, expected, "The text should not be quoted."); + } + + @Test + public void testQuoteEmptyStringTrue() { + String input = ""; + String expected = "\"\""; + String result = quote(input, true); + + assertEquals(result, expected, "The empty text should be quoted."); + } + + @Test + public void testQuoteEmptyStringFalse() { + String input = ""; + String expected = ""; + String result = quote(input, false); + + assertEquals(result, expected, "The empty text should not be quoted."); + } + + @Test + public void testQuoteNullStringTrue() { + String input = null; + String expected = "\"null\""; + String result = quote(input, true); + + assertEquals(result, expected, "The null text should be treated as a string 'null'."); + } + + @Test + public void testQuoteNullStringFalse() { + assertNull(quote(null, false)); + } + + @DataProvider(name = "trimTrailingCommaDataProvider") + public Object[][] trimTrailingCommaDataProvider() { + return new Object[][]{ + {new StringBuilder("Example text, "), "Example text"}, + {new StringBuilder("No trailing comma "), "No trailing comma"}, + {new StringBuilder("No trailing comma,\n\t\n "), "No trailing comma"}, + {new StringBuilder("Trailing comma,"), "Trailing comma"}, + {new StringBuilder("Multiple commas and spaces,,, "), "Multiple commas and spaces,,"}, + {new StringBuilder("No trim needed"), "No trim needed"}, + {new StringBuilder(), ""} + }; + } + + @Test(dataProvider = "trimTrailingCommaDataProvider") + public void testTrimTrailingComma(StringBuilder input, String expected) { + trimTrailingComma(input); + assertEquals(input.toString(), expected); + } + + @Test + public void testTrimTrailingCommaOnlySpaces() { + StringBuilder builder = new StringBuilder(" "); + trimTrailingComma(builder); + assertEquals(builder.toString(), ""); + + builder = new StringBuilder(","); + trimTrailingComma(builder); + assertEquals(builder.toString(), ""); + + builder = new StringBuilder(", , "); + trimTrailingComma(builder); + assertEquals(builder.toString(), ", "); + } + + @Test + public void testTrimTrailingCommaWithNull() { + StringBuilder builder = new StringBuilder(); + trimTrailingComma(builder); + assertEquals(builder.toString(), ""); + } + + @DataProvider + public Object[][] convertFirstChartToUpperCaseData() { + return new Object[][]{ + {"hello", "Hello"}, + {"h", "H"}, + {"Hello", "Hello"}, + {null, ""}, + {"", ""}, + {"hello world", "Hello world"}, + {" hello", " hello"}, + {"1test", "1test"}, + {"!special", "!special"} + }; + } + + @Test(dataProvider = "convertFirstChartToUpperCaseData") + public void testConvertFirstChartToUpperCase(String input, String expected) { + String actual = StringUtils.convertFirstChartToUpperCase(input); + assertEquals(actual, expected, + "The titleCase method did not return the expected result."); + } } diff --git a/core/citrus-base/src/test/java/org/citrusframework/validation/DefaultMessageHeaderValidatorTest.java b/core/citrus-base/src/test/java/org/citrusframework/validation/DefaultMessageHeaderValidatorTest.java index a23f8f800c..234d9457a5 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/validation/DefaultMessageHeaderValidatorTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/validation/DefaultMessageHeaderValidatorTest.java @@ -32,7 +32,7 @@ public class DefaultMessageHeaderValidatorTest extends UnitTestSupport { private HeaderValidationContext validationContext = new HeaderValidationContext(); @Test - public void testValidateNoMessageHeaders() throws Exception { + public void testValidateNoMessageHeaders() { Message receivedMessage = new DefaultMessage("Hello World!"); Message controlMessage = new DefaultMessage("Hello World!"); @@ -40,7 +40,7 @@ public void testValidateNoMessageHeaders() throws Exception { } @Test - public void testValidateMessageHeaders() throws Exception { + public void testValidateMessageHeaders() { Message receivedMessage = new DefaultMessage("Hello World!") .setHeader("foo", "foo_test") .setHeader("additional", "additional") @@ -53,7 +53,7 @@ public void testValidateMessageHeaders() throws Exception { } @Test - public void testValidateMessageHeadersIgnoreCase() throws Exception { + public void testValidateMessageHeadersIgnoreCase() { try { Message receivedMessage = new DefaultMessage("Hello World!") .setHeader("X-Foo", "foo_test") @@ -71,7 +71,7 @@ public void testValidateMessageHeadersIgnoreCase() throws Exception { } @Test(expectedExceptions = ValidationException.class) - public void testValidateMessageHeadersIgnoreCaseError() throws Exception { + public void testValidateMessageHeadersIgnoreCaseError() { Message receivedMessage = new DefaultMessage("Hello World!") .setHeader("X-Foo", "foo_test") .setHeader("X-Additional", "additional") @@ -84,7 +84,7 @@ public void testValidateMessageHeadersIgnoreCaseError() throws Exception { } @Test - public void testValidateMessageHeadersVariableSupport() throws Exception { + public void testValidateMessageHeadersVariableSupport() { Message receivedMessage = new DefaultMessage("Hello World!") .setHeader("foo", "foo_test") .setHeader("additional", "additional") @@ -99,7 +99,7 @@ public void testValidateMessageHeadersVariableSupport() throws Exception { } @Test - public void testValidateMessageHeadersMatcherSupport() throws Exception { + public void testValidateMessageHeadersMatcherSupport() { Message receivedMessage = new DefaultMessage("Hello World!") .setHeader("foo", "foo_test") .setHeader("additional", "additional") @@ -112,7 +112,7 @@ public void testValidateMessageHeadersMatcherSupport() throws Exception { } @Test(expectedExceptions = ValidationException.class) - public void testValidateError() throws Exception { + public void testValidateError() { Message receivedMessage = new DefaultMessage("Hello World!") .setHeader("foo", "other_value") .setHeader("bar", "bar_test"); @@ -124,7 +124,7 @@ public void testValidateError() throws Exception { } @Test(expectedExceptions = ValidationException.class) - public void testValidateErrorMissingHeader() throws Exception { + public void testValidateErrorMissingHeader() { Message receivedMessage = new DefaultMessage("Hello World!") .setHeader("bar", "bar_test"); Message controlMessage = new DefaultMessage("Hello World!") 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 30b947de8b..2102b7998c 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 @@ -16,8 +16,6 @@ package org.citrusframework.validation; -import java.nio.charset.StandardCharsets; - import org.citrusframework.UnitTestSupport; import org.citrusframework.exceptions.ValidationException; import org.citrusframework.message.DefaultMessage; @@ -27,6 +25,8 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.nio.charset.StandardCharsets; + public class DefaultTextEqualsMessageValidatorTest extends UnitTestSupport { private final DefaultTextEqualsMessageValidator validator = new DefaultTextEqualsMessageValidator(); @@ -41,7 +41,7 @@ public void testValidate(Object received, Object control) { } @Test(dataProvider = "errorTests", expectedExceptions = ValidationException.class) - public void testValidateError(Object received, Object control) throws Exception { + public void testValidateError(Object received, Object control) { Message receivedMessage = new DefaultMessage(received); Message controlMessage = new DefaultMessage(control); diff --git a/core/citrus-base/src/test/java/org/citrusframework/validation/MessageValidatorRegistryTest.java b/core/citrus-base/src/test/java/org/citrusframework/validation/MessageValidatorRegistryTest.java index a3edb691a2..cd5803a860 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/validation/MessageValidatorRegistryTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/validation/MessageValidatorRegistryTest.java @@ -16,11 +16,6 @@ package org.citrusframework.validation; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; @@ -34,6 +29,11 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -118,7 +118,7 @@ public void setupMocks() { } @Test - public void testFindMessageValidators() throws Exception { + public void testFindMessageValidators() { MessageValidatorRegistry messageValidatorRegistry = new MessageValidatorRegistry(); Map> messageValidators = new HashMap<>(); @@ -161,7 +161,7 @@ public void testFindMessageValidators() throws Exception { } @Test - public void testMessageValidatorRegistryXmlConfig() throws Exception { + public void testMessageValidatorRegistryXmlConfig() { //non XML message type List> matchingValidators = messageValidatorRegistry.findMessageValidators(MessageType.PLAINTEXT.name(), new DefaultMessage("")); @@ -193,7 +193,7 @@ public void testMessageValidatorRegistryXmlConfig() throws Exception { } @Test - public void testMessageValidatorRegistryJsonConfig() throws Exception { + public void testMessageValidatorRegistryJsonConfig() { //JSON message type and empty payload List> matchingValidators = messageValidatorRegistry.findMessageValidators(MessageType.JSON.name(), new DefaultMessage("")); @@ -216,7 +216,7 @@ public void testMessageValidatorRegistryJsonConfig() throws Exception { } @Test - public void testMessageValidatorRegistryPlaintextConfig() throws Exception { + public void testMessageValidatorRegistryPlaintextConfig() { //Plaintext message type and empty payload List> matchingValidators = messageValidatorRegistry.findMessageValidators(MessageType.PLAINTEXT.name(), new DefaultMessage("")); @@ -237,7 +237,7 @@ public void testMessageValidatorRegistryPlaintextConfig() throws Exception { } @Test - public void testMessageValidatorRegistryFallback() throws Exception { + public void testMessageValidatorRegistryFallback() { List> matchingValidators = messageValidatorRegistry.findMessageValidators(MessageType.XML.name(), new DefaultMessage("{ \"id\": 12345 }")); Assert.assertNotNull(matchingValidators); @@ -339,7 +339,7 @@ public void shouldAddDefaultEmptyMessagePayloadValidator() { } @Test - public void testSchemaValidators() throws Exception { + public void testSchemaValidators() { MessageValidatorRegistry messageValidatorRegistry = new MessageValidatorRegistry(); Map> schemaValidators = new HashMap<>(); diff --git a/core/citrus-base/src/test/java/org/citrusframework/validation/ValidationUtilsTest.java b/core/citrus-base/src/test/java/org/citrusframework/validation/ValidationUtilsTest.java index 2d8fe154fe..0160f1dade 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/validation/ValidationUtilsTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/validation/ValidationUtilsTest.java @@ -16,10 +16,6 @@ package org.citrusframework.validation; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - import org.citrusframework.UnitTestSupport; import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; @@ -29,6 +25,10 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import static org.citrusframework.validation.ValidationUtils.buildValueToBeInCollectionErrorMessage; import static org.testng.Assert.assertEquals; @@ -45,17 +45,17 @@ protected TestContextFactory createTestContextFactory() { } @Test(dataProvider = "testData") - public void testValidateValues(Object actualValue, Object expectedValue, String path) throws Exception { + public void testValidateValues(Object actualValue, Object expectedValue, String path) { ValidationUtils.validateValues(actualValue, expectedValue, path, context); } @Test(dataProvider = "testDataFailed", expectedExceptions = ValidationException.class) - public void testValidateValuesFailure(Object actualValue, Object expectedValue, String path) throws Exception { + public void testValidateValuesFailure(Object actualValue, Object expectedValue, String path) { ValidationUtils.validateValues(actualValue, expectedValue, path, context); } @Test(dataProvider = "testDataTypeFailed", expectedExceptions = ValidationException.class) - public void testValidateValuesTypeFailure(String actualValue, Object expectedValue, String path) throws Exception { + public void testValidateValuesTypeFailure(String actualValue, Object expectedValue, String path) { ValidationUtils.validateValues(actualValue, expectedValue, path, context); } diff --git a/core/citrus-base/src/test/java/org/citrusframework/validation/interceptor/BinaryMessageProcessorTest.java b/core/citrus-base/src/test/java/org/citrusframework/validation/interceptor/BinaryMessageProcessorTest.java index 701b897f53..bf99d1fb68 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/validation/interceptor/BinaryMessageProcessorTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/validation/interceptor/BinaryMessageProcessorTest.java @@ -16,9 +16,6 @@ package org.citrusframework.validation.interceptor; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - import org.citrusframework.UnitTestSupport; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.message.DefaultMessage; @@ -28,6 +25,8 @@ import org.citrusframework.util.FileUtils; import org.testng.annotations.Test; +import java.nio.charset.StandardCharsets; + import static org.testng.Assert.assertEquals; public class BinaryMessageProcessorTest extends UnitTestSupport { @@ -64,7 +63,7 @@ public void testTextMessageIsIntercepted(){ } @Test - public void testResourceMessageWithIsIntercepted() throws IOException { + public void testResourceMessageWithIsIntercepted() { //GIVEN final DefaultMessage message = new DefaultMessage(getTestFile()); diff --git a/core/citrus-base/src/test/java/org/citrusframework/validation/interceptor/GzipMessageProcessorTest.java b/core/citrus-base/src/test/java/org/citrusframework/validation/interceptor/GzipMessageProcessorTest.java index 0a9092bf0b..fa5fb4a058 100644 --- a/core/citrus-base/src/test/java/org/citrusframework/validation/interceptor/GzipMessageProcessorTest.java +++ b/core/citrus-base/src/test/java/org/citrusframework/validation/interceptor/GzipMessageProcessorTest.java @@ -16,13 +16,6 @@ package org.citrusframework.validation.interceptor; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - import org.citrusframework.UnitTestSupport; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.message.DefaultMessage; @@ -33,6 +26,13 @@ import org.testng.Assert; import org.testng.annotations.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + import static org.testng.Assert.assertEquals; public class GzipMessageProcessorTest extends UnitTestSupport { @@ -71,7 +71,7 @@ public void testTextMessageIsIntercepted() throws IOException { //THEN assertEquals(message.getType(), MessageType.GZIP.name()); try (ByteArrayOutputStream unzipped = new ByteArrayOutputStream(); - GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(message.getPayload(byte[].class)));) { + GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(message.getPayload(byte[].class)))) { unzipped.write(gzipInputStream.readAllBytes()); Assert.assertEquals(unzipped.toByteArray(), "foo".getBytes(StandardCharsets.UTF_8)); } @@ -90,7 +90,7 @@ public void testBinaryMessageIsIntercepted() throws IOException { //THEN assertEquals(message.getType(), MessageType.GZIP.name()); try (ByteArrayOutputStream unzipped = new ByteArrayOutputStream(); - GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(message.getPayload(byte[].class)));) { + GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(message.getPayload(byte[].class)))) { unzipped.write(gzipInputStream.readAllBytes()); Assert.assertEquals(unzipped.toByteArray(), "foo".getBytes(StandardCharsets.UTF_8)); } @@ -109,7 +109,7 @@ public void testInputStreamMessageIsIntercepted() throws IOException { //THEN assertEquals(message.getType(), MessageType.GZIP.name()); try (ByteArrayOutputStream unzipped = new ByteArrayOutputStream(); - GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(message.getPayload(byte[].class)));) { + GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(message.getPayload(byte[].class)))) { unzipped.write(gzipInputStream.readAllBytes()); Assert.assertEquals(unzipped.toByteArray(), "foo".getBytes(StandardCharsets.UTF_8)); } @@ -128,7 +128,7 @@ public void testResourceMessageIsIntercepted() throws IOException { //THEN assertEquals(message.getType(), MessageType.GZIP.name()); try (ByteArrayOutputStream unzipped = new ByteArrayOutputStream(); - GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(message.getPayload(byte[].class)));) { + GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(message.getPayload(byte[].class)))) { unzipped.write(gzipInputStream.readAllBytes()); Assert.assertEquals(unzipped.toByteArray(), FileUtils.copyToByteArray(getTestFile().getInputStream())); } diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/ComponentLifecycleProcessor.java b/core/citrus-spring/src/main/java/org/citrusframework/config/ComponentLifecycleProcessor.java index 2badf88110..c0cd1ca250 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/ComponentLifecycleProcessor.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/ComponentLifecycleProcessor.java @@ -47,9 +47,7 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro } if (bean instanceof InitializingPhase) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Initializing component '%s'", beanName)); - } + logger.debug("Initializing component '{}'", beanName); ((InitializingPhase) bean).initialize(); } @@ -59,9 +57,7 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro @Override public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { if (requiresDestruction(bean)) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Destroying component '%s'", beanName)); - } + logger.debug("Destroying component '{}'", beanName); ((ShutdownPhase) bean).destroy(); } } diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/TestCaseFactory.java b/core/citrus-spring/src/main/java/org/citrusframework/config/TestCaseFactory.java index f6c06dc486..008496584c 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/TestCaseFactory.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/TestCaseFactory.java @@ -16,12 +16,12 @@ package org.citrusframework.config; -import java.util.List; - import org.citrusframework.TestAction; import org.citrusframework.TestCase; import org.springframework.beans.factory.FactoryBean; +import java.util.List; + /** * Test case factory bean constructs test cases with test actions and test finally block. * @@ -37,13 +37,13 @@ public class TestCaseFactory implements FactoryBean { @Override public TestCase getObject() throws Exception { - if (this.testActions != null && this.testActions.size() > 0) { + if (this.testActions != null && !this.testActions.isEmpty()) { for (TestAction action : testActions) { testCase.addTestAction(action); } } - if (this.finalActions != null && this.finalActions.size() > 0) { + if (this.finalActions != null && !this.finalActions.isEmpty()) { for (TestAction action : finalActions) { testCase.addFinalAction(action); } diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/handler/CitrusConfigNamespaceHandler.java b/core/citrus-spring/src/main/java/org/citrusframework/config/handler/CitrusConfigNamespaceHandler.java index be4bb210a6..561add9bc1 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/handler/CitrusConfigNamespaceHandler.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/handler/CitrusConfigNamespaceHandler.java @@ -16,8 +16,6 @@ package org.citrusframework.config.handler; -import java.util.Map; - import org.citrusframework.config.xml.DefaultMessageQueueParser; import org.citrusframework.config.xml.DirectEndpointAdapterParser; import org.citrusframework.config.xml.DirectEndpointParser; @@ -44,6 +42,8 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; +import java.util.Map; + /** * Namespace handler for components in Citrus configuration. * @@ -88,9 +88,7 @@ private void lookupBeanDefinitionParser() { actionParserMap.forEach((k, p) -> { registerBeanDefinitionParser(k, p); - if (logger.isDebugEnabled()) { - logger.debug(String.format("Register bean definition parser %s from resource %s", p.getClass(), k)); - } + logger.debug("Register bean definition parser {} from resource {}", p.getClass(), k); }); } } diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/handler/CitrusTestCaseNamespaceHandler.java b/core/citrus-spring/src/main/java/org/citrusframework/config/handler/CitrusTestCaseNamespaceHandler.java index 2174e5f056..e9b5c84024 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/handler/CitrusTestCaseNamespaceHandler.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/handler/CitrusTestCaseNamespaceHandler.java @@ -25,8 +25,6 @@ /** * Namespace handler registers bean definition parser * for Citrus testcase schema elements. - * - * @since 2007 */ public class CitrusTestCaseNamespaceHandler extends NamespaceHandlerSupport { diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/util/VariableExtractorParserUtil.java b/core/citrus-spring/src/main/java/org/citrusframework/config/util/VariableExtractorParserUtil.java index dae8b76c38..4ca22c2675 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/util/VariableExtractorParserUtil.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/util/VariableExtractorParserUtil.java @@ -16,15 +16,15 @@ package org.citrusframework.config.util; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import org.citrusframework.validation.DelegatingPayloadVariableExtractor; import org.citrusframework.variable.VariableExtractor; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * Helper for parsing 'extract' elements containing nested xpath or json variable-extractors. * @@ -52,7 +52,7 @@ public static void addPayloadVariableExtractors(Element element, List namespaceElements = DomUtils.getChildElementsByTagName(messageElement, "namespace"); - if (namespaceElements.size() > 0) { + if (!namespaceElements.isEmpty()) { for (Object namespaceElementObject : namespaceElements) { Element namespaceElement = (Element) namespaceElementObject; namespaces.put(namespaceElement.getAttribute("prefix"), namespaceElement.getAttribute("value")); diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/AbstractMessageActionParser.java b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/AbstractMessageActionParser.java index 1ac012104d..1409dd283e 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/AbstractMessageActionParser.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/AbstractMessageActionParser.java @@ -16,14 +16,9 @@ package org.citrusframework.config.xml; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - +import org.citrusframework.CitrusSettings; import org.citrusframework.common.Named; +import org.citrusframework.config.util.BeanDefinitionParserUtils; import org.citrusframework.config.xml.parser.CitrusXmlConfigParser; import org.citrusframework.config.xml.parser.ScriptMessageBuilderParser; import org.citrusframework.message.DelegatingPathExpressionProcessor; @@ -47,16 +42,66 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; import org.springframework.util.CollectionUtils; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.citrusframework.util.StringUtils.hasText; + /** * Parser providing basic message element configurations used in send and receive actions. * */ public abstract class AbstractMessageActionParser implements BeanDefinitionParser { + protected BeanDefinitionBuilder getBeanDefinitionBuilder(Element element, ParserContext parserContext) { + String endpointUri = parseEndpoint(element); + + BeanDefinitionBuilder builder = parseComponent(element, parserContext); + builder.addPropertyValue("name", element.getLocalName()); + + if (endpointUri.contains(":") || (endpointUri.contains(CitrusSettings.VARIABLE_PREFIX) && endpointUri.contains(CitrusSettings.VARIABLE_SUFFIX))) { + builder.addPropertyValue("endpointUri", endpointUri); + } else { + builder.addPropertyReference("endpoint", endpointUri); + } + + DescriptionElementParser.doParse(element, builder); + + BeanDefinitionParserUtils.setPropertyReference(builder, element.getAttribute("actor"), "actor"); + return builder; + } + + protected String parseEndpoint(Element element) { + String endpointUri = element.getAttribute("endpoint"); + + if (!hasText(endpointUri)) { + throw new BeanCreationException("Endpoint reference must not be empty"); + } + + return endpointUri; + } + + /** + * Parse component returning generic bean definition. + * @param element + * @param parserContext + * @return + */ + protected BeanDefinitionBuilder parseComponent(Element element, ParserContext parserContext) { + return BeanDefinitionBuilder.genericBeanDefinition(getMessageFactoryClass()); + } + + protected abstract Class getMessageFactoryClass(); + /** * Static parse method taking care of basic message element parsing. * diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/ActionContainerParser.java b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/ActionContainerParser.java index 159986694d..c62353b78b 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/ActionContainerParser.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/ActionContainerParser.java @@ -16,8 +16,6 @@ package org.citrusframework.config.xml; -import java.util.List; - import org.citrusframework.config.CitrusNamespaceParserRegistry; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -27,6 +25,8 @@ import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; +import java.util.List; + /** * Abstract parser implementation that is aware of several embedded test actions of a container. Bean definitions that use * this parser component must have an 'actions' property of type {@link List} in order to receive the list of embedded test actions. @@ -70,7 +70,7 @@ public static void doParse(Element element, ParserContext parserContext, BeanDef } } - if (actions.size() > 0) { + if (!actions.isEmpty()) { builder.addPropertyValue(propertyName, actions); } } diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/BaseTestCaseParser.java b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/BaseTestCaseParser.java index 8c100bd343..efd2ab6963 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/BaseTestCaseParser.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/BaseTestCaseParser.java @@ -35,10 +35,7 @@ import org.w3c.dom.Element; /** - * Base test case for parsing the test case - * - * @param - * + * Base test case for parsing the test case. */ @SuppressWarnings("PMD.AvoidDuplicateLiterals") public class BaseTestCaseParser implements BeanDefinitionParser { diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/ReceiveMessageActionParser.java b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/ReceiveMessageActionParser.java index 5ee0feb42b..50c2460a1a 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/ReceiveMessageActionParser.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/ReceiveMessageActionParser.java @@ -16,18 +16,7 @@ package org.citrusframework.config.xml; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -import org.citrusframework.CitrusSettings; import org.citrusframework.actions.ReceiveMessageAction; -import org.citrusframework.config.util.BeanDefinitionParserUtils; import org.citrusframework.config.util.ValidateMessageParserUtil; import org.citrusframework.config.util.VariableExtractorParserUtil; import org.citrusframework.validation.builder.DefaultMessageBuilder; @@ -50,6 +39,15 @@ import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + import static java.lang.Boolean.parseBoolean; import static org.citrusframework.util.StringUtils.hasText; @@ -62,24 +60,7 @@ public class ReceiveMessageActionParser extends AbstractMessageActionParser { @Override public BeanDefinition parse(Element element, ParserContext parserContext) { - String endpointUri = element.getAttribute("endpoint"); - - if (!hasText(endpointUri)) { - throw new BeanCreationException("Endpoint reference must not be empty"); - } - - BeanDefinitionBuilder builder = parseComponent(element, parserContext); - builder.addPropertyValue("name", element.getLocalName()); - - if (endpointUri.contains(":") || (endpointUri.contains(CitrusSettings.VARIABLE_PREFIX) && endpointUri.contains(CitrusSettings.VARIABLE_SUFFIX))) { - builder.addPropertyValue("endpointUri", endpointUri); - } else { - builder.addPropertyReference("endpoint", endpointUri); - } - - DescriptionElementParser.doParse(element, builder); - - BeanDefinitionParserUtils.setPropertyReference(builder, element.getAttribute("actor"), "actor"); + BeanDefinitionBuilder builder = getBeanDefinitionBuilder(element, parserContext); String receiveTimeout = element.getAttribute("timeout"); if (hasText(receiveTimeout)) { @@ -267,7 +248,7 @@ private XmlMessageValidationContext getXmlMessageValidationContext(Element messa * @param messageElement The message element to get the configuration from * @param context The context to set the schema validation configuration to */ - private void addSchemaInformationToValidationContext(Element messageElement, SchemaValidationContext.Builder context) { + protected void addSchemaInformationToValidationContext(Element messageElement, SchemaValidationContext.Builder context) { String schemaValidation = messageElement.getAttribute("schema-validation"); if (hasText(schemaValidation)) { context.schemaValidation(parseBoolean(schemaValidation)); @@ -317,7 +298,7 @@ private JsonPathMessageValidationContext getJsonPathMessageValidationContext(Ele //for now we only handle jsonPath validation Map validateJsonPathExpressions = new HashMap<>(); List validateElements = DomUtils.getChildElementsByTagName(messageElement, "validate"); - if (validateElements.size() > 0) { + if (!validateElements.isEmpty()) { for (Element validateElement : validateElements) { extractJsonPathValidateExpressions(validateElement, validateJsonPathExpressions); } @@ -384,16 +365,17 @@ private void parseNamespaceValidationElements(Element messageElement, XmlMessage Map validateNamespaces = new HashMap<>(); List validateElements = DomUtils.getChildElementsByTagName(messageElement, "validate"); - if (validateElements.size() > 0) { + if (!validateElements.isEmpty()) { for (Element validateElement : validateElements) { //check for namespace validation elements List validateNamespaceElements = DomUtils.getChildElementsByTagName(validateElement, "namespace"); - if (validateNamespaceElements.size() > 0) { + if (!validateNamespaceElements.isEmpty()) { for (Element namespaceElement : validateNamespaceElements) { validateNamespaces.put(namespaceElement.getAttribute("prefix"), namespaceElement.getAttribute("value")); } } } + context.namespaces(validateNamespaces); } } @@ -410,7 +392,7 @@ private void parseXPathValidationElements(Element messageElement, XpathMessageVa Map validateXpathExpressions = new HashMap<>(); List validateElements = DomUtils.getChildElementsByTagName(messageElement, "validate"); - if (validateElements.size() > 0) { + if (!validateElements.isEmpty()) { for (Element validateElement : validateElements) { extractXPathValidateExpressions(validateElement, validateXpathExpressions); } @@ -460,8 +442,7 @@ private void extractXPathValidateExpressions( * @param validateElement * @param validateJsonPathExpressions */ - private void extractJsonPathValidateExpressions( - Element validateElement, Map validateJsonPathExpressions) { + private void extractJsonPathValidateExpressions(Element validateElement, Map validateJsonPathExpressions) { //check for jsonPath validation - old style with direct attribute String pathExpression = validateElement.getAttribute("path"); if (JsonPathMessageValidationContext.isJsonPathExpression(pathExpression)) { @@ -472,14 +453,9 @@ private void extractJsonPathValidateExpressions( ValidateMessageParserUtil.parseJsonPathElements(validateElement, validateJsonPathExpressions); } - /** - * Parse component returning generic bean definition. - * - * @param element - * @return - */ - protected BeanDefinitionBuilder parseComponent(Element element, ParserContext parserContext) { - return BeanDefinitionBuilder.genericBeanDefinition(ReceiveMessageActionFactoryBean.class); + @Override + protected Class getMessageFactoryClass() { + return ReceiveMessageActionFactoryBean.class; } /** diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SchemaParser.java b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SchemaParser.java index 461bbfbc73..fd5e7e8fad 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SchemaParser.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SchemaParser.java @@ -16,9 +16,6 @@ package org.citrusframework.config.xml; -import java.util.HashMap; -import java.util.Map; - import org.citrusframework.spi.ResourcePathTypeResolver; import org.citrusframework.util.FileUtils; import org.slf4j.Logger; @@ -28,6 +25,9 @@ import org.springframework.beans.factory.xml.ParserContext; import org.w3c.dom.Element; +import java.util.HashMap; +import java.util.Map; + /** * Bean definition parser for schema configuration. * @@ -68,7 +68,7 @@ private BeanDefinitionParser lookupSchemaParser(String location) { } BeanDefinitionParser parser = TYPE_RESOLVER.resolve(fileExtension); - logger.info(String.format("Found schema bean definition parser %s from resource %s", parser.getClass(), RESOURCE_PATH + "/" + fileExtension)); + logger.info("Found schema bean definition parser {} from resource {}", parser.getClass(), RESOURCE_PATH + "/" + fileExtension); SCHEMA_PARSER.put(fileExtension, parser); return parser; } diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SchemaRepositoryParser.java b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SchemaRepositoryParser.java index 98c9e95cec..2a9a2fd41c 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SchemaRepositoryParser.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SchemaRepositoryParser.java @@ -16,11 +16,6 @@ package org.citrusframework.config.xml; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - import org.citrusframework.spi.ResourcePathTypeResolver; import org.citrusframework.util.StringUtils; import org.slf4j.Logger; @@ -34,6 +29,11 @@ import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + /** * Bean definition parser for schema-repository configuration. * @@ -84,7 +84,7 @@ private BeanDefinitionParser lookupSchemaRepositoryParser(String type) { } BeanDefinitionParser parser = TYPE_RESOLVER.resolve(type); - logger.info(String.format("Found schema repository bean definition parser %s from resource %s", parser.getClass(), RESOURCE_PATH + "/" + type)); + logger.info("Found schema repository bean definition parser {} from resource {}", parser.getClass(), RESOURCE_PATH + "/" + type); SCHEMA_REPOSITORY_PARSER.put(type, parser); return parser; } diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SendMessageActionParser.java b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SendMessageActionParser.java index ec3f5ee57f..470f57da33 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SendMessageActionParser.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/SendMessageActionParser.java @@ -16,23 +16,22 @@ package org.citrusframework.config.xml; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.citrusframework.CitrusSettings; import org.citrusframework.actions.SendMessageAction; import org.citrusframework.config.util.BeanDefinitionParserUtils; +import org.citrusframework.message.MessageBuilder; import org.citrusframework.util.StringUtils; import org.citrusframework.validation.builder.DefaultMessageBuilder; import org.citrusframework.variable.VariableExtractor; -import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Bean definition parser for send action in test case. * @@ -41,23 +40,7 @@ public class SendMessageActionParser extends AbstractMessageActionParser { @Override public BeanDefinition parse(Element element, ParserContext parserContext) { - String endpointUri = element.getAttribute("endpoint"); - - if (!StringUtils.hasText(endpointUri)) { - throw new BeanCreationException("Endpoint reference must not be empty"); - } - - BeanDefinitionBuilder builder = parseComponent(element, parserContext); - builder.addPropertyValue("name", element.getLocalName()); - - if (endpointUri.contains(":") || (endpointUri.contains(CitrusSettings.VARIABLE_PREFIX) && endpointUri.contains(CitrusSettings.VARIABLE_SUFFIX))) { - builder.addPropertyValue("endpointUri", endpointUri); - } else { - builder.addPropertyReference("endpoint", endpointUri); - } - - DescriptionElementParser.doParse(element, builder); - BeanDefinitionParserUtils.setPropertyReference(builder, element.getAttribute("actor"), "actor"); + BeanDefinitionBuilder builder = getBeanDefinitionBuilder(element, parserContext); BeanDefinitionParserUtils.setPropertyValue(builder, element.getAttribute("fork"), "forkMode"); Element messageElement = DomUtils.getChildElementByTagName(element, "message"); @@ -108,21 +91,11 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { return builder.getBeanDefinition(); } - /** - * Parse component returning generic bean definition. - * @param element - * @param parserContext - * @return - */ - protected BeanDefinitionBuilder parseComponent(Element element, ParserContext parserContext) { - return BeanDefinitionBuilder.genericBeanDefinition(getBeanDefinitionClass()); - } - /** * Gets the bean definition builder class. - * @return */ - protected Class> getBeanDefinitionClass() { + @Override + protected Class> getMessageFactoryClass() { return SendMessageActionFactoryBean.class; } @@ -131,7 +104,17 @@ protected BeanDefinitionBuilder parseComponent(Element element, ParserContext pa */ public static class SendMessageActionFactoryBean extends AbstractSendMessageActionFactoryBean { - private final SendMessageAction.Builder builder = new SendMessageAction.Builder(); + private final SendMessageAction.Builder builder; + + + public SendMessageActionFactoryBean() { + builder = new SendMessageAction.Builder(); + } + + public SendMessageActionFactoryBean(MessageBuilder messageBuilder) { + builder = new SendMessageAction.Builder(); + builder.message(messageBuilder); + } @Override public SendMessageAction getObject() throws Exception { diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/StartServerActionParser.java b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/StartServerActionParser.java index 898c0b2cce..7d4c1db69e 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/StartServerActionParser.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/StartServerActionParser.java @@ -16,8 +16,6 @@ package org.citrusframework.config.xml; -import java.util.List; - import org.citrusframework.actions.StartServerAction; import org.citrusframework.config.util.BeanDefinitionParserUtils; import org.citrusframework.server.Server; @@ -30,6 +28,8 @@ import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; +import java.util.List; + /** * @since 2.2 */ @@ -47,7 +47,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { Element serversElement = DomUtils.getChildElementByTagName(element, "servers"); if (serversElement != null) { List serverElements = DomUtils.getChildElementsByTagName(serversElement, "server"); - if (serverElements.size() > 0) { + if (!serverElements.isEmpty()) { for (Element serverElement : serverElements) { servers.add(new RuntimeBeanReference(serverElement.getAttribute("name"))); } diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/StopServerActionParser.java b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/StopServerActionParser.java index 6310de499f..b2539b4d20 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/StopServerActionParser.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/StopServerActionParser.java @@ -16,8 +16,6 @@ package org.citrusframework.config.xml; -import java.util.List; - import org.citrusframework.actions.StopServerAction; import org.citrusframework.config.util.BeanDefinitionParserUtils; import org.citrusframework.server.Server; @@ -30,6 +28,8 @@ import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; +import java.util.List; + /** * @since 2.2 */ @@ -47,7 +47,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { Element serversElement = DomUtils.getChildElementByTagName(element, "servers"); if (serversElement != null) { List serverElements = DomUtils.getChildElementsByTagName(serversElement, "server"); - if (serverElements.size() > 0) { + if (!serverElements.isEmpty()) { for (Element serverElement : serverElements) { servers.add(new RuntimeBeanReference(serverElement.getAttribute("name"))); } diff --git a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/parser/CitrusXmlConfigParser.java b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/parser/CitrusXmlConfigParser.java index 72fb4ef8ca..cc4301f812 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/config/xml/parser/CitrusXmlConfigParser.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/config/xml/parser/CitrusXmlConfigParser.java @@ -16,15 +16,15 @@ package org.citrusframework.config.xml.parser; -import java.util.Map; -import java.util.Optional; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ResourcePathTypeResolver; import org.citrusframework.spi.TypeResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; +import java.util.Optional; + public interface CitrusXmlConfigParser { /** Logger */ @@ -45,7 +45,7 @@ static Map lookup(String category) { Map parser = TYPE_RESOLVER.resolveAll(category, TypeResolver.DEFAULT_TYPE_PROPERTY, null); if (logger.isDebugEnabled()) { - parser.forEach((k, v) -> logger.debug(String.format("Found XML config parser '%s/%s' as %s", category, k, v.getClass()))); + parser.forEach((k, v) -> logger.debug("Found XML config parser '{}/{}' as {}", category, k, v.getClass())); } return parser; @@ -64,7 +64,7 @@ static Optional lookup(String category, String name) { T instance = TYPE_RESOLVER.resolve(category + "/" + name); return Optional.of(instance); } catch (CitrusRuntimeException e) { - logger.warn(String.format("Failed to resolve XML config parser from resource '%s/%s/%s'", RESOURCE_PATH, category, name)); + logger.warn("Failed to resolve XML config parser from resource '{}/{}/{}'", RESOURCE_PATH, category, name); } return Optional.empty(); diff --git a/core/citrus-spring/src/main/java/org/citrusframework/context/SpringBeanReferenceResolver.java b/core/citrus-spring/src/main/java/org/citrusframework/context/SpringBeanReferenceResolver.java index 641716e76e..9b2f03f9e4 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/context/SpringBeanReferenceResolver.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/context/SpringBeanReferenceResolver.java @@ -16,13 +16,6 @@ package org.citrusframework.context; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; - import org.citrusframework.context.resolver.TypeAliasResolver; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.spi.ReferenceResolver; @@ -34,6 +27,13 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + /** * Spring bean reference resolver operates on given application context to resolve bean references. * @@ -209,7 +209,7 @@ private Optional resolveAlias(Class source, Function, ?> supp try { return Optional.of(resolver.adapt(supplier.apply(resolver.getAliasType()))); } catch (Exception e) { - logger.warn(String.format("Unable to resolve alias type %s for required source %s", resolver.getAliasType(), source)); + logger.warn("Unable to resolve alias type {} for required source {}", resolver.getAliasType(), source); return Optional.empty(); } } @@ -238,7 +238,7 @@ private Optional> resolveAllAlias(Class source, Function resolver.adapt(v.getValue())))); } catch (Exception e) { - logger.warn(String.format("Unable to resolve alias type %s for required source %s", resolver.getAliasType(), source)); + logger.warn("Unable to resolve alias type {} for required source {}", resolver.getAliasType(), source); return Optional.empty(); } } diff --git a/core/citrus-spring/src/main/java/org/citrusframework/util/SpringBeanTypeConverter.java b/core/citrus-spring/src/main/java/org/citrusframework/util/SpringBeanTypeConverter.java index 67c32c0be1..5892c5da8f 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/util/SpringBeanTypeConverter.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/util/SpringBeanTypeConverter.java @@ -16,16 +16,6 @@ package org.citrusframework.util; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.util.Map; -import java.util.Optional; -import java.util.Properties; -import javax.xml.transform.Source; -import javax.xml.transform.stream.StreamSource; - import org.citrusframework.exceptions.CitrusRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,10 +25,20 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; + public final class SpringBeanTypeConverter extends DefaultTypeConverter { /** Logger */ - private static final Logger logger = LoggerFactory.getLogger(DefaultTypeConverter.class); + private static final Logger logger = LoggerFactory.getLogger(SpringBeanTypeConverter.class); public static SpringBeanTypeConverter INSTANCE = new SpringBeanTypeConverter(); diff --git a/core/citrus-spring/src/main/java/org/citrusframework/variable/GlobalVariablesPropertyLoader.java b/core/citrus-spring/src/main/java/org/citrusframework/variable/GlobalVariablesPropertyLoader.java index ad70028756..03e5047314 100644 --- a/core/citrus-spring/src/main/java/org/citrusframework/variable/GlobalVariablesPropertyLoader.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/variable/GlobalVariablesPropertyLoader.java @@ -16,12 +16,6 @@ package org.citrusframework.variable; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.List; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.functions.FunctionRegistry; @@ -33,6 +27,12 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + /** * Loads properties from an external property file and creates global test variables. * @@ -69,7 +69,7 @@ public void afterPropertiesSet() { throw new CitrusRuntimeException(String.format("Error while loading property file %s - does not exist", propertyFile.getLocation())); } - logger.debug("Reading property file " + propertyFile.getLocation()); + logger.debug("Reading property file {}", propertyFile.getLocation()); // Use input stream as this also allows to read from resources in a JAR file reader = new BufferedReader(new InputStreamReader(propertyFile.getInputStream())); @@ -98,13 +98,10 @@ public void afterPropertiesSet() { logger.debug("Property value replace dynamic content [ {} ]", value); value = context.replaceDynamicContentInString(value); - if (logger.isDebugEnabled()) { - logger.debug("Loading property: " + key + "=" + value + " into default variables"); - } + logger.debug("Loading property: {}={} into default variables", key, value); if (logger.isDebugEnabled() && globalVariables.getVariables().containsKey(key)) { - logger.debug("Overwriting property " + key + " old value:" + globalVariables.getVariables().get(key) - + " new value:" + value); + logger.debug("Overwriting property {} old value:{} new value:{}", key, globalVariables.getVariables().get(key), value); } globalVariables.getVariables().put(key, value); @@ -112,7 +109,7 @@ public void afterPropertiesSet() { context.setVariable(key, globalVariables.getVariables().get(key)); } - logger.info("Loaded property file " + propertyFile.getLocation()); + logger.info("Loaded property file {}", propertyFile.getLocation()); } } } catch (IOException e) { @@ -129,7 +126,7 @@ public void afterPropertiesSet() { } private boolean propertyFilesSet() { - return propertyFiles != null && propertyFiles.size() > 0; + return propertyFiles != null && !propertyFiles.isEmpty(); } private boolean isPropertyLine(String line) { diff --git a/core/citrus-spring/src/test/java/org/citrusframework/util/InvocationDummy.java b/core/citrus-spring/src/test/java/org/citrusframework/util/InvocationDummy.java index 8cf2d5e467..a352f894fd 100644 --- a/core/citrus-spring/src/test/java/org/citrusframework/util/InvocationDummy.java +++ b/core/citrus-spring/src/test/java/org/citrusframework/util/InvocationDummy.java @@ -27,44 +27,34 @@ public class InvocationDummy { private static final Logger logger = LoggerFactory.getLogger(InvocationDummy.class); public InvocationDummy() { - if (logger.isDebugEnabled()) { - logger.debug("Constructor without argument"); - } + logger.debug("Constructor without argument"); } public InvocationDummy(String arg) { - if (logger.isDebugEnabled()) { - logger.debug("Constructor with argument: " + arg); - } + logger.debug("Constructor with argument: {}", arg); } public InvocationDummy(Integer arg1, String arg2, Boolean arg3) { if (logger.isDebugEnabled()) { - if (logger.isDebugEnabled()) { - logger.debug("Constructor with arguments:"); - logger.debug("arg1: " + arg1); - logger.debug("arg2: " + arg2); - logger.debug("arg3: " + arg3); - } + logger.debug("Constructor with arguments:"); + logger.debug("arg1: {}", arg1); + logger.debug("arg2: {}", arg2); + logger.debug("arg3: {}", arg3); } } public void invoke() { - if (logger.isDebugEnabled()) { - logger.debug("Methode invoke no arguments"); - } + logger.debug("Methode invoke no arguments"); } public void invoke(String text) { - if (logger.isDebugEnabled()) { - logger.debug("Methode invoke with string argument: '" + text + "'"); - } + logger.debug("Methode invoke with string argument: '{}'", text); } public void invoke(String[] args) { - for (var arg : args) { - if (logger.isDebugEnabled()) { - logger.debug("Methode invoke with argument: " + arg); + if (logger.isDebugEnabled()) { + for (var arg : args) { + logger.debug("Methode invoke with argument: {}", arg); } } } @@ -72,16 +62,16 @@ public void invoke(String[] args) { public void invoke(Integer arg1, String arg2, Boolean arg3) { if (logger.isDebugEnabled()) { logger.debug("Method invoke with arguments:"); - logger.debug("arg1: " + arg1); - logger.debug("arg2: " + arg2); - logger.debug("arg3: " + arg3); + logger.debug("arg1: {}", arg1); + logger.debug("arg2: {}", arg2); + logger.debug("arg3: {}", arg3); } } public static void main(String[] args) { - for (int i = 0; i < args.length; i++) { - if (logger.isDebugEnabled()) { - logger.debug("arg" + i + ": " + args[i]); + if (logger.isDebugEnabled()) { + for (int i = 0; i < args.length; i++) { + logger.debug("arg{}: {}", i, args[i]); } } } diff --git a/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/integration/CamelJBangIT.java b/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/integration/CamelJBangIT.java index a039791085..778fca3b0f 100644 --- a/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/integration/CamelJBangIT.java +++ b/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/integration/CamelJBangIT.java @@ -16,20 +16,27 @@ package org.citrusframework.camel.integration; +import static org.citrusframework.camel.dsl.CamelSupport.camel; +import static org.citrusframework.container.Catch.Builder.catchException; +import static org.citrusframework.container.FinallySequence.Builder.doFinally; + import org.citrusframework.annotations.CitrusTest; import org.citrusframework.spi.Resources; import org.citrusframework.testng.TestNGCitrusSupport; +import org.citrusframework.util.TestUtils; +import org.testng.SkipException; import org.testng.annotations.Test; -import static org.citrusframework.camel.dsl.CamelSupport.camel; -import static org.citrusframework.container.Catch.Builder.catchException; -import static org.citrusframework.container.FinallySequence.Builder.doFinally; - public class CamelJBangIT extends TestNGCitrusSupport { @Test @CitrusTest(name = "RunIntegration_SourceCode_IT") public void runIntegrationWithSourceCodeIT() { + + if (!TestUtils.isNetworkReachable()) { + throw new SkipException("Test skipped because network is not reachable. We are probably running behind a proxy and JBang download is not possible."); + } + given(doFinally().actions( catchException().actions(camel().jbang().stop("hello")) )); @@ -58,6 +65,11 @@ public void runIntegrationWithSourceCodeIT() { @Test @CitrusTest(name = "RunIntegration_Resource_IT") public void runIntegrationWithResourceIT() { + + if (!TestUtils.isNetworkReachable()) { + throw new SkipException("Test skipped because network is not reachable. We are probably running behind a proxy and JBang download is not possible."); + } + given(doFinally().actions( catchException().actions(camel().jbang().stop("route")) )); diff --git a/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/xml/JBangTest.java b/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/xml/JBangTest.java index 789ca41cd7..e07645dbba 100644 --- a/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/xml/JBangTest.java +++ b/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/xml/JBangTest.java @@ -25,12 +25,22 @@ import org.citrusframework.camel.actions.CamelRunIntegrationAction; import org.citrusframework.camel.actions.CamelStopIntegrationAction; import org.citrusframework.camel.actions.CamelVerifyIntegrationAction; +import org.citrusframework.util.TestUtils; import org.citrusframework.xml.XmlTestLoader; import org.testng.Assert; +import org.testng.SkipException; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; public class JBangTest extends AbstractXmlActionTest { + @BeforeClass + public static void beforeEach() { + if (!TestUtils.isNetworkReachable()) { + throw new SkipException("Test skipped because network is not reachable. We are probably running behind a proxy and JBang download is not possible."); + } + } + @Test public void shouldLoadCamelActions() throws Exception { XmlTestLoader testLoader = createTestLoader("classpath:org/citrusframework/camel/xml/camel-jbang-test.xml"); diff --git a/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/yaml/JBangTest.java b/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/yaml/JBangTest.java index 53610b08eb..8bcb745d07 100644 --- a/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/yaml/JBangTest.java +++ b/endpoints/citrus-camel/src/test/java/org/citrusframework/camel/yaml/JBangTest.java @@ -25,12 +25,22 @@ import org.citrusframework.camel.actions.CamelRunIntegrationAction; import org.citrusframework.camel.actions.CamelStopIntegrationAction; import org.citrusframework.camel.actions.CamelVerifyIntegrationAction; +import org.citrusframework.util.TestUtils; import org.citrusframework.yaml.YamlTestLoader; import org.testng.Assert; +import org.testng.SkipException; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; public class JBangTest extends AbstractYamlActionTest { + @BeforeClass + public static void beforeEach() { + if (!TestUtils.isNetworkReachable()) { + throw new SkipException("Test skipped because network is not reachable. We are probably running behind a proxy and JBang download is not possible."); + } + } + @Test public void shouldLoadCamelActions() throws Exception { YamlTestLoader testLoader = createTestLoader("classpath:org/citrusframework/camel/yaml/camel-jbang-test.yaml"); diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientRequestActionBuilder.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientRequestActionBuilder.java index cced421b22..f7c1e9e3c2 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientRequestActionBuilder.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientRequestActionBuilder.java @@ -17,7 +17,6 @@ package org.citrusframework.http.actions; import jakarta.servlet.http.Cookie; - import org.citrusframework.actions.SendMessageAction; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; @@ -31,9 +30,12 @@ /** * @since 2.4 */ -public class HttpClientRequestActionBuilder extends SendMessageAction.SendMessageActionBuilder { +public class HttpClientRequestActionBuilder extends + SendMessageAction.SendMessageActionBuilder { - /** Http message to send or receive */ + /** + * Http message to send or receive + */ private final HttpMessage httpMessage; /** @@ -46,10 +48,12 @@ public HttpClientRequestActionBuilder() { /** * Subclasses may use custom message builder and Http message. + * * @param messageBuilder * @param httpMessage */ - protected HttpClientRequestActionBuilder(MessageBuilder messageBuilder, HttpMessage httpMessage) { + protected HttpClientRequestActionBuilder(MessageBuilder messageBuilder, + HttpMessage httpMessage) { this.httpMessage = httpMessage; message(messageBuilder); } @@ -57,13 +61,18 @@ protected HttpClientRequestActionBuilder(MessageBuilder messageBuilder, HttpMess @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { - messageBuilderSupport = new HttpMessageBuilderSupport(httpMessage, this); + messageBuilderSupport = createHttpMessageBuilderSupport(); } return super.getMessageBuilderSupport(); } + protected HttpMessageBuilderSupport createHttpMessageBuilderSupport() { + return new HttpMessageBuilderSupport(httpMessage, this); + } + /** * Sets the request path. + * * @param path * @return */ @@ -74,6 +83,7 @@ public HttpClientRequestActionBuilder path(String path) { /** * Sets the request method. + * * @param method * @return */ @@ -83,8 +93,8 @@ public HttpClientRequestActionBuilder method(HttpMethod method) { } /** - * Set the endpoint URI for the request. This works only if the HTTP endpoint used - * doesn't provide an own endpoint URI resolver. + * Set the endpoint URI for the request. This works only if the HTTP endpoint used doesn't + * provide an own endpoint URI resolver. * * @param uri absolute URI to use for the endpoint * @return self @@ -96,6 +106,7 @@ public HttpClientRequestActionBuilder uri(String uri) { /** * Adds a query param to the request uri. + * * @param name * @return */ @@ -106,6 +117,7 @@ public HttpClientRequestActionBuilder queryParam(String name) { /** * Adds a query param to the request uri. + * * @param name * @param value * @return @@ -115,11 +127,13 @@ public HttpClientRequestActionBuilder queryParam(String name, String value) { return this; } - public static class HttpMessageBuilderSupport extends SendMessageBuilderSupport { + public static class HttpMessageBuilderSupport extends + SendMessageBuilderSupport { private final HttpMessage httpMessage; - protected HttpMessageBuilderSupport(HttpMessage httpMessage, HttpClientRequestActionBuilder delegate) { + protected HttpMessageBuilderSupport(HttpMessage httpMessage, + HttpClientRequestActionBuilder delegate) { super(delegate); this.httpMessage = httpMessage; } @@ -131,12 +145,13 @@ public HttpMessageBuilderSupport body(String payload) { } /** - * Adds message payload multi value map data to this builder. This is used when using multipart file upload via - * Spring RestTemplate. + * Adds message payload multi value map data to this builder. This is used when using + * multipart file upload via Spring RestTemplate. + * * @param payload * @return */ - public HttpMessageBuilderSupport body(MultiValueMap payload) { + public HttpMessageBuilderSupport body(MultiValueMap payload) { httpMessage.setPayload(payload); return this; } @@ -155,6 +170,7 @@ public HttpMessageBuilderSupport from(Message controlMessage) { /** * Sets the request method. + * * @param method * @return */ @@ -164,8 +180,8 @@ public HttpMessageBuilderSupport method(HttpMethod method) { } /** - * Set the endpoint URI for the request. This works only if the HTTP endpoint used - * doesn't provide an own endpoint URI resolver. + * Set the endpoint URI for the request. This works only if the HTTP endpoint used doesn't + * provide an own endpoint URI resolver. * * @param uri absolute URI to use for the endpoint * @return self @@ -177,6 +193,7 @@ public HttpMessageBuilderSupport uri(String uri) { /** * Adds a query param to the request uri. + * * @param name * @return */ @@ -187,6 +204,7 @@ public HttpMessageBuilderSupport queryParam(String name) { /** * Adds a query param to the request uri. + * * @param name * @param value * @return @@ -198,6 +216,7 @@ public HttpMessageBuilderSupport queryParam(String name, String value) { /** * Sets the http version. + * * @param version * @return */ @@ -208,6 +227,7 @@ public HttpMessageBuilderSupport version(String version) { /** * Sets the request content type header. + * * @param contentType * @return */ @@ -218,6 +238,7 @@ public HttpMessageBuilderSupport contentType(String contentType) { /** * Sets the request accept header. + * * @param accept * @return */ @@ -228,6 +249,7 @@ public HttpMessageBuilderSupport accept(String accept) { /** * Adds cookie to response by "Cookie" header. + * * @param cookie * @return */ @@ -239,6 +261,14 @@ public HttpMessageBuilderSupport cookie(Cookie cookie) { @Override public SendMessageAction doBuild() { + return createSendMessageAction(); + } + + /** + * Creates the actual SendMessageAction. Subclasses may override this method to provide specific + * implementations. + */ + protected SendMessageAction createSendMessageAction() { return new SendMessageAction(this); } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientResponseActionBuilder.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientResponseActionBuilder.java index 292caf3016..07b3fd87b6 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientResponseActionBuilder.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpClientResponseActionBuilder.java @@ -17,6 +17,7 @@ package org.citrusframework.http.actions; import jakarta.servlet.http.Cookie; +import java.util.Optional; import org.citrusframework.actions.ReceiveMessageAction; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; @@ -26,8 +27,6 @@ import org.citrusframework.message.builder.ReceiveMessageBuilderSupport; import org.springframework.http.HttpStatusCode; -import java.util.Optional; - /** * @since 2.4 */ @@ -59,11 +58,15 @@ public HttpClientResponseActionBuilder(MessageBuilder messageBuilder, HttpMessag @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { - messageBuilderSupport = new HttpMessageBuilderSupport(httpMessage, this); + messageBuilderSupport = createHttpMessageBuilderSupport(); } return super.getMessageBuilderSupport(); } + protected HttpMessageBuilderSupport createHttpMessageBuilderSupport() { + return new HttpMessageBuilderSupport(httpMessage, this); + } + public static class HttpMessageBuilderSupport extends ReceiveMessageBuilderSupport { private final HttpMessage httpMessage; @@ -154,6 +157,15 @@ public HttpMessageBuilderSupport cookie(Cookie cookie) { @Override public ReceiveMessageAction doBuild() { + return createReceiveMessageAction(); + } + + + /** + * Creates the actual SendMessageAction. Subclasses may override this method to provide specific + * implementations. + */ + protected ReceiveMessageAction createReceiveMessageAction() { return new ReceiveMessageAction(this); } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerRequestActionBuilder.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerRequestActionBuilder.java index 39f22cc090..148af48b6a 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerRequestActionBuilder.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerRequestActionBuilder.java @@ -16,9 +16,8 @@ package org.citrusframework.http.actions; -import java.util.Optional; - import jakarta.servlet.http.Cookie; +import java.util.Optional; import org.citrusframework.actions.ReceiveMessageAction; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; @@ -61,11 +60,15 @@ public HttpServerRequestActionBuilder(MessageBuilder messageBuilder, HttpMessage @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { - messageBuilderSupport = new HttpMessageBuilderSupport(httpMessage, this); + messageBuilderSupport = createMessageBuilderSupport(); } return super.getMessageBuilderSupport(); } + protected HttpMessageBuilderSupport createMessageBuilderSupport() { + return new HttpMessageBuilderSupport(httpMessage, this); + } + /** * Sets the request path. * @param path diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerResponseActionBuilder.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerResponseActionBuilder.java index e0610ce5d0..8a0ed55d59 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerResponseActionBuilder.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/actions/HttpServerResponseActionBuilder.java @@ -57,11 +57,15 @@ public HttpServerResponseActionBuilder(MessageBuilder messageBuilder, HttpMessag @Override public HttpMessageBuilderSupport getMessageBuilderSupport() { if (messageBuilderSupport == null) { - messageBuilderSupport = new HttpMessageBuilderSupport(httpMessage, this); + messageBuilderSupport = createMessageBuilderSupport(); } return super.getMessageBuilderSupport(); } + protected HttpMessageBuilderSupport createMessageBuilderSupport() { + return new HttpMessageBuilderSupport(httpMessage, this); + } + public static class HttpMessageBuilderSupport extends SendMessageBuilderSupport { private final HttpMessage httpMessage; diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/HttpEndpointConfiguration.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/HttpEndpointConfiguration.java index 4970fe7936..9d93c11db2 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/HttpEndpointConfiguration.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/client/HttpEndpointConfiguration.java @@ -16,9 +16,6 @@ package org.citrusframework.http.client; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.citrusframework.endpoint.AbstractPollableEndpointConfiguration; import org.citrusframework.endpoint.resolver.DynamicEndpointUriResolver; @@ -41,6 +38,10 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestTemplate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + /** * @since 1.4 */ @@ -102,6 +103,10 @@ public class HttpEndpointConfiguration extends AbstractPollableEndpointConfigura */ private boolean disableRedirectHandling = false; + /** Should handle http semicolon uri content e.g. for matrix encoded path variables */ + // TODO: Christoph Deppisch advise whether this is a reasonable approach to support matrix encoded path parameters + private boolean handleSemicolonPathContent = false; + /** Default status code returned by http server */ private int defaultStatusCode = HttpStatus.OK.value(); @@ -422,6 +427,24 @@ public void setDisableRedirectHandling(boolean disableRedirectHandling) { this.disableRedirectHandling = disableRedirectHandling; } + /** + * Gets the handleSemicolonPathContent. + * + * @return + */ + public boolean isHandleSemicolonPathContent() { + return handleSemicolonPathContent; + } + + /** + * Sets the handleSemicolonPathContent. + * + * @param handleSemicolonPathContent + */ + public void setHandleSemicolonPathContent(boolean handleSemicolonPathContent) { + this.handleSemicolonPathContent = handleSemicolonPathContent; + } + /** * Gets the errorHandler. * diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/CookieUtils.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/CookieUtils.java new file mode 100644 index 0000000000..82a25f7acf --- /dev/null +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/CookieUtils.java @@ -0,0 +1,42 @@ +package org.citrusframework.http.config.xml; + +import jakarta.servlet.http.Cookie; +import org.citrusframework.http.message.HttpMessage; +import org.w3c.dom.Element; + +import java.util.List; + +import static java.lang.Boolean.parseBoolean; +import static java.lang.Integer.parseInt; + +final class CookieUtils { + + private CookieUtils() { + // Static utility class + } + + static void setCookieElement(HttpMessage httpMessage, List cookieElements) { + for (Object item : cookieElements) { + Element cookieElement = (Element) item; + Cookie cookie = new Cookie(cookieElement.getAttribute("name"), cookieElement.getAttribute("value")); + + if (cookieElement.hasAttribute("path")) { + cookie.setPath(cookieElement.getAttribute("path")); + } + + if (cookieElement.hasAttribute("domain")) { + cookie.setDomain(cookieElement.getAttribute("domain")); + } + + if (cookieElement.hasAttribute("max-age")) { + cookie.setMaxAge(parseInt(cookieElement.getAttribute("max-age"))); + } + + if (cookieElement.hasAttribute("secure")) { + cookie.setSecure(parseBoolean(cookieElement.getAttribute("secure"))); + } + + httpMessage.cookie(cookie); + } + } +} diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpReceiveResponseActionParser.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpReceiveResponseActionParser.java index 3d8ce1b216..a5ed612497 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpReceiveResponseActionParser.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpReceiveResponseActionParser.java @@ -16,7 +16,6 @@ package org.citrusframework.http.config.xml; -import jakarta.servlet.http.Cookie; import org.citrusframework.config.util.BeanDefinitionParserUtils; import org.citrusframework.config.xml.DescriptionElementParser; import org.citrusframework.config.xml.ReceiveMessageActionParser; @@ -36,8 +35,8 @@ import java.util.List; import static java.lang.Boolean.parseBoolean; -import static java.lang.Integer.parseInt; import static org.citrusframework.config.xml.MessageSelectorParser.doParse; +import static org.citrusframework.http.config.xml.CookieUtils.setCookieElement; import static org.springframework.util.xml.DomUtils.getChildElementByTagName; import static org.springframework.util.xml.DomUtils.getChildElementsByTagName; @@ -48,6 +47,13 @@ public class HttpReceiveResponseActionParser extends ReceiveMessageActionParser @Override public BeanDefinition parse(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = createBeanDefinitionBuilder( + element, parserContext); + return builder.getBeanDefinition(); + } + + protected BeanDefinitionBuilder createBeanDefinitionBuilder(Element element, + ParserContext parserContext) { BeanDefinitionBuilder builder = parseComponent(element, parserContext); builder.addPropertyValue("name", "http:" + element.getLocalName()); @@ -59,9 +65,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { builder.addPropertyValue("receiveTimeout", Long.valueOf(receiveTimeout)); } - if (!element.hasAttribute("uri") && !element.hasAttribute("client")) { - throw new BeanCreationException("Neither http request uri nor http client endpoint reference is given - invalid test action definition"); - } + validateEndpointConfiguration(element); if (element.hasAttribute("client")) { builder.addPropertyReference("endpoint", element.getAttribute("client")); @@ -98,28 +102,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { } List cookieElements = getChildElementsByTagName(headers, "cookie"); - for (Object item : cookieElements) { - Element cookieElement = (Element) item; - Cookie cookie = new Cookie(cookieElement.getAttribute("name"), cookieElement.getAttribute("value")); - - if (cookieElement.hasAttribute("path")) { - cookie.setPath(cookieElement.getAttribute("path")); - } - - if (cookieElement.hasAttribute("domain")) { - cookie.setDomain(cookieElement.getAttribute("domain")); - } - - if (cookieElement.hasAttribute("max-age")) { - cookie.setMaxAge(parseInt(cookieElement.getAttribute("max-age"))); - } - - if (cookieElement.hasAttribute("secure")) { - cookie.setSecure(parseBoolean(cookieElement.getAttribute("secure"))); - } - - httpMessage.cookie(cookie); - } + setCookieElement(httpMessage, cookieElements); boolean ignoreCase = !headers.hasAttribute("ignore-case") || parseBoolean(headers.getAttribute("ignore-case")); validationContexts.stream() @@ -130,7 +113,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { doParse(element, builder); - HttpMessageBuilder httpMessageBuilder = new HttpMessageBuilder(httpMessage); + HttpMessageBuilder httpMessageBuilder = createMessageBuilder(httpMessage); DefaultMessageBuilder messageContentBuilder = constructMessageBuilder(body, builder); httpMessageBuilder.setName(messageContentBuilder.getName()); @@ -141,6 +124,28 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { builder.addPropertyValue("validationContexts", validationContexts); builder.addPropertyValue("variableExtractors", getVariableExtractors(element)); - return builder.getBeanDefinition(); + return builder; + } + + protected HttpMessageBuilder createMessageBuilder(HttpMessage httpMessage) { + HttpMessageBuilder httpMessageBuilder = new HttpMessageBuilder(httpMessage); + return httpMessageBuilder; + } + + /** + * Validates the endpoint configuration for the given XML element. + *

      + * This method is designed to be overridden by subclasses if custom validation logic is required. + * By default, it checks whether the 'uri' or 'client' attributes are present in the element. + * If neither is found, it throws a {@link BeanCreationException} indicating an invalid test action definition. + *

      + * + * @param element the XML element representing the endpoint configuration to validate + * @throws BeanCreationException if neither 'uri' nor 'client' attributes are present + */ + protected void validateEndpointConfiguration(Element element) { + if (!element.hasAttribute("uri") && !element.hasAttribute("client")) { + throw new BeanCreationException("Neither http request uri nor http client endpoint reference is given - invalid test action definition"); + } } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpSendRequestActionParser.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpSendRequestActionParser.java index 028d3c17f9..4b886b437f 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpSendRequestActionParser.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpSendRequestActionParser.java @@ -16,9 +16,6 @@ package org.citrusframework.http.config.xml; -import java.util.ArrayList; -import java.util.List; - import jakarta.servlet.http.Cookie; import org.citrusframework.config.util.BeanDefinitionParserUtils; import org.citrusframework.config.xml.DescriptionElementParser; @@ -37,6 +34,9 @@ import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; +import java.util.ArrayList; +import java.util.List; + /** * @since 2.4 */ @@ -44,6 +44,11 @@ public class HttpSendRequestActionParser extends SendMessageActionParser { @Override public BeanDefinition parse(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = createBeanDefinitionBuilder(element, parserContext); + return builder.getBeanDefinition(); + } + + protected BeanDefinitionBuilder createBeanDefinitionBuilder(Element element, ParserContext parserContext) { BeanDefinitionBuilder builder = parseComponent(element, parserContext); builder.addPropertyValue("name", "http:" + element.getLocalName()); @@ -53,9 +58,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { HttpMessage httpMessage = new HttpMessage(); - if (!element.hasAttribute("uri") && !element.hasAttribute("client")) { - throw new BeanCreationException("Neither http request uri nor http client endpoint reference is given - invalid test action definition"); - } + validateEndpointConfiguration(element); if (element.hasAttribute("client")) { builder.addPropertyReference("endpoint", element.getAttribute("client")); @@ -69,7 +72,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { } } - Element requestElement = DomUtils.getChildElements(element).get(0); + Element requestElement = getRequestElement(element); httpMessage.method(HttpMethod.valueOf(requestElement.getLocalName().toUpperCase())); if (requestElement.hasAttribute("path")) { httpMessage.path(requestElement.getAttribute("path")); @@ -81,7 +84,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { httpMessage.queryParam(param.getAttribute("name"), param.getAttribute("value")); } - Element headers = DomUtils.getChildElementByTagName(requestElement, "headers"); + Element headers = getHeadersElement(requestElement); if (headers != null) { List headerElements = DomUtils.getChildElementsByTagName(headers, "header"); for (Object headerElement : headerElements) { @@ -141,7 +144,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { } } - HttpMessageBuilder httpMessageBuilder = new HttpMessageBuilder(httpMessage); + HttpMessageBuilder httpMessageBuilder = createMessageBuilder(httpMessage); DefaultMessageBuilder messageContentBuilder = constructMessageBuilder(body, builder); httpMessageBuilder.setName(messageContentBuilder.getName()); @@ -157,6 +160,42 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { builder.addPropertyValue("variableExtractors", variableExtractors); } - return builder.getBeanDefinition(); + return builder; + } + + protected Element getRequestElement(Element element) { + if (element.hasChildNodes()) { + return DomUtils.getChildElements(element).get(0); + } + + throw new BeanCreationException("No request element specified for http send - invalid test action definition"); + } + + protected Element getHeadersElement(Element requestElement) { + return DomUtils.getChildElementByTagName(requestElement, "headers"); + } + + /** + * This method is designed to be overridden by subclasses if a custom message builder is required. + */ + protected HttpMessageBuilder createMessageBuilder(HttpMessage httpMessage) { + return new HttpMessageBuilder(httpMessage); + } + + /** + * Validates the endpoint configuration for the given XML element. + *

      + * This method is designed to be overridden by subclasses if custom validation logic is required. + * By default, it checks whether the 'uri' or 'client' attributes are present in the element. + * If neither is found, it throws a {@link BeanCreationException} indicating an invalid test action definition. + *

      + * + * @param element the XML element representing the endpoint configuration to validate + * @throws BeanCreationException if neither 'uri' nor 'client' attributes are present + */ + protected void validateEndpointConfiguration(Element element) { + if (!element.hasAttribute("uri") && !element.hasAttribute("client")) { + throw new BeanCreationException("Neither http request uri nor http client endpoint reference is given - invalid test action definition"); + } } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpSendResponseActionParser.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpSendResponseActionParser.java index 809024d9a8..0a4a96df3b 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpSendResponseActionParser.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/config/xml/HttpSendResponseActionParser.java @@ -16,7 +16,6 @@ package org.citrusframework.http.config.xml; -import jakarta.servlet.http.Cookie; import org.citrusframework.config.xml.SendMessageActionParser; import org.citrusframework.http.message.HttpMessage; import org.citrusframework.http.message.HttpMessageBuilder; @@ -30,10 +29,9 @@ import java.util.List; -import static java.lang.Boolean.parseBoolean; -import static java.lang.Integer.parseInt; import static org.citrusframework.config.util.BeanDefinitionParserUtils.setPropertyReference; import static org.citrusframework.config.xml.DescriptionElementParser.doParse; +import static org.citrusframework.http.config.xml.CookieUtils.setCookieElement; import static org.citrusframework.util.StringUtils.hasText; /** @@ -78,28 +76,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { } List cookieElements = DomUtils.getChildElementsByTagName(headers, "cookie"); - for (Object item : cookieElements) { - Element cookieElement = (Element) item; - Cookie cookie = new Cookie(cookieElement.getAttribute("name"), cookieElement.getAttribute("value")); - - if (cookieElement.hasAttribute("path")) { - cookie.setPath(cookieElement.getAttribute("path")); - } - - if (cookieElement.hasAttribute("domain")) { - cookie.setDomain(cookieElement.getAttribute("domain")); - } - - if (cookieElement.hasAttribute("max-age")) { - cookie.setMaxAge(parseInt(cookieElement.getAttribute("max-age"))); - } - - if (cookieElement.hasAttribute("secure")) { - cookie.setSecure(parseBoolean(cookieElement.getAttribute("secure"))); - } - - httpMessage.cookie(cookie); - } + setCookieElement(httpMessage, cookieElements); } Element body = DomUtils.getChildElementByTagName(element, "body"); diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/controller/HttpMessageController.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/controller/HttpMessageController.java index 9e3bb54f1d..aa62ab4f47 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/controller/HttpMessageController.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/controller/HttpMessageController.java @@ -130,6 +130,7 @@ private ResponseEntity handleRequestInternal(HttpMethod method, HttpEntity HttpServletRequest servletRequest = ((ServletRequestAttributes) attributes).getRequest(); UrlPathHelper pathHelper = new UrlPathHelper(); + pathHelper.setRemoveSemicolonContent(!endpointConfiguration.isHandleSemicolonPathContent()); Enumeration allHeaders = servletRequest.getHeaderNames(); for (String headerName : CollectionUtils.toArray(allHeaders, new String[] {})) { diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/interceptor/LoggingClientInterceptor.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/interceptor/LoggingClientInterceptor.java index 6d6f26554a..150530fb05 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/interceptor/LoggingClientInterceptor.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/interceptor/LoggingClientInterceptor.java @@ -82,9 +82,7 @@ public void handleRequest(String request) { logger.debug("Sending Http request message"); messageListener.onOutboundMessage(new RawMessage(request), contextFactory.getObject()); } else { - if (logger.isDebugEnabled()) { - logger.debug("Sending Http request message:" + NEWLINE + request); - } + logger.debug("Sending Http request message:{}{}", NEWLINE, request); } } @@ -98,9 +96,7 @@ public void handleResponse(String response) { logger.debug("Received Http response message"); messageListener.onInboundMessage(new RawMessage(response), contextFactory.getObject()); } else { - if (logger.isDebugEnabled()) { - logger.debug("Received Http response message:" + NEWLINE + response); - } + logger.debug("Received Http response message:{}{}", NEWLINE, response); } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/interceptor/LoggingHandlerInterceptor.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/interceptor/LoggingHandlerInterceptor.java index d8288b6c39..f333335497 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/interceptor/LoggingHandlerInterceptor.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/interceptor/LoggingHandlerInterceptor.java @@ -18,9 +18,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Enumeration; - import org.citrusframework.context.TestContextFactory; import org.citrusframework.http.controller.HttpMessageController; import org.citrusframework.message.RawMessage; @@ -33,6 +30,9 @@ import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; +import java.io.IOException; +import java.util.Enumeration; + import static java.lang.System.lineSeparator; /** @@ -80,9 +80,7 @@ public void handleRequest(String request) { logger.debug("Received Http request"); messageListener.onInboundMessage(new RawMessage(request), contextFactory.getObject()); } else { - if (logger.isDebugEnabled()) { - logger.debug("Received Http request:" + NEWLINE + request); - } + logger.debug("Received Http request:{}{}", NEWLINE, request); } } @@ -95,9 +93,7 @@ public void handleResponse(String response) { logger.debug("Sending Http response"); messageListener.onOutboundMessage(new RawMessage(response), contextFactory.getObject()); } else { - if (logger.isDebugEnabled()) { - logger.debug("Sending Http response:" + NEWLINE + response); - } + logger.debug("Sending Http response:{}{}", NEWLINE, response); } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpMessage.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpMessage.java index 345d9ee2d8..2c9fc6d1f3 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpMessage.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpMessage.java @@ -84,6 +84,10 @@ public HttpMessage(final Message message) { public HttpMessage(final Message message, boolean forceCitrusHeaderUpdate) { super(message, forceCitrusHeaderUpdate); copyCookies(message); + + if (message instanceof HttpMessage httpMessage) { + queryParams.putAll(httpMessage.queryParams); + } } /** @@ -368,7 +372,11 @@ public String getContentType() { * @return The accept header value */ public String getAccept() { - final Object accept = getHeader("Accept"); + Object accept = getHeader("Accept"); + + if (accept == null) { + accept = getHeader("accept"); + } if (accept != null) { return accept.toString(); diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpMessageUtils.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpMessageUtils.java index 47b42c3195..de22dc95a1 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpMessageUtils.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpMessageUtils.java @@ -16,9 +16,21 @@ package org.citrusframework.http.message; +import org.apache.commons.lang3.tuple.Pair; import org.citrusframework.message.Message; import org.citrusframework.message.MessageHeaders; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.emptyMap; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toList; +import static org.citrusframework.http.message.HttpMessageHeaders.HTTP_QUERY_PARAMS; +import static org.citrusframework.util.StringUtils.hasText; + /** * @since 2.7.5 */ @@ -38,8 +50,8 @@ private HttpMessageUtils() { */ public static void copy(Message from, HttpMessage to) { HttpMessage source; - if (from instanceof HttpMessage) { - source = (HttpMessage) from; + if (from instanceof HttpMessage httpMessage) { + source = httpMessage; } else { source = new HttpMessage(from); } @@ -65,4 +77,30 @@ public static void copy(HttpMessage from, HttpMessage to) { from.getHeaderData().forEach(to::addHeaderData); from.getCookies().forEach(to::cookie); } + + /** + * Extracts query parameters from the citrus HTTP message header and returns them as a map. + * + * @param httpMessage the HTTP message containing the query parameters in the header + * @return a map of query parameter names and their corresponding values + * @throws IllegalArgumentException if the query parameters are not formatted correctly + */ + public static Map> getQueryParameterMap(HttpMessage httpMessage) { + String queryParams = (String) httpMessage.getHeader(HTTP_QUERY_PARAMS); + if (hasText(queryParams)) { + return Arrays.stream(queryParams.split(",")) + .map(queryParameterKeyValue -> { + String[] keyAndValue = queryParameterKeyValue.split("=", 2); + if (keyAndValue.length == 0) { + throw new IllegalArgumentException("Query parameter must have a key."); + } + String key = keyAndValue[0]; + String value = keyAndValue.length > 1 ? keyAndValue[1] : ""; + return Pair.of(key, value); + }) + .collect(groupingBy(Pair::getLeft,mapping(Pair::getRight,toList()))); + } + + return emptyMap(); + } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpQueryParamHeaderValidator.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpQueryParamHeaderValidator.java index 2988291f2c..2d32f076e9 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpQueryParamHeaderValidator.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/message/HttpQueryParamHeaderValidator.java @@ -16,18 +16,22 @@ package org.citrusframework.http.message; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.ValidationException; -import org.citrusframework.util.StringUtils; import org.citrusframework.validation.DefaultHeaderValidator; import org.citrusframework.validation.context.HeaderValidationContext; import org.citrusframework.validation.matcher.ValidationMatcherUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toMap; +import static org.citrusframework.http.message.HttpMessageHeaders.HTTP_QUERY_PARAMS; +import static org.citrusframework.util.StringUtils.hasText; + /** * @since 2.7.6 */ @@ -38,49 +42,62 @@ public void validateHeader(String name, Object received, Object control, TestCon if (ValidationMatcherUtils.isValidationMatcherExpression(Optional.ofNullable(control) .map(Object::toString) .orElse(""))) { - super.validateHeader(HttpMessageHeaders.HTTP_QUERY_PARAMS, received, control, context, validationContext); + super.validateHeader(HTTP_QUERY_PARAMS, received, control, context, validationContext); return; } - Map receiveParams = convertToMap(received); - Map controlParams = convertToMap(control); + // TODO Christoph Deppisch: I changed this to support multi lists, e.q. required for array query parameters. + // Not sure about consequences though. Therefore, i call a new method super.validateHeaderArray below. + // Maybe we should fix this in general. + Map receiveParams = convertToMap(received); + Map controlParams = convertToMap(control); - for (Map.Entry param : controlParams.entrySet()) { + for (Map.Entry param : controlParams.entrySet()) { if (!receiveParams.containsKey(param.getKey())) { throw new ValidationException("Validation failed: Query param '" + param.getKey() + "' is missing"); } - super.validateHeader(HttpMessageHeaders.HTTP_QUERY_PARAMS + "(" + param.getKey() + ")", receiveParams.get(param.getKey()), param.getValue(), context, validationContext); + super.validateHeaderArray(HTTP_QUERY_PARAMS + "(" + param.getKey() + ")", receiveParams.get(param.getKey()), param.getValue(), context, validationContext); } } /** - * Convert query string key-value expression to map. - * @param expression - * @return + * Convert query string key-value expression to map. Note, that there could be hamcrest matchers + * encoded in the expression. */ - private Map convertToMap(Object expression) { - if (expression instanceof Map) { - return (Map) expression; + private Map convertToMap(Object expression) { + if (expression instanceof Map) { + return (Map) expression; } return Stream.of(Optional.ofNullable(expression) .map(Object::toString) .orElse("") .split(",")) - .map(keyValue -> keyValue.split("=")) - .filter(keyValue -> StringUtils.hasText(keyValue[0])) - .map(keyValue -> { - if (keyValue.length < 2) { - return new String[]{keyValue[0], ""}; + .map(keyValue -> keyValue.split("=")) + .filter(keyValue -> hasText(keyValue[0])) + .collect(toMap( + keyValue -> keyValue[0], // Key function + keyValue -> + // Value function: if no value is present, use an empty string + (keyValue.length < 2 ? "" : keyValue[1]) + , + (existingValue, newValue) -> { // Merge function to handle duplicate keys + if (existingValue instanceof List) { + ((List) existingValue).add(newValue.toString()); + return existingValue; + } else { + List list = new ArrayList<>(); + list.add((String) existingValue); + list.add(newValue.toString()); + return list; } - return keyValue; - }) - .collect(Collectors.toMap(keyValue -> keyValue[0], keyValue -> keyValue[1])); + } + )); } @Override public boolean supports(String headerName, Class type) { - return headerName.equals(HttpMessageHeaders.HTTP_QUERY_PARAMS); + return headerName.equals(HTTP_QUERY_PARAMS); } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/AbstractHttpServerBuilder.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/AbstractHttpServerBuilder.java index 2333db5517..68fbc42d68 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/AbstractHttpServerBuilder.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/AbstractHttpServerBuilder.java @@ -250,6 +250,17 @@ public B handleCookies(boolean flag) { return self; } + /** + * Sets the semicolon handling property. + * + * @param flag + * @return + */ + public B handleHandleSemicolonPathContent(boolean flag) { + endpoint.setHandleSemicolonPathContent(flag); + return self; + } + /** * Sets the default status code property. * diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/HttpServer.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/HttpServer.java index dc71774c66..dd69a926bd 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/HttpServer.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/server/HttpServer.java @@ -149,6 +149,11 @@ public class HttpServer extends AbstractServer { */ private boolean handleCookies = false; + /** + * Should handle matrix encoded path parameters + */ + private boolean handleSemicolonPathContent = false; + /** * Default status code returned by http server */ @@ -645,6 +650,24 @@ public void setHandleCookies(boolean handleCookies) { this.handleCookies = handleCookies; } + /** + * Gets the handleSemicolonPathContent. + * + * @return + */ + public boolean isHandleSemicolonPathContent() { + return handleSemicolonPathContent; + } + + /** + * Sets the handleSemicolonPathContent. + * + * @param handleSemicolonPathContent + */ + public void setHandleSemicolonPathContent(boolean handleSemicolonPathContent) { + this.handleSemicolonPathContent = handleSemicolonPathContent; + } + /** * Gets the response cache size. * diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/servlet/CachingHttpServletRequestWrapper.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/servlet/CachingHttpServletRequestWrapper.java index 56ea78eaf8..805c329540 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/servlet/CachingHttpServletRequestWrapper.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/servlet/CachingHttpServletRequestWrapper.java @@ -16,16 +16,6 @@ package org.citrusframework.http.servlet; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.StringTokenizer; - import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; @@ -40,6 +30,15 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMethod; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.StringTokenizer; + /** * Caching wrapper saves request body data to cache when read. */ @@ -129,15 +128,8 @@ private void fillParams(final Map params, final String querySt paramValue = ""; } - try { - params.put(URLDecoder.decode(paramName, charset.name()), - new String[] { URLDecoder.decode(paramValue, charset.name()) }); - } catch (final UnsupportedEncodingException e) { - throw new CitrusRuntimeException(String.format( - "Failed to decode query param value '%s=%s'", - paramName, - paramValue), e); - } + params.put(URLDecoder.decode(paramName, charset), + new String[] { URLDecoder.decode(paramValue, charset) }); } } } diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/servlet/CitrusDispatcherServlet.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/servlet/CitrusDispatcherServlet.java index 2631ca1db3..e2dd9c6b8e 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/servlet/CitrusDispatcherServlet.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/servlet/CitrusDispatcherServlet.java @@ -111,6 +111,7 @@ protected void configureMessageController(ApplicationContext context) { endpointConfiguration.setHeaderMapper(DefaultHttpHeaderMapper.inboundMapper()); endpointConfiguration.setHandleAttributeHeaders(httpServer.isHandleAttributeHeaders()); endpointConfiguration.setHandleCookies(httpServer.isHandleCookies()); + endpointConfiguration.setHandleSemicolonPathContent(httpServer.isHandleSemicolonPathContent()); endpointConfiguration.setDefaultStatusCode(httpServer.getDefaultStatusCode()); messageController.setEndpointConfiguration(endpointConfiguration); diff --git a/endpoints/citrus-http/src/main/java/org/citrusframework/http/validation/FormUrlEncodedMessageValidator.java b/endpoints/citrus-http/src/main/java/org/citrusframework/http/validation/FormUrlEncodedMessageValidator.java index 1619c97b3b..ec59aed613 100644 --- a/endpoints/citrus-http/src/main/java/org/citrusframework/http/validation/FormUrlEncodedMessageValidator.java +++ b/endpoints/citrus-http/src/main/java/org/citrusframework/http/validation/FormUrlEncodedMessageValidator.java @@ -16,16 +16,6 @@ package org.citrusframework.http.validation; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.StringTokenizer; -import java.util.stream.Collectors; - import org.citrusframework.CitrusSettings; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -46,6 +36,16 @@ import org.slf4j.LoggerFactory; import org.springframework.util.MultiValueMap; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.StringTokenizer; +import java.util.stream.Collectors; + /** * Validates x-www-form-urlencoded HTML form data content by marshalling form fields to Xml representation. * @@ -101,7 +101,7 @@ private List prepareValidationContexts(List enriched = new ArrayList<>(validationContexts); enriched.add(new XmlMessageValidationContext()); return enriched; @@ -124,12 +124,12 @@ private MessageValidator getXmlMessageValidator(Tes // try to find xml message validator in registry Optional> defaultMessageValidator = context.getMessageValidatorRegistry().findMessageValidator(DEFAULT_XML_MESSAGE_VALIDATOR); - if (!defaultMessageValidator.isPresent() + if (defaultMessageValidator.isEmpty() && context.getReferenceResolver().isResolvable(DEFAULT_XML_MESSAGE_VALIDATOR)) { defaultMessageValidator = Optional.of(context.getReferenceResolver().resolve(DEFAULT_XML_MESSAGE_VALIDATOR, MessageValidator.class)); } - if (!defaultMessageValidator.isPresent()) { + if (defaultMessageValidator.isEmpty()) { // try to find xml message validator via resource path lookup defaultMessageValidator = MessageValidator.lookup("xml"); } diff --git a/endpoints/citrus-http/src/main/resources/org/citrusframework/schema/citrus-http-testcase.xsd b/endpoints/citrus-http/src/main/resources/org/citrusframework/schema/citrus-http-testcase.xsd index c42b48b394..68825b37b1 100644 --- a/endpoints/citrus-http/src/main/resources/org/citrusframework/schema/citrus-http-testcase.xsd +++ b/endpoints/citrus-http/src/main/resources/org/citrusframework/schema/citrus-http-testcase.xsd @@ -22,179 +22,116 @@ - + Sends Http request as client to server. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Received Http response as client from server. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -206,131 +143,198 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + Receives Http request as server. - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sends Http response as server to calling client. + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + - + + - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - Sends Http response as server to calling client. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/endpoints/citrus-http/src/test/java/org/citrusframework/http/integration/HttpMessageControllerIT.java b/endpoints/citrus-http/src/test/java/org/citrusframework/http/integration/HttpMessageControllerIT.java index c08f68796e..e03888e2c5 100644 --- a/endpoints/citrus-http/src/test/java/org/citrusframework/http/integration/HttpMessageControllerIT.java +++ b/endpoints/citrus-http/src/test/java/org/citrusframework/http/integration/HttpMessageControllerIT.java @@ -22,6 +22,7 @@ import org.testng.annotations.Test; public class HttpMessageControllerIT extends TestNGCitrusSpringSupport { + @Test @CitrusTestSource(type = TestLoader.SPRING) public void HttpMessageControllerIT() {} diff --git a/endpoints/citrus-http/src/test/java/org/citrusframework/http/integration/HttpServerStandaloneJavaIT.java b/endpoints/citrus-http/src/test/java/org/citrusframework/http/integration/HttpServerStandaloneJavaIT.java index 41e79ac3ce..bc3e734305 100644 --- a/endpoints/citrus-http/src/test/java/org/citrusframework/http/integration/HttpServerStandaloneJavaIT.java +++ b/endpoints/citrus-http/src/test/java/org/citrusframework/http/integration/HttpServerStandaloneJavaIT.java @@ -27,6 +27,7 @@ import static org.citrusframework.actions.EchoAction.Builder.echo; import static org.citrusframework.container.Assert.Builder.assertException; import static org.citrusframework.http.actions.HttpActionBuilder.http; +import static org.hamcrest.Matchers.oneOf; @Test public class HttpServerStandaloneJavaIT extends TestNGCitrusSpringSupport { @@ -102,9 +103,10 @@ public void httpServerStandalone() { .receive() .response() .message() - .header(HttpMessageHeaders.HTTP_STATUS_CODE, Matchers.isOneOf(HttpStatus.CREATED.value(), - HttpStatus.ACCEPTED.value(), - HttpStatus.OK.value()))); + .header(HttpMessageHeaders.HTTP_STATUS_CODE, Matchers.is( + oneOf(HttpStatus.CREATED.value(), + HttpStatus.ACCEPTED.value(), + HttpStatus.OK.value())))); run(echo("Test header validation error")); diff --git a/endpoints/citrus-http/src/test/java/org/citrusframework/http/message/HttpMessageUtilsTest.java b/endpoints/citrus-http/src/test/java/org/citrusframework/http/message/HttpMessageUtilsTest.java index 92edb71fe9..80eb101492 100644 --- a/endpoints/citrus-http/src/test/java/org/citrusframework/http/message/HttpMessageUtilsTest.java +++ b/endpoints/citrus-http/src/test/java/org/citrusframework/http/message/HttpMessageUtilsTest.java @@ -17,18 +17,28 @@ package org.citrusframework.http.message; import jakarta.servlet.http.Cookie; -import java.util.Collections; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; -import org.citrusframework.message.MessageHeaders; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.citrusframework.http.message.HttpMessageHeaders.HTTP_COOKIE_PREFIX; +import static org.citrusframework.http.message.HttpMessageUtils.getQueryParameterMap; +import static org.citrusframework.message.MessageHeaders.ID; +import static org.citrusframework.message.MessageHeaders.MESSAGE_TYPE; +import static org.citrusframework.message.MessageHeaders.TIMESTAMP; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + /** * @since 2.7 */ @@ -46,16 +56,16 @@ public void testCopy() { HttpMessageUtils.copy(from, to); - Assert.assertNotEquals(from.getId(), to.getId()); - Assert.assertEquals(to.getName(), "FooMessage"); - Assert.assertEquals(to.getPayload(String.class), "fooMessage"); - Assert.assertEquals(to.getHeaders().size(), 4L); - Assert.assertNotNull(to.getHeader(MessageHeaders.ID)); - Assert.assertNotNull(to.getHeader(MessageHeaders.MESSAGE_TYPE)); - Assert.assertNotNull(to.getHeader(MessageHeaders.TIMESTAMP)); - Assert.assertEquals(to.getHeader("X-Foo"), "foo"); - Assert.assertEquals(to.getHeaderData().size(), 1L); - Assert.assertEquals(to.getHeaderData().get(0), "HeaderData"); + assertNotEquals(from.getId(), to.getId()); + assertEquals(to.getName(), "FooMessage"); + assertEquals(to.getPayload(String.class), "fooMessage"); + assertEquals(to.getHeaders().size(), 4L); + assertNotNull(to.getHeader(ID)); + assertNotNull(to.getHeader(MESSAGE_TYPE)); + assertNotNull(to.getHeader(TIMESTAMP)); + assertEquals(to.getHeader("X-Foo"), "foo"); + assertEquals(to.getHeaderData().size(), 1L); + assertEquals(to.getHeaderData().get(0), "HeaderData"); } @Test @@ -76,20 +86,20 @@ public void testCopyPreventExistingOverwritePayload() { HttpMessageUtils.copy(from, to); - Assert.assertNotEquals(from.getId(), to.getId()); - Assert.assertEquals(to.getName(), "FooMessage"); - Assert.assertEquals(to.getPayload(String.class), "fooMessage"); - Assert.assertEquals(to.getHeaders().size(), 7L); - Assert.assertNotNull(to.getHeader(MessageHeaders.ID)); - Assert.assertNotNull(to.getHeader(MessageHeaders.MESSAGE_TYPE)); - Assert.assertNotNull(to.getHeader(MessageHeaders.TIMESTAMP)); - Assert.assertEquals(to.getHeader("X-Foo"), "foo"); - Assert.assertEquals(to.getHeader("X-Existing"), "existing"); - Assert.assertEquals(to.getHeader(HttpMessageHeaders.HTTP_COOKIE_PREFIX + "Foo"), "Foo=fooCookie"); - Assert.assertEquals(to.getHeader(HttpMessageHeaders.HTTP_COOKIE_PREFIX + "Existing"), "Existing=existingCookie"); - Assert.assertEquals(to.getHeaderData().size(), 2L); - Assert.assertEquals(to.getHeaderData().get(0), "ExistingHeaderData"); - Assert.assertEquals(to.getHeaderData().get(1), "HeaderData"); + assertNotEquals(from.getId(), to.getId()); + assertEquals(to.getName(), "FooMessage"); + assertEquals(to.getPayload(String.class), "fooMessage"); + assertEquals(to.getHeaders().size(), 7L); + assertNotNull(to.getHeader(ID)); + assertNotNull(to.getHeader(MESSAGE_TYPE)); + assertNotNull(to.getHeader(TIMESTAMP)); + assertEquals(to.getHeader("X-Foo"), "foo"); + assertEquals(to.getHeader("X-Existing"), "existing"); + assertEquals(to.getHeader(HTTP_COOKIE_PREFIX + "Foo"), "Foo=fooCookie"); + assertEquals(to.getHeader(HTTP_COOKIE_PREFIX + "Existing"), "Existing=existingCookie"); + assertEquals(to.getHeaderData().size(), 2L); + assertEquals(to.getHeaderData().get(0), "ExistingHeaderData"); + assertEquals(to.getHeaderData().get(1), "HeaderData"); } @Test @@ -104,19 +114,19 @@ public void testConvertAndCopy() { HttpMessageUtils.copy(from, to); - Assert.assertNotEquals(from.getId(), to.getId()); - Assert.assertEquals(to.getName(), "FooMessage"); - Assert.assertEquals(to.getPayload(String.class), "fooMessage"); - Assert.assertEquals(to.getHeader("X-Foo"), "foo"); - Assert.assertEquals(to.getHeaderData().size(), 1L); - Assert.assertEquals(to.getHeaderData().get(0), "HeaderData"); + assertNotEquals(from.getId(), to.getId()); + assertEquals(to.getName(), "FooMessage"); + assertEquals(to.getPayload(String.class), "fooMessage"); + assertEquals(to.getHeader("X-Foo"), "foo"); + assertEquals(to.getHeaderData().size(), 1L); + assertEquals(to.getHeaderData().get(0), "HeaderData"); } @Test(dataProvider = "queryParamStrings") public void testQueryParamsExtraction(String queryParamString, Map params) { HttpMessage message = new HttpMessage(); message.queryParams(queryParamString); - Assert.assertEquals(message.getQueryParams().size(), params.size()); + assertEquals(message.getQueryParams().size(), params.size()); params.forEach((key, value) -> Assert.assertTrue(message.getQueryParams().get(key).contains(value))); } @@ -136,4 +146,53 @@ public Object[][] queryParamStrings() { .collect(Collectors.toMap(keyValue -> keyValue[0], keyValue -> keyValue[1])) } }; } + + @Test + public void testGetQueryParameterMapWithValues() { + HttpMessage httpMessage = new HttpMessage(); + httpMessage.queryParam("q1", "v1"); + httpMessage.queryParam("q1", "v2"); + httpMessage.queryParam("q2", "v3"); + httpMessage.queryParam("q2", "v4"); + httpMessage.queryParam("q3", "v5"); + + Map> queryParams = getQueryParameterMap(httpMessage); + + assertEquals(queryParams.size(), 3); + List q1Values = queryParams.get("q1"); + assertTrue(q1Values.contains("v1")); + assertTrue(q1Values.contains("v2")); + List q2Values = queryParams.get("q2"); + assertTrue(q2Values.contains("v3")); + assertTrue(q2Values.contains("v4")); + List q3Values = queryParams.get("q3"); + assertTrue(q3Values.contains("v5")); + } + + @Test + public void testGetQueryParameterMapWithNoValues() { + HttpMessage httpMessage = new HttpMessage(); + + Map> queryParams = getQueryParameterMap(httpMessage); + + assertTrue(queryParams.isEmpty()); + } + + @Test + public void testGetQueryParameterMapWithMissingValues() { + HttpMessage httpMessage = new HttpMessage(); + httpMessage.queryParam("q1", ""); + httpMessage.queryParam("q2", ""); + httpMessage.queryParam("q3", ""); + + Map> queryParams = getQueryParameterMap(httpMessage); + + assertEquals(queryParams.size(), 3); + List q1Values = queryParams.get("q1"); + assertTrue(q1Values.contains("")); + List q2Values = queryParams.get("q2"); + assertTrue(q2Values.contains("")); + List q3Values = queryParams.get("q3"); + assertTrue(q3Values.contains("")); + } } 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 ab37efc1c5..da3037ed6b 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 @@ -16,15 +16,14 @@ package org.citrusframework.http.message; -import java.util.Collections; - -import org.citrusframework.context.TestContextFactory; import org.citrusframework.exceptions.ValidationException; import org.citrusframework.testng.AbstractTestNGUnitTest; import org.citrusframework.validation.context.HeaderValidationContext; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.util.Collections; + import static org.hamcrest.Matchers.is; /** @@ -32,13 +31,8 @@ */ public class HttpQueryParamHeaderValidatorTest extends AbstractTestNGUnitTest { - private HttpQueryParamHeaderValidator validator = new HttpQueryParamHeaderValidator(); - private HeaderValidationContext validationContext = new HeaderValidationContext(); - - @Override - protected TestContextFactory createTestContextFactory() { - return TestContextFactory.newInstance(); - } + private final HttpQueryParamHeaderValidator validator = new HttpQueryParamHeaderValidator(); + private final HeaderValidationContext validationContext = new HeaderValidationContext(); @Test(dataProvider = "successData") public void testValidateHeader(Object receivedValue, Object controlValue) { @@ -51,6 +45,8 @@ public Object[][] successData() { new Object[] { "foobar", "@contains(foo)@" }, new Object[] { "foo=fooValue,bar=barValue", "foo=fooValue,bar=barValue" }, new Object[] { "foo=,bar=barValue", "foo=,bar=barValue" }, + new Object[] { "foo=1,foo=2,foo=3,bar=barValue", "foo=1,foo=2,foo=3,bar=barValue" }, + new Object[] { "foo=1,foo=2,foo=3,bar=barValue", "foo=3,foo=2,foo=1,bar=barValue" }, new Object[] { null, null }, new Object[] { Collections.singletonMap("key", "value"), Collections.singletonMap("key", "value") }, new Object[] { Collections.singletonMap("key", "value"), Collections.singletonMap("key", is("value")) } diff --git a/endpoints/citrus-http/src/test/java/org/citrusframework/http/servlet/CachingHttpServletRequestWrapperTest.java b/endpoints/citrus-http/src/test/java/org/citrusframework/http/servlet/CachingHttpServletRequestWrapperTest.java index 8e58a9c5d7..1a86de23a9 100644 --- a/endpoints/citrus-http/src/test/java/org/citrusframework/http/servlet/CachingHttpServletRequestWrapperTest.java +++ b/endpoints/citrus-http/src/test/java/org/citrusframework/http/servlet/CachingHttpServletRequestWrapperTest.java @@ -16,13 +16,6 @@ package org.citrusframework.http.servlet; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.Collections; -import java.util.Map; - import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; @@ -34,6 +27,13 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; @@ -164,7 +164,7 @@ public void testParseUrlEncodedBodyWithExtendedApplicationType(final RequestMeth wrapper.getInputStream(); when(serverRequestMock.getContentType()) - .thenReturn(ContentType.APPLICATION_FORM_URLENCODED.withCharset(Charset.forName("UTF-8")).toString()); + .thenReturn(ContentType.APPLICATION_FORM_URLENCODED.withCharset(StandardCharsets.UTF_8).toString()); when(serverRequestMock.getMethod()).thenReturn(requestMethod.name()); @@ -187,11 +187,11 @@ public void testParseUrlEncodedBodyWithSpecialEncoding() throws Exception { when(serverRequestMock.getInputStream()) .thenReturn(new DelegatingServletInputStream( new ByteArrayInputStream( - (requestMethod.name() + "=ÄäÖöÜü").getBytes(Charset.forName("ISO-8859-1"))))); + (requestMethod.name() + "=ÄäÖöÜü").getBytes(StandardCharsets.ISO_8859_1)))); wrapper.getInputStream(); when(serverRequestMock.getContentType()) - .thenReturn(ContentType.APPLICATION_FORM_URLENCODED.withCharset(Charset.forName("ISO-8859-1")).toString()); + .thenReturn(ContentType.APPLICATION_FORM_URLENCODED.withCharset(StandardCharsets.ISO_8859_1).toString()); when(serverRequestMock.getMethod()).thenReturn(requestMethod.name()); diff --git a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/actions/PurgeJmsQueuesAction.java b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/actions/PurgeJmsQueuesAction.java index ba6de91105..73dc9cf3b8 100644 --- a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/actions/PurgeJmsQueuesAction.java +++ b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/actions/PurgeJmsQueuesAction.java @@ -144,7 +144,7 @@ private void purgeQueue(Queue queue, Session session) throws JMSException { */ private void purgeDestination(Destination destination, Session session, String destinationName) throws JMSException { if (logger.isDebugEnabled()) { - logger.debug("Try to purge destination " + destinationName); + logger.debug("Try to purge destination {}", destinationName); } int messagesPurged = 0; @@ -155,7 +155,7 @@ private void purgeDestination(Destination destination, Session session, String d message = (receiveTimeout >= 0) ? messageConsumer.receive(receiveTimeout) : messageConsumer.receive(); if (message != null) { - logger.debug("Removed message from destination " + destinationName); + logger.debug("Removed message from destination {}", destinationName); messagesPurged++; try { @@ -167,7 +167,7 @@ private void purgeDestination(Destination destination, Session session, String d } while (message != null); if (logger.isDebugEnabled()) { - logger.debug("Purged " + messagesPurged + " messages from destination"); + logger.debug("Purged {} messages from destination", messagesPurged); } } finally { JmsUtils.closeMessageConsumer(messageConsumer); diff --git a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsConsumer.java b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsConsumer.java index 77c9b281d5..2ff38d9e54 100644 --- a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsConsumer.java +++ b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsConsumer.java @@ -80,7 +80,7 @@ private jakarta.jms.Message receive(String destinationName, String selector) { jakarta.jms.Message receivedJmsMessage; if (logger.isDebugEnabled()) { - logger.debug("Receiving JMS message on destination: '" + getDestinationNameWithSelector(destinationName, selector) + "'"); + logger.debug("Receiving JMS message on destination: '{}'", getDestinationNameWithSelector(destinationName, selector)); } if (StringUtils.hasText(selector)) { @@ -93,7 +93,7 @@ private jakarta.jms.Message receive(String destinationName, String selector) { throw new MessageTimeoutException(endpointConfiguration.getTimeout(), getDestinationNameWithSelector(destinationName, selector)); } - logger.info("Received JMS message on destination: '" + getDestinationNameWithSelector(destinationName, selector) + "'"); + logger.info("Received JMS message on destination: '{}'", getDestinationNameWithSelector(destinationName, selector)); return receivedJmsMessage; } @@ -108,7 +108,7 @@ private jakarta.jms.Message receive(Destination destination, String selector) { jakarta.jms.Message receivedJmsMessage; if (logger.isDebugEnabled()) { - logger.debug("Receiving JMS message on destination: '" + getDestinationNameWithSelector(endpointConfiguration.getDestinationName(destination), selector) + "'"); + logger.debug("Receiving JMS message on destination: '{}'", getDestinationNameWithSelector(endpointConfiguration.getDestinationName(destination), selector)); } if (StringUtils.hasText(selector)) { @@ -121,7 +121,7 @@ private jakarta.jms.Message receive(Destination destination, String selector) { throw new MessageTimeoutException(endpointConfiguration.getTimeout(), getDestinationNameWithSelector(endpointConfiguration.getDestinationName(destination), selector)); } - logger.info("Received JMS message on destination: '" + getDestinationNameWithSelector(endpointConfiguration.getDestinationName(destination), selector) + "'"); + logger.info("Received JMS message on destination: '{}'", getDestinationNameWithSelector(endpointConfiguration.getDestinationName(destination), selector)); return receivedJmsMessage; } diff --git a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsProducer.java b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsProducer.java index b490869ca5..73aa80d62a 100644 --- a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsProducer.java +++ b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsProducer.java @@ -81,7 +81,7 @@ public void send(final Message message, final TestContext context) { */ private void send(Message message, String destinationName, TestContext context) { if (logger.isDebugEnabled()) { - logger.debug("Sending JMS message to destination: '" + destinationName + "'"); + logger.debug("Sending JMS message to destination: '{}'", destinationName); } endpointConfiguration.getJmsTemplate().send(destinationName, session -> { @@ -90,7 +90,7 @@ private void send(Message message, String destinationName, TestContext context) return jmsMessage; }); - logger.info("Message was sent to JMS destination: '" + destinationName + "'"); + logger.info("Message was sent to JMS destination: '{}'", destinationName); } /** @@ -101,7 +101,7 @@ private void send(Message message, String destinationName, TestContext context) */ private void send(Message message, Destination destination, TestContext context) { if (logger.isDebugEnabled()) { - logger.debug("Sending JMS message to destination: '" + endpointConfiguration.getDestinationName(destination) + "'"); + logger.debug("Sending JMS message to destination: '{}'", endpointConfiguration.getDestinationName(destination)); } endpointConfiguration.getJmsTemplate().send(destination, session -> { @@ -110,7 +110,7 @@ private void send(Message message, Destination destination, TestContext context) return jmsMessage; }); - logger.info("Message was sent to JMS destination: '" + endpointConfiguration.getDestinationName(destination) + "'"); + logger.info("Message was sent to JMS destination: '{}'", endpointConfiguration.getDestinationName(destination)); } @Override diff --git a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsSyncConsumer.java b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsSyncConsumer.java index 02edad0b3b..baf47a437a 100644 --- a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsSyncConsumer.java +++ b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsSyncConsumer.java @@ -79,7 +79,7 @@ public void send(final Message message, final TestContext context) { ObjectHelper.assertNotNull(replyDestination, "Failed to find JMS reply destination for message correlation key: '" + correlationKey + "'"); if (logger.isDebugEnabled()) { - logger.debug("Sending JMS message to destination: '" + endpointConfiguration.getDestinationName(replyDestination) + "'"); + logger.debug("Sending JMS message to destination: '{}'", endpointConfiguration.getDestinationName(replyDestination)); } endpointConfiguration.getJmsTemplate().send(replyDestination, session -> { @@ -90,7 +90,7 @@ public void send(final Message message, final TestContext context) { context.onOutboundMessage(message); - logger.info("Message was sent to JMS destination: '" + endpointConfiguration.getDestinationName(replyDestination) + "'"); + logger.info("Message was sent to JMS destination: '{}'", endpointConfiguration.getDestinationName(replyDestination)); } /** @@ -107,8 +107,7 @@ public void saveReplyDestination(JmsMessage jmsMessage, TestContext context) { correlationManager.saveCorrelationKey(correlationKeyName, correlationKey, context); correlationManager.store(correlationKey, jmsMessage.getReplyTo()); } else { - logger.warn("Unable to retrieve reply to destination for message \n" + - jmsMessage + "\n - no reply to destination found in message headers!"); + logger.warn("Unable to retrieve reply to destination for message \n{}\n - no reply to destination found in message headers!", jmsMessage); } } diff --git a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsSyncProducer.java b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsSyncProducer.java index 8477409dc4..6b0793beca 100644 --- a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsSyncProducer.java +++ b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsSyncProducer.java @@ -91,7 +91,7 @@ public void send(Message message, TestContext context) { Destination destination; if (endpointConfiguration.getDestination() != null) { if (logger.isDebugEnabled()) { - logger.debug("Sending JMS message to destination: '" + endpointConfiguration.getDestinationName(endpointConfiguration.getDestination()) + "'"); + logger.debug("Sending JMS message to destination: '{}'", endpointConfiguration.getDestinationName(endpointConfiguration.getDestination())); } destination = endpointConfiguration.getDestination(); @@ -103,7 +103,7 @@ public void send(Message message, TestContext context) { } } else if (endpointConfiguration.getJmsTemplate().getDefaultDestination() != null) { if (logger.isDebugEnabled()) { - logger.debug("Sending JMS message to destination: '" + endpointConfiguration.getDestinationName(endpointConfiguration.getJmsTemplate().getDefaultDestination()) + "'"); + logger.debug("Sending JMS message to destination: '{}'", endpointConfiguration.getDestinationName(endpointConfiguration.getJmsTemplate().getDefaultDestination())); } destination = endpointConfiguration.getJmsTemplate().getDefaultDestination(); @@ -201,8 +201,7 @@ protected void createConnection() throws JMSException { connection = ((TopicConnectionFactory) endpointConfiguration.getConnectionFactory()).createTopicConnection(); connection.setClientID(getName()); } else { - logger.warn("Not able to create a connection with connection factory '" + endpointConfiguration.getConnectionFactory() + "'" + - " when using setting 'publish-subscribe-domain' (=" + endpointConfiguration.isPubSubDomain() + ")"); + logger.warn("Not able to create a connection with connection factory '{}' when using setting 'publish-subscribe-domain' (={})", endpointConfiguration.getConnectionFactory(), endpointConfiguration.isPubSubDomain()); connection = endpointConfiguration.getConnectionFactory().createConnection(); } @@ -224,8 +223,7 @@ protected void createSession(Connection connection) throws JMSException { } else if (endpointConfiguration.isPubSubDomain() && endpointConfiguration.getConnectionFactory() instanceof TopicConnectionFactory) { session = ((TopicConnection) connection).createTopicSession(false, Session.AUTO_ACKNOWLEDGE); } else { - logger.warn("Not able to create a session with connection factory '" + endpointConfiguration.getConnectionFactory() + "'" + - " when using setting 'publish-subscribe-domain' (=" + endpointConfiguration.isPubSubDomain() + ")"); + logger.warn("Not able to create a session with connection factory '{}' when using setting 'publish-subscribe-domain' (={})", endpointConfiguration.getConnectionFactory(), endpointConfiguration.isPubSubDomain()); session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); } @@ -269,7 +267,7 @@ private void deleteTemporaryDestination(Destination destination) { ((TemporaryTopic) destination).delete(); } } catch (JMSException e) { - logger.error("Error while deleting temporary destination '" + destination + "'", e); + logger.error("Error while deleting temporary destination '{}'", destination, e); } } @@ -310,7 +308,7 @@ private Destination getReplyDestination(Session session, Message message) throws */ private Destination resolveDestination(String destinationName) throws JMSException { if (logger.isDebugEnabled()) { - logger.debug("Sending JMS message to destination: '" + destinationName + "'"); + logger.debug("Sending JMS message to destination: '{}'", destinationName); } return resolveDestinationName(destinationName, session); diff --git a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsTopicSubscriber.java b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsTopicSubscriber.java index eab4550be3..43443cf707 100644 --- a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsTopicSubscriber.java +++ b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/endpoint/JmsTopicSubscriber.java @@ -50,7 +50,7 @@ public class JmsTopicSubscriber extends JmsConsumer implements Runnable { /** Logger */ - private static final Logger logger = LoggerFactory.getLogger(JmsConsumer.class); + private static final Logger logger = LoggerFactory.getLogger(JmsTopicSubscriber.class); /** Boolean flag for continued message consumption, if false stop */ private boolean running = true; @@ -118,7 +118,7 @@ public void run() { TopicSubscriber subscriber; if (endpointConfiguration.isDurableSubscription()) { - logger.debug(String.format("Create JMS topic durable subscription '%s'", Optional.ofNullable(endpointConfiguration.getDurableSubscriberName()).orElseGet(this::getName))); + logger.debug("Create JMS topic durable subscription '{}'", Optional.ofNullable(endpointConfiguration.getDurableSubscriberName()).orElseGet(this::getName)); subscriber = session.createDurableSubscriber(topic, Optional.ofNullable(endpointConfiguration.getDurableSubscriberName()).orElseGet(this::getName)); } else { logger.debug("Create JMS topic subscription"); @@ -137,12 +137,12 @@ public void run() { Message message = endpointConfiguration.getMessageConverter().convertInbound(event, endpointConfiguration, context); if (logger.isDebugEnabled()) { - logger.debug(String.format("Received topic event '%s'", message.getId())); + logger.debug("Received topic event '{}'", message.getId()); } messageQueue.createProducer().send(message, context); } else { if (logger.isDebugEnabled()) { - logger.debug("Topic subscriber received null message - continue after " + endpointConfiguration.getPollingInterval() + " milliseconds"); + logger.debug("Topic subscriber received null message - continue after {} milliseconds", endpointConfiguration.getPollingInterval()); } try { diff --git a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/message/SoapJmsMessageConverter.java b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/message/SoapJmsMessageConverter.java index c0b84413d8..fbc4e280d8 100644 --- a/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/message/SoapJmsMessageConverter.java +++ b/endpoints/citrus-jms/src/main/java/org/citrusframework/jms/message/SoapJmsMessageConverter.java @@ -122,7 +122,7 @@ public org.citrusframework.message.Message convertInbound(Message jmsMessage, Jm public Message createJmsMessage(org.citrusframework.message.Message message, Session session, JmsEndpointConfiguration endpointConfiguration, TestContext context) { String payload = message.getPayload(String.class); - logger.debug("Creating SOAP message from payload: " + payload); + logger.debug("Creating SOAP message from payload: {}", payload); try { SoapMessage soapMessage = soapMessageFactory.createWebServiceMessage(); diff --git a/endpoints/citrus-jms/src/test/java/org/citrusframework/jms/integration/service/LoggingInterceptor.java b/endpoints/citrus-jms/src/test/java/org/citrusframework/jms/integration/service/LoggingInterceptor.java index 63f37626c8..96ed8f3d8c 100644 --- a/endpoints/citrus-jms/src/test/java/org/citrusframework/jms/integration/service/LoggingInterceptor.java +++ b/endpoints/citrus-jms/src/test/java/org/citrusframework/jms/integration/service/LoggingInterceptor.java @@ -28,7 +28,7 @@ public class LoggingInterceptor implements ChannelInterceptor { @Override public Message preSend(Message message, MessageChannel channel) { if (logger.isDebugEnabled()) { - logger.debug(channel.toString() + ": " + message.getPayload()); + logger.debug("{}: {}", channel.toString(), message.getPayload()); } if (message.getPayload() instanceof Throwable) { diff --git a/endpoints/citrus-rmi/src/main/java/org/citrusframework/rmi/client/RmiClient.java b/endpoints/citrus-rmi/src/main/java/org/citrusframework/rmi/client/RmiClient.java index f58da3dcc8..1abc0c744a 100644 --- a/endpoints/citrus-rmi/src/main/java/org/citrusframework/rmi/client/RmiClient.java +++ b/endpoints/citrus-rmi/src/main/java/org/citrusframework/rmi/client/RmiClient.java @@ -16,14 +16,6 @@ package org.citrusframework.rmi.client; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.rmi.NotBoundException; -import java.rmi.Remote; -import java.rmi.RemoteException; -import java.rmi.registry.Registry; -import java.util.Arrays; - import org.citrusframework.context.TestContext; import org.citrusframework.endpoint.AbstractEndpoint; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -46,6 +38,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.rmi.NotBoundException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; +import java.util.Arrays; + /** * @since 2.5 */ @@ -112,8 +112,8 @@ public void send(final Message message, TestContext context) { } if (logger.isDebugEnabled()) { - logger.debug("Sending message to RMI server: '" + binding + "'"); - logger.debug("Message to send:\n" + message.getPayload(String.class)); + logger.debug("Sending message to RMI server: '{}'", binding); + logger.debug("Message to send:\n{}", message.getPayload(String.class)); } context.onOutboundMessage(message); @@ -132,7 +132,7 @@ public void send(final Message message, TestContext context) { Message response = new DefaultMessage(payload.toString()); correlationManager.store(correlationKey, response); - logger.info("Message was sent to RMI server: '" + binding + "'"); + logger.info("Message was sent to RMI server: '{}'", binding); if (result != null) { context.onInboundMessage(response); } @@ -146,7 +146,7 @@ public void send(final Message message, TestContext context) { throw new CitrusRuntimeException("Failed to invoke method on remote target, because remote method not accessible", e); } - logger.info("Message was sent to RMI server: '" + binding + "'"); + logger.info("Message was sent to RMI server: '{}'", binding); } @Override diff --git a/endpoints/citrus-rmi/src/main/java/org/citrusframework/rmi/server/RmiServer.java b/endpoints/citrus-rmi/src/main/java/org/citrusframework/rmi/server/RmiServer.java index 900eec01ed..80484d5cb5 100644 --- a/endpoints/citrus-rmi/src/main/java/org/citrusframework/rmi/server/RmiServer.java +++ b/endpoints/citrus-rmi/src/main/java/org/citrusframework/rmi/server/RmiServer.java @@ -16,6 +16,17 @@ package org.citrusframework.rmi.server; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.message.Message; +import org.citrusframework.rmi.endpoint.RmiEndpointConfiguration; +import org.citrusframework.rmi.model.RmiServiceInvocation; +import org.citrusframework.rmi.model.RmiServiceResult; +import org.citrusframework.server.AbstractServer; +import org.citrusframework.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.xml.transform.Source; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -26,17 +37,6 @@ import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; import java.util.List; -import javax.xml.transform.Source; - -import org.citrusframework.exceptions.CitrusRuntimeException; -import org.citrusframework.message.Message; -import org.citrusframework.rmi.endpoint.RmiEndpointConfiguration; -import org.citrusframework.rmi.model.RmiServiceInvocation; -import org.citrusframework.rmi.model.RmiServiceResult; -import org.citrusframework.server.AbstractServer; -import org.citrusframework.util.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * @since 2.5 @@ -78,7 +78,7 @@ public RmiServer(RmiEndpointConfiguration endpointConfiguration) { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (logger.isDebugEnabled()) { - logger.debug("Received message on RMI server: '" + endpointConfiguration.getBinding() + "'"); + logger.debug("Received message on RMI server: '{}'", endpointConfiguration.getBinding()); } Message response = getEndpointAdapter().handleMessage(endpointConfiguration.getMessageConverter() @@ -153,7 +153,7 @@ protected void shutdown() { try { registry.unbind(endpointConfiguration.getBinding()); } catch (Exception e) { - logger.warn("Failed to unbind from registry:" + e.getMessage()); + logger.warn("Failed to unbind from registry:{}", e.getMessage()); } } @@ -161,7 +161,7 @@ protected void shutdown() { try { UnicastRemoteObject.unexportObject(proxy, true); } catch (Exception e) { - logger.warn("Failed to unexport from remote object:" + e.getMessage()); + logger.warn("Failed to unexport from remote object:{}", e.getMessage()); } } diff --git a/endpoints/citrus-rmi/src/test/java/org/citrusframework/rmi/integration/RmiDynamicEndpointIT.java b/endpoints/citrus-rmi/src/test/java/org/citrusframework/rmi/integration/RmiDynamicEndpointIT.java index 8e55806ada..72fde4cb7c 100644 --- a/endpoints/citrus-rmi/src/test/java/org/citrusframework/rmi/integration/RmiDynamicEndpointIT.java +++ b/endpoints/citrus-rmi/src/test/java/org/citrusframework/rmi/integration/RmiDynamicEndpointIT.java @@ -24,9 +24,9 @@ /** * @since 2.0 */ -@Test public class RmiDynamicEndpointIT extends TestNGCitrusSpringSupport { + @Test @CitrusTestSource(type = TestLoader.SPRING, name = "RmiDynamicEndpointIT") public void testDynamicEndpoint() {} } diff --git a/endpoints/citrus-rmi/src/test/java/org/citrusframework/rmi/server/RmiServerTest.java b/endpoints/citrus-rmi/src/test/java/org/citrusframework/rmi/server/RmiServerTest.java index 324f897192..c234483b20 100644 --- a/endpoints/citrus-rmi/src/test/java/org/citrusframework/rmi/server/RmiServerTest.java +++ b/endpoints/citrus-rmi/src/test/java/org/citrusframework/rmi/server/RmiServerTest.java @@ -16,11 +16,6 @@ package org.citrusframework.rmi.server; -import java.io.IOException; -import java.rmi.Remote; -import java.rmi.registry.Registry; -import java.util.List; - import org.citrusframework.endpoint.EndpointAdapter; import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.Message; @@ -34,9 +29,15 @@ import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; import org.testng.Assert; -import org.testng.annotations.BeforeClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import java.io.IOException; +import java.rmi.Remote; +import java.rmi.registry.Registry; +import java.util.List; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; @@ -49,12 +50,20 @@ public class RmiServerTest extends AbstractTestNGUnitTest { @Mock private Registry registry; + @Mock private EndpointAdapter endpointAdapter; - @BeforeClass + private AutoCloseable mockitoContext; + + @BeforeMethod public void setup() { - MockitoAnnotations.openMocks(this); + mockitoContext = MockitoAnnotations.openMocks(this); + } + + @AfterMethod + public void teardown() throws Exception { + mockitoContext.close(); } @Test @@ -83,8 +92,8 @@ public void testServiceInvocationWithArgument() throws Exception { try { Assert.assertEquals( - message.getPayload(String.class).replaceAll("\\s", ""), - FileUtils.readToString(Resources.create("service-invocation.xml", RmiServer.class)).replaceAll("\\s", "") + message.getPayload(String.class).replaceAll("\\s", ""), + FileUtils.readToString(Resources.create("service-invocation.xml", RmiServer.class)).replaceAll("\\s", "") ); } catch (IOException e) { Assert.fail(e.getMessage()); @@ -96,7 +105,7 @@ public void testServiceInvocationWithArgument() throws Exception { rmiServer.startup(); try { - ((HelloService)remote[0]).sayHello("Hello RMI this is cool!"); + ((HelloService) remote[0]).sayHello("Hello RMI this is cool!"); } catch (Throwable throwable) { Assert.fail("Failed to invoke remote service", throwable); } @@ -128,8 +137,8 @@ public void testServiceInvocationWithResult() throws Exception { try { Assert.assertEquals( - message.getPayload(String.class).replaceAll("\\s", ""), - FileUtils.readToString(Resources.create("service-invocation-2.xml", RmiServer.class)).replaceAll("\\s", "") + message.getPayload(String.class).replaceAll("\\s", ""), + FileUtils.readToString(Resources.create("service-invocation-2.xml", RmiServer.class)).replaceAll("\\s", "") ); } catch (IOException e) { Assert.fail(e.getMessage()); @@ -141,7 +150,7 @@ public void testServiceInvocationWithResult() throws Exception { rmiServer.startup(); try { - Assert.assertEquals(((HelloService)remote[0]).getHelloCount(), 10); + Assert.assertEquals(((HelloService) remote[0]).getHelloCount(), 10); } catch (Throwable throwable) { Assert.fail("Failed to invoke remote service", throwable); } diff --git a/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/actions/ReceiveSoapMessageAction.java b/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/actions/ReceiveSoapMessageAction.java index 4957debb01..679f300879 100644 --- a/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/actions/ReceiveSoapMessageAction.java +++ b/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/actions/ReceiveSoapMessageAction.java @@ -98,7 +98,7 @@ public SoapAttachmentValidator getAttachmentValidator() { /** * Action builder. */ - public static final class Builder extends ReceiveMessageActionBuilder { + public static class Builder extends ReceiveMessageActionBuilder { /** Soap message to receive */ private final SoapMessage soapMessage = new SoapMessage(); diff --git a/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/actions/SendSoapMessageAction.java b/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/actions/SendSoapMessageAction.java index 9f4a591ac2..2216336fb1 100644 --- a/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/actions/SendSoapMessageAction.java +++ b/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/actions/SendSoapMessageAction.java @@ -139,7 +139,7 @@ public boolean isMtomEnabled() { /** * Action builder. */ - public static final class Builder extends SendSoapMessageBuilder { + public static class Builder extends SendSoapMessageBuilder { public Builder() { message(new StaticMessageBuilder(soapMessage)); diff --git a/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/ReceiveSoapMessageActionParser.java b/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/ReceiveSoapMessageActionParser.java index 446afe7518..f959ca124c 100644 --- a/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/ReceiveSoapMessageActionParser.java +++ b/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/ReceiveSoapMessageActionParser.java @@ -28,6 +28,7 @@ import org.citrusframework.validation.builder.DefaultMessageBuilder; import org.citrusframework.validation.context.ValidationContext; import org.citrusframework.ws.actions.ReceiveSoapMessageAction; +import org.citrusframework.ws.actions.ReceiveSoapMessageAction.Builder; import org.citrusframework.ws.message.SoapAttachment; import org.citrusframework.ws.message.SoapMessageHeaders; import org.citrusframework.ws.validation.SoapAttachmentValidator; @@ -44,7 +45,7 @@ public class ReceiveSoapMessageActionParser extends ReceiveMessageActionParser { @Override protected BeanDefinitionBuilder parseComponent(Element element, ParserContext parserContext) { - BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ReceiveSoapMessageActionFactoryBean.class); + BeanDefinitionBuilder builder = super.parseComponent(element, parserContext); List attachmentElements = DomUtils.getChildElementsByTagName(element, "attachment"); List attachments = new ArrayList<>(); @@ -62,6 +63,11 @@ protected BeanDefinitionBuilder parseComponent(Element element, ParserContext pa return builder; } + @Override + protected Class getMessageFactoryClass() { + return ReceiveSoapMessageActionFactoryBean.class; + } + @Override protected void parseHeaderElements(Element actionElement, DefaultMessageBuilder messageBuilder, List validationContexts) { super.parseHeaderElements(actionElement, messageBuilder, validationContexts); @@ -89,7 +95,15 @@ protected void parseHeaderElements(Element actionElement, DefaultMessageBuilder */ public static class ReceiveSoapMessageActionFactoryBean extends AbstractReceiveMessageActionFactoryBean { - private final ReceiveSoapMessageAction.Builder builder = new ReceiveSoapMessageAction.Builder(); + private final Builder builder; + + public ReceiveSoapMessageActionFactoryBean() { + this(new Builder()); + } + + public ReceiveSoapMessageActionFactoryBean(Builder builder) { + this.builder = builder; + } /** * Sets the control attachments. diff --git a/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/SendSoapFaultActionParser.java b/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/SendSoapFaultActionParser.java index 877469e157..11e811362d 100644 --- a/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/SendSoapFaultActionParser.java +++ b/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/SendSoapFaultActionParser.java @@ -108,7 +108,7 @@ private void parseFaultDetail(BeanDefinitionBuilder builder, Element faultElemen } @Override - protected Class getBeanDefinitionClass() { + protected Class getMessageFactoryClass() { return SendSoapFaultActionFactoryBean.class; } diff --git a/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/SendSoapMessageActionParser.java b/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/SendSoapMessageActionParser.java index 1bb724f17f..c918b1b546 100644 --- a/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/SendSoapMessageActionParser.java +++ b/endpoints/citrus-ws/src/main/java/org/citrusframework/ws/config/xml/SendSoapMessageActionParser.java @@ -27,6 +27,7 @@ import org.citrusframework.validation.builder.DefaultMessageBuilder; import org.citrusframework.validation.context.ValidationContext; import org.citrusframework.ws.actions.SendSoapMessageAction; +import org.citrusframework.ws.actions.SendSoapMessageAction.Builder; import org.citrusframework.ws.message.SoapAttachment; import org.citrusframework.ws.message.SoapMessageHeaders; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -82,7 +83,7 @@ protected void parseHeaderElements(Element actionElement, DefaultMessageBuilder } @Override - protected Class> getBeanDefinitionClass() { + protected Class> getMessageFactoryClass() { return SendSoapMessageActionFactoryBean.class; } @@ -91,7 +92,15 @@ protected void parseHeaderElements(Element actionElement, DefaultMessageBuilder */ public static class SendSoapMessageActionFactoryBean extends AbstractSendMessageActionFactoryBean { - private final SendSoapMessageAction.Builder builder = new SendSoapMessageAction.Builder(); + private final SendSoapMessageAction.Builder builder; + + public SendSoapMessageActionFactoryBean() { + this(new Builder()); + } + + public SendSoapMessageActionFactoryBean(SendSoapMessageAction.Builder builder) { + this.builder = builder; + } /** * Sets the control attachments. diff --git a/pom.xml b/pom.xml index 5b51a4e602..65d546a50f 100644 --- a/pom.xml +++ b/pom.xml @@ -50,14 +50,15 @@ - core - utils - runtime - validation + catalog connectors + core endpoints + runtime + test-api-generator tools - catalog + utils + validation @@ -154,7 +155,7 @@ ${project.version} 17 - 3.9.6 + 3.9.9 3.11.0 ${java.version} ${java.version} @@ -173,8 +174,9 @@ 3.3.0 3.11.2 1.6.13 - 3.9.0 + 3.15.1 3.13.1 + 3.3.0 3.0.1 3.3.1 1.11.2 @@ -192,6 +194,7 @@ 1.10.15 4.9.0 1.1.27 + 2.44.1 1.8.0 3.27.2 4.2.2 @@ -249,6 +252,8 @@ 3.2.0 4.1.105.Final 4.12.0 + 7.9.0 + 0.2.6 4.7.6 42.7.5 3.17.4 @@ -258,6 +263,7 @@ 1.1.10.7 2.3 6.2.2 + 3.4.1 4.0.11 6.4.1 3.0.3 @@ -518,15 +524,29 @@ - org.springframework - spring-test - ${spring.version} + org.awaitility + awaitility + ${awaitility.version} + + + org.openapitools + openapi-generator + ${org.openapitools.version} + provided + + org.testng testng ${testng.version} + + org.springframework + spring-test + ${spring.version} + + junit @@ -696,9 +716,15 @@ - org.awaitility - awaitility - ${awaitility.version} + com.atlassian.oai + swagger-request-validator-core + ${swagger-request-validator.version} + + + commons-logging + commons-logging + + @@ -994,6 +1020,16 @@ jackson-databind ${jackson.version} + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + + + org.openapitools + jackson-databind-nullable + ${jackson-databind-nullable.version} + com.fasterxml.jackson.module jackson-module-jaxb-annotations @@ -1323,6 +1359,12 @@ mockito-junit-jupiter test + + uk.org.webcompere + system-stubs-core + 2.1.6 + test + diff --git a/runtime/citrus-groovy/src/test/java/org/citrusframework/groovy/dsl/container/WaitForTest.java b/runtime/citrus-groovy/src/test/java/org/citrusframework/groovy/dsl/container/WaitForTest.java index f47ff42fdb..fdfc564c5f 100644 --- a/runtime/citrus-groovy/src/test/java/org/citrusframework/groovy/dsl/container/WaitForTest.java +++ b/runtime/citrus-groovy/src/test/java/org/citrusframework/groovy/dsl/container/WaitForTest.java @@ -16,6 +16,8 @@ package org.citrusframework.groovy.dsl.container; +import static org.citrusframework.util.TestUtils.HTTPS_CITRUSFRAMEWORK_ORG; + import org.citrusframework.TestCase; import org.citrusframework.TestCaseMetaInfo; import org.citrusframework.actions.EchoAction; @@ -30,7 +32,9 @@ import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.DefaultMessageStore; import org.citrusframework.message.MessageStore; +import org.citrusframework.util.TestUtils; import org.testng.Assert; +import org.testng.SkipException; import org.testng.annotations.Test; public class WaitForTest extends AbstractGroovyActionDslTest { @@ -42,7 +46,11 @@ public class WaitForTest extends AbstractGroovyActionDslTest { @Test public void shouldLoadWaitFor() { - String httpUrl = "https://citrusframework.org"; + + if (!TestUtils.isNetworkReachable()) { + throw new SkipException("Test skipped because citrus is not reachable. We are probably running behind a proxy."); + } + String filePath = "classpath:org/citrusframework/groovy/test-request-payload.xml"; MessageStore messageStore = new DefaultMessageStore(); @@ -70,11 +78,11 @@ public void shouldLoadWaitFor() { validateWaitAction(action, "10000", "2000", condition); action = (Wait) result.getTestAction(actionIndex++); - condition = getHttpCondition(httpUrl, DEFAULT_RESPONSE_CODE, DEFAULT_TIMEOUT); + condition = getHttpCondition(HTTPS_CITRUSFRAMEWORK_ORG, DEFAULT_RESPONSE_CODE, DEFAULT_TIMEOUT); validateWaitAction(action, DEFAULT_WAIT_TIME, DEFAULT_INTERVAL, condition); action = (Wait) result.getTestAction(actionIndex++); - condition = getHttpCondition(httpUrl + "/doesnotexist", "404", "2000"); + condition = getHttpCondition(HTTPS_CITRUSFRAMEWORK_ORG + "/doesnotexist", "404", "2000"); ((HttpCondition)condition).setMethod("GET"); validateWaitAction(action, "3000", DEFAULT_INTERVAL, condition); diff --git a/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtension.java b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtension.java index f32a95d7a3..7b89af2568 100644 --- a/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtension.java +++ b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtension.java @@ -16,6 +16,17 @@ package org.citrusframework.junit.jupiter; +import static org.citrusframework.annotations.CitrusAnnotations.injectCitrusFramework; +import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getBaseKey; +import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getCitrus; +import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getTestCase; +import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getTestContext; +import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getTestLoader; +import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getTestRunner; +import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.requiresCitrus; +import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.setCitrus; + +import java.lang.reflect.Method; import org.citrusframework.Citrus; import org.citrusframework.CitrusContext; import org.citrusframework.CitrusInstanceManager; @@ -37,18 +48,7 @@ import org.junit.jupiter.api.extension.ReflectiveInvocationContext; import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; import org.junit.jupiter.api.extension.TestInstancePostProcessor; - -import java.lang.reflect.Method; - -import static org.citrusframework.annotations.CitrusAnnotations.injectCitrusFramework; -import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getBaseKey; -import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getCitrus; -import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getTestCase; -import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getTestContext; -import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getTestLoader; -import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.getTestRunner; -import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.requiresCitrus; -import static org.citrusframework.junit.jupiter.CitrusExtensionHelper.setCitrus; +import org.opentest4j.TestAbortedException; /** * JUnit5 extension adding {@link TestCaseRunner} support as well as Citrus annotation based resource injection @@ -77,6 +77,11 @@ public class CitrusExtension implements BeforeAllCallback, InvocationInterceptor public static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(CitrusExtension.class); private static void failTestCaseIfNotDoneYet(ExtensionContext extensionContext, Throwable throwable) { + + if (throwable instanceof TestAbortedException) { + return; + } + var testCase = getTestCase(extensionContext); var testResult = testCase.getTestResult(); diff --git a/runtime/citrus-testng/src/main/java/org/citrusframework/testng/TestNGHelper.java b/runtime/citrus-testng/src/main/java/org/citrusframework/testng/TestNGHelper.java index f119dfea34..de2fcd364d 100644 --- a/runtime/citrus-testng/src/main/java/org/citrusframework/testng/TestNGHelper.java +++ b/runtime/citrus-testng/src/main/java/org/citrusframework/testng/TestNGHelper.java @@ -24,7 +24,6 @@ import java.util.Arrays; import java.util.List; import java.util.Set; - import org.citrusframework.CitrusSettings; import org.citrusframework.DefaultTestCase; import org.citrusframework.TestCaseRunner; @@ -45,6 +44,7 @@ import org.slf4j.LoggerFactory; import org.testng.IHookCallBack; import org.testng.ITestResult; +import org.testng.SkipException; public final class TestNGHelper { @@ -71,10 +71,41 @@ private TestNGHelper() { */ public static void invokeTestMethod(Object target, ITestResult testResult, Method method, TestLoader testLoader, TestContext context, int invocationCount) { - Object[] params = TestNGParameterHelper.resolveParameter(target, testResult, method, context, invocationCount); - testLoader.configureTestCase(t -> TestNGParameterHelper.injectTestParameters(method, t, params)); - testLoader.doWithTestCase(t -> ReflectionHelper.invokeMethod(method, target, params)); - testLoader.load(); + + try { + Object[] params = TestNGParameterHelper.resolveParameter(target, testResult, method, + context, invocationCount); + testLoader.configureTestCase( + t -> TestNGParameterHelper.injectTestParameters(method, t, params)); + testLoader.doWithTestCase(t -> ReflectionHelper.invokeMethod(method, target, params)); + testLoader.load(); + } catch (CitrusRuntimeException e) { + SkipException skipException = getCauseOfType(e, SkipException.class); + if (skipException != null) { + throw skipException; + } + + throw e; + } + } + + /** + * Recursively checks if the cause of the given exception matches the target exception. + * + * @param exception The exception to check. + * @param targetCause The exception cause to search for. + * @return true if the target cause is found in the exception chain, false otherwise. + */ + public static T getCauseOfType(Throwable exception, Class targetCause) { + if (exception == null) { + return null; + } + + if (targetCause.isInstance(exception)) { + return targetCause.cast(exception); + } + + return getCauseOfType(exception.getCause(), targetCause); } /** diff --git a/runtime/citrus-testng/src/main/java/org/citrusframework/testng/spring/TestNGCitrusSpringSupport.java b/runtime/citrus-testng/src/main/java/org/citrusframework/testng/spring/TestNGCitrusSpringSupport.java index 213a08c22d..a262bb4b40 100644 --- a/runtime/citrus-testng/src/main/java/org/citrusframework/testng/spring/TestNGCitrusSpringSupport.java +++ b/runtime/citrus-testng/src/main/java/org/citrusframework/testng/spring/TestNGCitrusSpringSupport.java @@ -58,14 +58,11 @@ /** * Basic Citrus TestNG support base class with Spring support automatically handles test case runner creation. Also provides method parameter resolution * and resource injection. Users can just extend this class and make use of the action runner methods provided in {@link org.citrusframework.TestActionRunner} - * and {@link GherkinTestActionRunner}. Provides Spring test listener support and - * loads basic Spring application context for Citrus. - * + * and {@link GherkinTestActionRunner}. Provides Spring test listener support and loads basic Spring application context for Citrus. */ @Listeners( { TestNGCitrusSpringMethodInterceptor.class } ) @ContextConfiguration(classes = {CitrusSpringConfig.class}) -public class TestNGCitrusSpringSupport extends AbstractTestNGSpringContextTests - implements GherkinTestActionRunner { +public class TestNGCitrusSpringSupport extends AbstractTestNGSpringContextTests implements GherkinTestActionRunner { /** Citrus instance */ protected Citrus citrus; @@ -163,6 +160,9 @@ protected void run(ITestResult testResult, Method method, List metho @BeforeClass(alwaysRun = true) public final void before() { + // We need to consider the possibility, that one test has meanwhile modified the current citrus instance, + // as there can be plenty of tests running between @BeforeSuite and the execution of an actual subclass of + // this support. The citrus instance may even have a mocked context. if (citrus == null) { citrus = Citrus.newInstance(new CitrusSpringContextProvider(applicationContext)); CitrusAnnotations.injectCitrusFramework(this, citrus); @@ -205,7 +205,7 @@ public final void beforeSuite() { CitrusAnnotations.injectCitrusFramework(this, citrus); beforeSuite(citrus.getCitrusContext()); citrus.beforeSuite(Reporter.getCurrentTestResult().getTestContext().getSuite().getName(), - Reporter.getCurrentTestResult().getTestContext().getIncludedGroups()); + Reporter.getCurrentTestResult().getTestContext().getIncludedGroups()); } /** 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 74eb05ebfc..f3d92d87a5 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 @@ -1412,6 +1412,14 @@ + + + + + + + + @@ -1430,6 +1438,7 @@ + @@ -1447,6 +1456,7 @@ + @@ -1457,6 +1467,8 @@ + + @@ -1466,6 +1478,8 @@ + + diff --git a/runtime/citrus-xml/src/test/java/org/citrusframework/xml/container/WaitForTest.java b/runtime/citrus-xml/src/test/java/org/citrusframework/xml/container/WaitForTest.java index 40bea3f589..5869454777 100644 --- a/runtime/citrus-xml/src/test/java/org/citrusframework/xml/container/WaitForTest.java +++ b/runtime/citrus-xml/src/test/java/org/citrusframework/xml/container/WaitForTest.java @@ -16,6 +16,8 @@ package org.citrusframework.xml.container; +import static org.citrusframework.util.TestUtils.HTTPS_CITRUSFRAMEWORK_ORG; + import org.citrusframework.TestCase; import org.citrusframework.TestCaseMetaInfo; import org.citrusframework.actions.EchoAction; @@ -28,9 +30,11 @@ import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.DefaultMessageStore; import org.citrusframework.message.MessageStore; +import org.citrusframework.util.TestUtils; import org.citrusframework.xml.XmlTestLoader; import org.citrusframework.xml.actions.AbstractXmlActionTest; import org.testng.Assert; +import org.testng.SkipException; import org.testng.annotations.Test; public class WaitForTest extends AbstractXmlActionTest { @@ -42,7 +46,11 @@ public class WaitForTest extends AbstractXmlActionTest { @Test public void shouldLoadWaitFor() { - String httpUrl = "https://citrusframework.org"; + + if (!TestUtils.isNetworkReachable()) { + throw new SkipException("Test skipped because citrus is not reachable. We are probably running behind a proxy."); + } + String filePath = "classpath:org/citrusframework/xml/test-request-payload.xml"; MessageStore messageStore = new DefaultMessageStore(); @@ -70,11 +78,11 @@ public void shouldLoadWaitFor() { validateWaitAction(action, "10000", "2000", condition); action = (Wait) result.getTestAction(actionIndex++); - condition = getHttpCondition(httpUrl, DEFAULT_RESPONSE_CODE, DEFAULT_TIMEOUT); + condition = getHttpCondition(HTTPS_CITRUSFRAMEWORK_ORG, DEFAULT_RESPONSE_CODE, DEFAULT_TIMEOUT); validateWaitAction(action, DEFAULT_WAIT_TIME, DEFAULT_INTERVAL, condition); action = (Wait) result.getTestAction(actionIndex++); - condition = getHttpCondition(httpUrl + "/doesnotexist", "404", "2000"); + condition = getHttpCondition(HTTPS_CITRUSFRAMEWORK_ORG + "/doesnotexist", "404", "2000"); ((HttpCondition)condition).setMethod("GET"); validateWaitAction(action, "3000", DEFAULT_INTERVAL, condition); diff --git a/runtime/citrus-yaml/src/test/java/org/citrusframework/yaml/container/WaitForTest.java b/runtime/citrus-yaml/src/test/java/org/citrusframework/yaml/container/WaitForTest.java index 83933741b8..cb758c371d 100644 --- a/runtime/citrus-yaml/src/test/java/org/citrusframework/yaml/container/WaitForTest.java +++ b/runtime/citrus-yaml/src/test/java/org/citrusframework/yaml/container/WaitForTest.java @@ -16,6 +16,8 @@ package org.citrusframework.yaml.container; +import static org.citrusframework.util.TestUtils.HTTPS_CITRUSFRAMEWORK_ORG; + import org.citrusframework.TestCase; import org.citrusframework.TestCaseMetaInfo; import org.citrusframework.actions.EchoAction; @@ -28,9 +30,11 @@ import org.citrusframework.message.DefaultMessage; import org.citrusframework.message.DefaultMessageStore; import org.citrusframework.message.MessageStore; +import org.citrusframework.util.TestUtils; import org.citrusframework.yaml.YamlTestLoader; import org.citrusframework.yaml.actions.AbstractYamlActionTest; import org.testng.Assert; +import org.testng.SkipException; import org.testng.annotations.Test; public class WaitForTest extends AbstractYamlActionTest { @@ -42,7 +46,10 @@ public class WaitForTest extends AbstractYamlActionTest { @Test public void shouldLoadWaitFor() { - String httpUrl = "https://citrusframework.org"; + + if (!TestUtils.isNetworkReachable()) { + throw new SkipException("Test skipped because citrus is not reachable. We are probably running behind a proxy."); + } String filePath = "classpath:org/citrusframework/yaml/test-request-payload.xml"; MessageStore messageStore = new DefaultMessageStore(); @@ -70,11 +77,11 @@ public void shouldLoadWaitFor() { validateWaitAction(action, "10000", "2000", condition); action = (Wait) result.getTestAction(actionIndex++); - condition = getHttpCondition(httpUrl, DEFAULT_RESPONSE_CODE, DEFAULT_TIMEOUT); + condition = getHttpCondition(HTTPS_CITRUSFRAMEWORK_ORG, DEFAULT_RESPONSE_CODE, DEFAULT_TIMEOUT); validateWaitAction(action, DEFAULT_WAIT_TIME, DEFAULT_INTERVAL, condition); action = (Wait) result.getTestAction(actionIndex++); - condition = getHttpCondition(httpUrl + "/doesnotexist", "404", "2000"); + condition = getHttpCondition(HTTPS_CITRUSFRAMEWORK_ORG + "/doesnotexist", "404", "2000"); ((HttpCondition)condition).setMethod("GET"); validateWaitAction(action, "3000", DEFAULT_INTERVAL, condition); diff --git a/src/manual/connector-openapi.adoc b/src/manual/connector-openapi.adoc index 810fda6a05..f79c43f801 100644 --- a/src/manual/connector-openapi.adoc +++ b/src/manual/connector-openapi.adoc @@ -4,10 +4,17 @@ 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. +In particular, the message body is generated according to 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 +Taking it a step further, Citrus OpenAPI offers a TestAPI generator powered by OpenApiGenerator. +This generator produces the necessary Builders, which define explicit actions for each operation in a given OpenAPI. +These actions enable access to any operation within the OpenAPI, making it ideal for testing the API itself or interacting with other supporting services. +The current implementation of Citrus TestAPI uses Maven for generation and Spring for integration. +For more details, please refer to +<> . + +NOTE: The OpenAPI support in Citrus gets enabled by adding a separate Maven module as dependency to your project [source,xml] ---- @@ -29,16 +36,153 @@ 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-specification-settings]] +== OpenAPI specification settings + +The `OpenApiSettings` class provides configuration settings for controlling various aspects of OpenAPI request and response validation and random data generation. +These settings can be controlled globally through system properties or environment variables. +The following table outlines the available configuration options: + +|=== +| Parameter Name | Environment Variable Name | Description + +| `citrus.openapi.validation.enabled.request` | `CITRUS_OPENAPI_VALIDATION_DISABLE_REQUEST` | Controls whether request validation is enabled for OpenAPI operations. +| `citrus.openapi.validation.enabled.response` | `CITRUS_OPENAPI_VALIDATION_DISABLE_RESPONSE` | Controls whether response validation is enabled for OpenAPI operations. +| `citrus.openapi.neglect.base.path` | `CITRUS_OPENAPI_NEGLECT_BASE_PATH` | Determines whether to neglect the base path when building openration paths. +| `citrus.openapi.request.fill.random.values` | `CITRUS_OPENAPI_REQUEST_FILL_RANDOM_VALUES` | Specifies whether to automatically fill missing values in request bodies with random data. One of `REQUIRED` (default) `ALL` or `NONE`) +| `citrus.openapi.response.fill.random.values` | `CITRUS_OPENAPI_RESPONSE_FILL_RANDOM_VALUES` | Specifies whether to automatically fill missing values in response bodies with random data. One of `REQUIRED` (default) `ALL` or `NONE`) +| `citrus.openapi.generate.optional.fields` | `CITRUS_OPENAPI_GENERATE_OPTIONAL_FIELDS` | Enables or disables the generation of optional fields in the OpenAPI Json requests and responses. +| `citrus.openapi.validation.policy` | `CITRUS_OPENAPI_VALIDATION_POLICY` | Defines the validation policy for OpenAPI operations (e.g., `FAIL`, `REPORT`, `IGNORE`). +|=== + + +[[openapi-repository]] +== OpenAPI specification repositories + +An OpenApiRepository provides a centralized way to manage OpenAPI specifications within a test environment. +When bound to a bean container, it automatically makes registered specifications available for message +validation in conjunction with messages send by actions created by OpenApiActionBuilders. These built actions +ensure that all necessary validation information is present, enabling seamless verification without additional user input. + +While an OpenApiRepository is particularly useful for loading multiple OpenAPI definitions from a specified +location, in many cases, directly registering an individual OpenApiSpecification within the bean container +is sufficient. +Any registered specification—whether added individually or via a repository—will be considered during message validation. + +This approach simplifies OpenAPI-based validation in Citrus, ensuring that test messages conform to their expected structure without requiring manual intervention. + +.Java +[source,java,indent=0,role="primary"] +---- +@Bean +public OpenApiRepository petstoreOpenApiRepository() { + return new OpenApiRepository() + .locations(singletonList( + "classpath:org/citrusframework/openapi/petstore/petstore-v3.json")) + .neglectBasePath(true) + .validationPolicy(OpenApiValidationPolicy.REPORT) + .rootContextPath("/petstore/v3/api") + .requestValidationEnabled(false) + .responseValidationEnabled(false) + ; +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + + + + + +---- + +|=== +|Property | Description | Default + +| locations | Defines the location(s) of the OpenAPI specification files. May be one file or a directory. E.g. `classpath:org/citrusframework/openapi/petstore/*.json`. Note that when loading more than one OpenAPI via a repository, care must be taken with respect to the context paths of the OpenAPIs. See the context path configuration properties of the repository and the following <<_hooking_the_operation_path>> chapter. | +| rootContextPath | Sets the root context path for the API, which overrides any context path defined within the specification itself. | Use the context path specified in the first server element of the OpenApi. +| validationPolicy | Controls the handling of validation errors. The available options are: + +- `FAIL`: Causes the test to fail when validation errors occur. + +- `REPORT`: Reports validation errors without failing the test. + +- `IGNORE`: Ignores any validation errors during test execution. | REPORT (from OpenApiSettings) +| requestValidationEnabled | Enables or disables validation of requests. | true (from OpenApiSettings) +| responseValidationEnabled | Enables or disables validation of responses. | true (from OpenApiSettings) + +|=== + +=== Hooking the Operation Path + +In OpenAPI, an operation represents a single API endpoint, typically corresponding to an HTTP method (e.g., GET, POST) and a specific path. +The path defines how the client interacts with the API and often includes dynamic parameters. +These paths are defined relative to the API's base URL and are an essential part of structuring API interactions. + +The OpenAPI specification allows defining a server URL, which acts as the root of the API. This URL +may include a basePath, which serves as a prefix for all operation paths. +If more than one server is specified, the first one will be used. +Additionally, applications may define a context path, which can be used to namespace APIs when deployed in different environments. +In the following, this context path is denoted rootContextPath. +It is a user-specified value that can be assigned to an OpenApiRepository or to an OpenApiSpecification itself. + +The final path used for an operation during testing is built based on the OpenAPI specification's `basePath`, the `rootContextPath`. +For most flexibility, Citrus provides several options to configure the final operation path: + +1. **Default Path Construction**: +- By default, the path is constructed using the `basePath` from the OpenAPI specification (if defined) combined with the `operationPath`. +- For example, if the `basePath` is `/v1/petstore` and the operation path is `/pet`, the final path will be: `*/v1/petstore/pet*` + +2. **When `rootContextPath` is Set**: +- If a `rootContextPath` is provided, it will be prepended to the `basePath` (if present) and the `operationPath`. +- or example, if the `basePath` is `/v1/petstore`, the `operationPath` is `/pet`, and the `rootContextPath` is `/api`, the resulting path will be: `*/api/v1/petstore/pet*` + +3. **When `neglectBasePath` is Set**: +- If `neglectBasePath` is set to `true`, the `basePath` is ignored, and only the `operationPath` and `rootContextPath` is used. +- For example, if the `basePath` is `/v1/petstore` and the `operationPath` is `/pet`, and the `rootContextPath` is `/api`, setting `neglectBasePath=true` will result in the path: `*/api/pet*` + +Likewise, if the `rootContextPath` is not set, the resulting path will be the `operationPath` only: `*/pet*` + +These properties allow for flexible configuration of the OpenAPI repository, enabling you to customize how API validation is handled and how the OpenAPI specifications are loaded into the system. + +[[openapi-specification]] +== OpenAPI aliases + +You can refer to registered OpenAPI specification by an alias. +That way, you can register a specification in your preferred bean container and refer to it, rather than instantiating a specification over and over in all your tests. +The specification can then be resolved at runtime when needed. + +The following aliases are derived from the specification. + +1. If the specification has an info element with a title, the title will be assigned as alias: `Swagger Petstore` +2. If the specification has an info element with a version and a title, the title plus version will be assigned as alias: `Swagger Petstore/1.0.1` +3. If the specification is loaded from a `Resource` via an OpenAPIRepository, the resource name without file extension will be added as alias: `petstore-v3` +4. If the specification has an extension named `x-citrus-alias`, the value of this extension will be assigned as alias. +5. For technical reasons, a unique-id will also be added as alias. +This unique-id alias is determined from the document SHA value and the full context path, to which the specification is mounted, making it unique, even if the same API is used at different mount-points. +Note that this unique-id alias is used internally during validation, to identify the OpenAPI specification that relates to a specific message. + +Citrus will try to resolve the specification from a given alias by querying all registered OpenApiRepositories as well as all registered OpenApiSpecifications. + +[[openapi-specification]] +== XML Support + +This is a brief note on the XML support of OpenAPI. Due to the comprehensive <> approach +with Spring support, which covers all aspects of OpenAPI, the plain XML support has not yet been fully +implemented. We recommend using the generated TestAPI with Spring, instead of plain XML for better functionality and coverage. + [[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. +On the client side, Citrus uses the OpenAPI specification to generate the appropriate HTTP request sent to the server. +While you can manually configure every aspect of the message sent by the client, it’s also possible to auto-generate the message from the specification. +In either case, the client will automatically use the correct request path (e.g., `/petstore/v3/pet`) and Content-Type (e.g., `application/json`) based on the specification. -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. +A response is automatically validated against the corresponding response defined in the OpenAPI specification +for the given status code. +You just specify the expected HTTP status code, which must match one defined in the specification (e.g., 200 OK), and validation will be performed automatically. +For more details see <>. As an example the following OpenAPI specification defines the operation `getPetById`. @@ -116,10 +260,12 @@ Pet: # ... ---- -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. +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)) @@ -130,6 +276,9 @@ private final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( @CitrusTest public void openApiClientTest() { + // Define variables to set request parameters. + // Note that the path-param in `petstore-v3.yaml` is named `petId` + variable("petId", "1001"); when(openapi(petstoreSpec) .client(httpClient) .send("getPetById")); @@ -146,6 +295,9 @@ public void openApiClientTest() { + + @@ -185,37 +337,202 @@ actions: ---- - + ---- -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. +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. +It is also possible to reference a given specification by one of its aliases. In the following example, +this is demonstrated through the usage of either an OpenApiRepository or an OpenApiSpecification. Note +that it is sufficient to register the specification using either method. The `openapi` call then accepts +a string argument representing one of the specification's aliases.. -The generated control message contains validation matchers and expressions as follows. +.Java +[source,java,indent=0,role="primary"] -.Generated control message body -[source,json] ---- -{ - "id": "@isNumber()@", - "name": "@notEmpty()@", - "category": { - "id": "@isNumber()@", - "name": "@notEmpty()@" - }, - "photoUrls": "@notEmpty()@", - "tags": "@ignore@", - "status": "@matches(sold|pending|available)@" +private final HttpClient httpClient = new HttpClientBuilder() + .requestUrl("http://localhost:%d".formatted(port)) + .build(); + +@BindToRegistry +private OpenApiRepository openApiRepository = new OpenApiRepository() + .locations(singletonList( + "classpath:org/citrusframework/openapi/petstore/petstore-v3.json")) + .neglectBasePath(true) + .validationPolicy(OpenApiValidationPolicy.REPORT); + +@BindToRegistry +private OpenApiSpecification openApiSpecification = OpenApiSpecification + .from(Resources.create("classpath:org/citrusframework/openapi/petstore/petstore-v3.json"), OpenApiValidationPolicy.REPORT) + .neglectBasePath(true); + +@CitrusTest +public void openApiClientTest() { + variable("petId", "1001"); + when(openapi("petstore-v3") + .client(httpClient) + .send("getPetById")); + + then(openapi("petstore-v3") + .client(httpClient) + .receive("getPetById", HttpStatus.OK)); } ---- -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. +[[openapi-client]] +=== Message Content + +All variables that match the parameters of the operation will be automatically assigned to the constructed method. +For example, in the samples above, the `petId` will be assigned to the corresponding path parameter. +The same applies to `header`, `query`, and `cookie` parameters. Additionally, it is also possible to +specify parameters and body at the message level. + +For example, setting the `verbose` query parameter at the message level in the sample below has the +same effect as specifying a `verbose` variable. + +.Java +[source,java,indent=0,role="primary"] +---- +@CitrusTest +public void openApiClientTest() { + variable("petId", "1001"); + when(openapi("petstore-v3") + .client(httpClient) + .send("getPetById") + .message() + .header("verbose", "true")); + + then(openapi("petstore-v3") + .client(httpClient) + .receive("getPetById", HttpStatus.OK)); +} +---- + +[[openapi-autofill]] +=== Autofill + +As all parameters and the body are defined in the OpenAPI specification, it is possible to autofill +missing values. Autofill works by generating random, schema-conforming values for parameters and the body. +The ability to create random values based on OpenAPI schema definitions has been significantly enhanced +compared to the previous implementations. It now respects constraints such as `min/max` definitions for +numbers, composite patterns like `oneOf`, `anyOf`, and `allOf`, `arrays`, and specific patterns like +`email`, `URI`, `hostname`, `IPv4`, and `IPv6`. Regular expression patterns for strings are properly +generated using the `com.github.mifmif:generex` library. + +Note that random message generation has limitations. For example, in the case of complex schemas containing +nested objects, the random generator currently stops if it encounters the same object type during generation, +in order to avoid infinite recursion. + +There are three autofill modes: + +|=== +|Mode | Description +| `REQUIRED` | Autofills only the required parts of the message, such as required body attributes, and required header, query, cookie, and path parameters. This is the default mode. +| `ALL` | Autofills all parts of the message, including both required and optional parameters and body attributes. +| `NONE` | No autofill is applied. All missing parameters and body must be explicitly provided. +|=== + +The autofill mode is supported at client and server and can be specified at send the send message: + +.Java +[source,java,indent=0,role="primary"] +---- +@CitrusTest +public void openApiClientTest() { + // This request is invalid because no body will be generated + when(openapi("petstore-v3") + .client(httpClient) + .send("addPet") + .autoFill(AutoFillType.NONE)); +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + + + + + + + + +---- + +[[openapi-validation]] +=== Validation + +The foundation of the OpenAPI validation concept is the OpenAPI validator provided by `com.atlassian.oai:swagger-request-validator-core`. +This concept applies to both client-side and server-side implementations, covering both requests and responses. +It includes parameter validation as well as message validation, with the latter being limited to messages +based on schema definitions in the OpenAPI specification + +Since most use cases require sent and received messages to conform to the specification, schema validation +is `enabled by default`. This ensures that parameter values and the body are valid not only in terms +of type but also with respect to other constraints, such as minimum and maximum values, patterns, +composites (`oneOf`, `anyOf`, `allOf`), and other restrictions. + +Unlike the previous validation implementation in Citrus, no explicit control message is involved in the validation. +Technically, the OpenAPI validation is implemented as a Citrus SchemaValidation, similar to JSON and XML +validation. However, you can still use the standard message validation features of Citrus to +validate the explicit content of the body or header parameters. + +If you intentionally want to send or receive invalid data to test the response behavior or error handling +of your service, you can disable the validation as follows: + +.Java +[source,java,indent=0,role="primary"] +---- +@CitrusTest +public void openApiClientTest() { + variable("petId", "invalid-string-as-pet-id"); + + // Although the petId is not an integer, this call will not fail due to disabled schema validation + when(openapi("petstore-v3") + .client(httpClient) + .send("getPetById") + .schemaValidation(false)); + + then(openapi("petstore-v3") + .client(httpClient) + .receive("getPetById", HttpStatus.OK)) + .schemaValidation(false); +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + + + + + + + + + + +---- + +Of course, the resulting HTTP response from the server is also verified against the OpenAPI specification. +Programmatically, verification is configured by providing the `operationId` and the expected `status`. +From this, the expected response is determined and validated against the actual response. + +Response schema validation can also be disabled, as shown in the previous example. This completes the client side OpenAPI support. Now let's have a closer look at the server side OpenAPI support in the next section. @@ -223,8 +540,10 @@ Now let's have a closer look at the server side OpenAPI support in the next sect [[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. +As already mentioned in chapter <>, Citrus is able to verify incoming requests and +outgoing responses, based on the OpenAPI specification. The expected request message content as well +as the expected resource URL path, query, header, cookie parameters and the Content-Type are automatically +validated. .Java [source,java,indent=0,role="primary"] @@ -241,6 +560,9 @@ private final OpenApiSpecification petstoreSpec = OpenApiSpecification.from( @CitrusTest public void openApiClientTest() { + // Define variables to set request parameters. + // Note that the path-param in `petstore-v3.yaml` is named `petId` + variable("petId", "1001"); when(openapi(petstoreSpec) .server(httpServer) .receive("addPet")); @@ -257,6 +579,9 @@ public void openApiClientTest() { + + @@ -302,10 +627,801 @@ actions: 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. +The OpenAPI server validates the incoming message using `com.atlassian.oai:swagger-request-validator-core`. 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`. +Also, the server will verify the HTTP request method, the Content-Type header as well as the used +resource path `/petstore/v3/pet`. For more information check chapter <>. 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). +The server will generate a proper response according to the OpenAPI specification and the autofill mode +described in chapter <>. This also includes a potential response message body (e.g. pet object). + +Note that the OpenAPI specification does not require all possible responses to be defined. Therefore, +a response for a given operation and status code may not always be specified by the OpenAPI specification. +In such cases, Citrus will fail to generate a valid random response, and you will need to specify the response manually. + +In case validation is unwanted, it can always be turned off for server side send and receive. Again, see +<> for details. + +[[openapi-test-api-generator]] +== OpenAPI Test API Generator + +For a deeper integration with a given OpenAPI, Citrus offers the ability to generate a dedicated +TestAPI, providing test actions tailored to the specific operations of the OpenAPI under evaluation. +These actions can be used with both XML and Java DSL. + +Please note the following restrictions: +- Only OpenApiClient send/receive is implemented +- XML integration is only available for Spring Framework + +The TestAPI functionality is provided by the https://github.com/citrusframework/citrus/tree/main/test-api-generator[Citrus OpenAPI TestAPI Generator] +module, which utilizes the link:https://github.com/swagger-api/swagger-codegen/tree/master[OpenAPI Code Generator] +to generate the necessary code. Citrus provides the following modules to build and run the TestAPI code: + +|=== +| Artifact | Purpose + +| `citrus-test-api-core` | Runtime dependencies of Citrus TestAPI feature. +| `citrus-test-api-generator-core` | Citrus specific generator `org.citrusframework.openapi.generator.CitrusJavaCodegen` and required +https://github.com/citrusframework/citrus/tree/main/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus[mustache templates]. +| `citrus-test-api-generator-maven-plugin` | The maven plugin used to build the TestAPI. + +|=== + + +The generator offers the following features: + +* Generation of a TestAPI +** From OpenAPI Specification +** From WSDL +* Integration into Citrus XML test cases: +** Integration into XML editors via dedicated generated XSD for: +*** Schema validation +*** Auto-completion +* Integration into Citrus Java test cases via Java DSL + +Keep in mind that a generated TestAPI not only serves as a powerful tool for calling and validating +the operations of your system under test, but also streamlines access to other supporting libraries +that offer an OpenAPI specification, enhancing the overall integration and testing experience. + +[[openapi-test-api-when-to-use]] +=== When to use a TestAPI + +Using a TestAPI simplifies testing OpenAPI-based services but introduces additional configuration overhead. +It is essential to have a solid understanding of Maven and managing generated code. + +If you're writing multiple tests for a service, investing time in the following chapters will be worthwhile. +However, if you only need a basic smoke test, using plain Citrus features may be the better choice. + +[[openapi-test-api-wsdl]] +=== TestAPI from WSDL + +Citrus also supports the generation of a TestAPI from a WSDL. The WSDL is parsed, and a simple OpenAPI +specification is generated, which includes an OpenAPI operation for each WSDL binding. Using specific +code generation templates, the generator provides WS-specific action builders that can be used in both +Java DSL and XML. All this happens behind the scenes, if you specifiy a WSDL as source and use the +`citrus-test-api-generator-maven-plugin` for code generation. + +Note that there is currently no support for generating random messages. Also, the WSDL is not yet +properly registered with an `XsdSchemaRepository`, nor are the actions configured for schema validation. +Therefore, out of the box validation is currently not supported. + +[[openapi-test-api-files]] +=== Generated Files + +The following directory structure depicts the files that are produced during code generation. Note +that the `Prefix` in folder and file-names is a placeholder for a specific value configured by a +parameter in the build configuration. This value should uniquely identify an API to avoid name clashes. + +.Generated Folder Structure and Files +[source] +---- +target/ +└generated-test-sources/ + └ openapi/ + ├ .openapi-generator/ // OpenApiGenerator Metafiles + ├ docs/ // Standard OpenApiGenerator + │ // documentation + └ src/ + ├ main/ + │ ├ java/ + │ │ └ org/ // default + │ │ └ citrusframework/ // TestAPI + │ │ └ automation/ // package structure + │ │ │ + │ │ └ prefix/ // Api prefix as specified in Maven + │ │ │ // build + │ │ │ + │ │ └ version/ // Optional version as specified in + │ │ │ // Maven build + │ │ ├ api/ + │ │ │ └ PrefixApi.java // The dedicated api action builder + │ │ │ + │ │ ├ model/ // Model classes generated for + │ │ │ ├ TypeA.java // schema components + │ │ │ └ TypeB.java + │ │ ├ spring/ + │ │ │ ├ PrefixBeanConfiguration.java // Spring BeanConfiguration + │ │ │ │ // providing OpenApiSpecification + │ │ │ │ // and PrefixApi bean + │ │ │ │ + │ │ │ └ PrefixNamespaceHandler.java // Spring NamespaceHandler for + │ │ │ // XML integration + │ │ │ + │ │ └ PrefixApi.java // OpenApiSpecification provider + │ └ resources/ + │ ├ META-INF/ // Spring Integration files + │ │ ├ spring.handlers + │ │ └ spring.schemas + │ ├ org/ + │ │ └ citrusframework/ + │ │ └ automation/ + │ │ ├ apiprefix/ + │ │ └ version/ + │ │ └ prefix_openApi.yml // Copy of the OpenAPI specification + │ │ // for validation purposes + │ └ schema/ + │ └ xsd + │ └ prefix-api.xsd // Generated XSD schema for XML + │ // integration + │ + └ test // Unused +---- + +|=== +| File | Content + +| `PrefixApi.java` | The class containing the dedicated TestAPI action builder and actions. +| `TypeA.java, TypeB.java` | Model files generated with respect to the schema components of the OpenAPI. +| `PrefixBeanConfiguration.java` | A Spring @Configuration class, that provides an OpenApiRepository with the Specification and an instance of PrefixApi. +| `PrefixNamespaceHandler.java` | A Spring class, that registers bean definition parsers for TestAPI XML elements. +| `PrefixApi.java` | Provides static access to an instance of the TestAPI OpenAPI specification. +| `spring.handlers` | Spring namespace handler configuration, that contains all NamespaceHandlers for all generated APIs. +| `spring.schemas` | Spring schema definitions, with mappings of namespaces to schemas for all generated APIs. +| `prefix-openApi.yml` | The OpenAPI source that was used to build the TestAPI. +| `prefix-api.xsd` | XSD schema for the integration of the TestAPI into XML. +|=== + +[[openapi-test-api-generator]] +=== Configuration of TestAPI Generation + +Code generation is typically integrated into the build process, and for the `Citrus TestAPI Generator`, +this is accomplished using a Maven or Gradle plugin. While the standard `org.openapitools:openapi-generator-maven-plugin` +can be used for this purpose, configuring it, especially for multiple APIs, can be cumbersome and complex. +However, it is certainly possible, and a sample configuration is available in the +https://github.com/citrusframework/citrus/tree/main/test-api-generator/citrus-test-api-generator-core/pom.xml/[module descriptor]. + +To streamline this process, Citrus provides its own adaptation of the standard generator plugin: the +`Citrus OpenAPI Generator Plugin`. This plugin simplifies TestAPI generation by offering sensible +default configurations and better support for generating multiple APIs. It also enhances integration +with Spring by automatically generating Spring-specific files (`spring.handlers` and `spring.schemas`), +making it easier to integrate the generated APIs into Spring-based applications. + +Given these advantages, the Citrus OpenAPI Generator Plugin is recommended in most scenarios as it +greatly simplifies the configuration process and improves overall flexibility. + +The plugin is thoroughly tested across a wide variety of configurations. For additional details, you +can refer to +https://github.com/citrusframework/citrus/tree/main/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest[these] +sample project build descriptors used for testing. + +The following section provides a configuration example for basic TestAPI generation scenarios: + +.Citrus OpenAPI Generator Plugin - multiple APIs, minimal configuration +[source,xml,indent=0,role="primary"] +---- + + citrus-test-api-generator-maven-plugin + + + + + Multi1 + api/test-api.yml + + + Multi2 + api/test-api.yml + + + Multi3 + api/test-api.yml + + + + + + + create-test-api + + + + + +---- + +.Citrus OpenAPI Generator Plugin - single API full configuration +[source,xml,indent=0,role="secondary"] +---- + + citrus-test-api-generator-maven-plugin + + + + b + otherOption + + + + myschema/xsd + src/main/resources/META-INF + + Full + api/test-api.yml + org.mypackage.%PREFIX%.api + org.mypackage.%PREFIX%.invoker + org.mypackage.%PREFIX%.model + myEndpoint + + "http://company/citrus-test-api/myNamespace" + + v1 + + a=b,c=d + + + generated-sources + generated-resources + + + + + myschema/xsd + + src/main/resource-mod/META-INF-MOD + + + + + create-test-api + + + + +---- + +.Standard OpenAPI Generator Plugin +[source,xml,indent=0,role="secondary"] +---- + + + org.openapitools + openapi-generator-maven-plugin + + + + org.citrusframework + citrus-test-api-generator-core + ${project.version} + + + + + REST + generated-test-resources + generated-test-sources + true + + true + + java-citrus + ${project.build.directory} + + + + generate-openapi-petstore-files + compile + + generate + + + ${project.basedir}/src/test/resources/apis/petstore.yaml + + + org.citrusframework.openapi.generator.rest.petstore + + + org.citrusframework.openapi.generator.rest.petstore.request + + + org.citrusframework.openapi.generator.rest.petstore.model + + PetStore + petStoreEndpoint + + + + + generate-openapi-files-for-soap + compile + + generate + + + + ${project.basedir}/src/test/resources/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookService-generated.yaml + + + SOAP + + org.citrusframework.openapi.generator.soap.bookservice + + + org.citrusframework.openapi.generator.soap.bookservice.request + + + org.citrusframework.openapi.generator.soap.bookservice.model + + OpenApiFromWsdl + soapSampleEndpoint + + + + + +---- + +These are the primary elements you can configure in the `` section: + +|=== +| Configuration element | Maven Property (ns = citrus.test.api.generator) | Description | Default Value + +| `schemaFolder` | `ns.schema.folder` | Location of the generated XSD schemas | `schema/xsd/%VERSION%` +| `metaInfFolder` | `ns.meta.inf.folder` | Location into which spring meta files are generated/updated | `target/generated-test-resources/META-INF` +| `generateSpringIntegrationFiles` | `ns.generate.spring.integration.files` | Specifies whether spring integration files should be generated | `true` + +4+| Nested `` element +| `prefix` | `ns.prefix` | Specifies the prefix used for the TestAPI, typically an acronym. | (no default, **required**) +| `source` | `ns.source` | Specifies the source of the TestAPI. | (no default, **required**) +| `version` | `ns.version` | Specifies the version of the API, may be `null`. | (none) +| `endpoint` | `ns.endpoint` | Specifies the default endpoint name of the TestAPI. | `%PREFIX%Endpoint` +| `type` | `ns.type` | Specifies the type of the TestAPI. | `REST`, other option is `SOAP` +| `useTags` | `ns.use.tags` | Specifies whether the generator should generate an API class per tag name. | `true` +| `invokerPackage` | `ns.invoker.package` | Package for the TestAPI classes. | `org.citrusframework.automation.%PREFIX%.%VERSION%` +| `apiPackage` | `ns.api.package` | Package for the TestAPI interface classes. | `org.citrusframework.automation.%PREFIX%.%VERSION%.api` +| `modelPackage` | `ns.model.package` | Package for the TestAPI model classes. | `org.citrusframework.automation.%PREFIX%.%VERSION%.model` +| `targetXmlnsNamespace` | `ns.namespace` | XML namespace used by the API. | `http://www.citrusframework.org/schema/%VERSION%/%PREFIX%-api` +| Nested `` element 3+| https://openapi-generator.tech/docs/generators/java[OpenAPI Generator Options] +| `resourceFolder` | | Location into which the resources are generated | Dependend on the phase in which the source is generated: +-`target/generated-sources/openapi/src/main/java` +-`target/generated-test-sources/openapi/src/main/java` +| `sourceFolder` | | Location into which the sources are generated | Dependend on the phase in which the source is generated: +-`target/generated-sources/openapi/src/main/resources` +-`target/generated-test-sources/openapi/src/main/resources` +| `` 3+| https://openapi-generator.tech/docs/globals[OpenAPI Generator global Config Options] +|=== + +Note: `%PREFIX%` and `%VERSION%` are placeholders that will be replaced by their specific values as configured. +The plugin performs a conversion to lowercase for `PREFIX` used in package names and in `targetXmlnsNamespace`. + +[[openapi-test-api-generator-run]] +==== Running the generator + +To run the generator, execute the following command in your project directory: + +[source,bash] +---- +mvn citrus-test-api-generator:create-test-api +---- + +This command will generate the classes and XSD files as configured for your APIs in the specified locations. + +[[openapi-test-api-generator-spring-meta]] +==== Spring meta file generation + +The `citrus-test-api-generator-maven-plugin` supports the generation of essential Spring integration +files, namely `spring.handlers` and `spring.schemas`. These files play a crucial role for Spring applications +that use XML configuration. + +The generated Spring integration files provide mappings between custom XML namespaces and their +corresponding namespace handlers and schema locations. This mapping enables Spring to correctly parse +and validate XML configuration files that contain custom elements and attributes, ensuring seamless +integration with your Spring-based application. + +[[openapi-test-api-generator-spring-meta-config]] +===== Configuration + +The `citrus-test-api-generator-maven-plugin` generates the Spring integration files based on the +configuration provided in the `citrus-test-api-generator-maven-plugin` section of the pom.xml file. +For each API defined, the plugin generates entries in the `spring.handlers` and `spring.schemas` files, +mapping XML namespaces to their respective handlers and schema locations. + +===== Meta File Update Process + +If you are running your TestAPI alongside a non-generated API and need to modify the existing +`spring.handlers` and `spring.schemas` files from your non-generated source code, you should point +the metaInfoFolder to the location of your existing META-INF folder (e.g., src/test/resources/META-INF). +This ensures that the plugin updates the existing files without overwriting any content. + +To distinguish the generated schemas from the non-generated ones during the metafile update process, +the plugin checks for namespace URLs containing the segment `citrus-test-schemas`. When updating the +files, all schemas that match this segment will be removed, while the other schemas will be preserved. +After that, the plugin will add the namespaces for the generated TestAPI according to the configuration. + +===== Usage + +Once generated, the `spring.handlers` and `spring.schemas` files, along with any existing +non-generated content, should be included in the classpath of your Spring application. +During runtime, Spring will pick up and use these files to resolve custom XML namespaces and handle elements +accordingly. This automatically happens if one of the following folders is chosen: + +- target/generated-sources/openapi/src/main/resources/META-INF +- target/generated-test-sources/openapi/src/main/resources/META-INF (`default`) +- src/main/resources/META-INF - for mixing existing meta files with generated +- src/test/resources/META-INF - for mixing existing meta files with generated + +For the directories listed above, the resources folder is included in the classpath by default, depending +on whether tests are executed. For other directories, the Citrus TestAPI generator plugin automatically +adds them to the classpath. + +If your IDE fails to resolve the files, you may need to manually configure the directories as source +or resource folders. Additionally, consider adding a Maven build step to ensure these folders are handled +correctly during the build process. + +==== Configuration of the Classpath for using TestAPI + +In case you encounter issues with the classpath when running your TestAPI, you may want to manually +configure the classpath to contain the generated sources/resources. You can use the +link:https://www.mojohaus.org/build-helper-maven-plugin/usage.html[build-helper-maven-plugin] plugin to do so. +Explicit definition of these classpath entries should solve all related issues. + +.Configuration of `build-helper-maven-plugin` +[source,xml] +---- + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-test-sources + generate-test-sources + + add-test-source + + + + ${project.build.directory}/generated-test-sources/openapi/src/main/java + + + + + add-test-resource + generate-test-resources + + add-test-resource + + + + + ${project.build.directory}/generated-test-sources/openapi/src/main/resources + + + + + + + + +---- + +==== Sample usage + +To utilize the TestAPI in XML, it's necessary to import the respective namespace. +Once imported, requests can be directly employed as actions, as illustrated in the sample below. +Further examples can be found here `org.citrusframework.openapi.generator.GeneratedApiIT`. + +.Java +[source,java,indent=0,role="primary"] +---- +@ExtendWith(CitrusSpringExtension.class) +@SpringBootTest(classes = {PetStoreBeanConfiguration.class, CitrusSpringConfig.class}) +class GetPetByIdTest { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private PetApi petApi; + + @Test + @CitrusTest + void testByJsonPath(@CitrusResource TestCaseRunner runner) { + runner.when(petApi.sendGetPetById(1234L)); + runner.then(petApi.receiveGetPetById(OK)); + } +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + + + + + +---- + +To use the TestAPI in Java, you need to import the relevant API configuration, which provides the necessary request actions. +In the example above, this configuration is named PetStoreBeanConfiguration. +Once imported, you can autowire the API and use its builder methods to create dedicated actions for your operations. + +*Type-Safe Action Builders* + +The Java DSL offers type-safe methods for all required parameters. +For example, the `getPetById` operation requires a valid petId of type long, as shown in the sample. +This ensures that all parameters are correctly typed, providing compile-time validation and reducing the risk of errors. + +*Dynamic Content with String Expressions* + +In addition to type-safe builder methods, there is another version of each action that allows you to +pass string expressions, which are dynamically resolved by Citrus at runtime. +These methods have the same name as their type-safe counterparts, but they end with a `$`. +For instance, if the type-safe method is getPetById(long petId), the dynamic method would be getPetById$(String petId). + +The reason for the $ in the method name is related to the underlying code generation mechanism, which facilitates dynamic content substitution during runtime. + +*Example: Type-Safe vs Dynamic Method* +Both method calls achieve the same outcome, but one provides type safety at compile-time, while the other allows for dynamic content resolution at runtime: + +- *Type-Safe:* getPetById(123L); +- *Dynamic:* getPetById$("123"); + +In both cases, the correct action is created, but the former ensures type correctness at compile time, while the latter allows flexibility with dynamic values. + + +==== OpenAPI Default Endpoint + +It is possible to specify a default endpoint for a generated TestAPI. +The endpoint needs to be registered as bean and will be resolved when needed. +The name of the endpoint can be specified as configuration parameter in the <> config options. +If not specified a default name is derived from the TestAPI prefix as follows: + +Endpointname: `prefixEndpoint` + +Because of the default endpoint option, it is not required to specify the endpoint in the action builder. +If omitted, the endpoint will be resolved at runtime. +Failure in specification of a default endpoint will result in an exception at runtime. + +The following shows an example of how to specify two endpoints for the same server: + +.Java +[source,java,indent=0,role="primary"] +---- +@Bean(name = {"petstoreEndpoint", "extpetstoreEndpoint"}) +public HttpClient applicationServiceClient() { + return new HttpClientBuilder() + .requestUrl("http://localhost:8080") + .handleCookies(true) + .build(); +} +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + + +---- + +==== OpenAPI Security + +An OpenAPI may contain security specifications which can be referenced by operations. +Several schemes exist, of which Citrus currently supports: + +- Basic Authentication +- Bearer Authentication +- Api Key Authentication + +The following snippet shows the definition of these security schemes in an OpenAPI. + +.Yaml +[source,yaml,indent=0,role="primary"] +---- +openapi: 3.0.2 +info: +title: Extended Petstore API +description: "This API extends the standard Petstore API. Although the operations\ +\ may not be meaningful in\na real-world context, they are designed to showcase\ +\ various advanced OpenAPI features that \nare not present in the standard Petstore\ +\ API.\n" +version: 1.0.0 +servers: +- url: http://localhost:9000/api/v3/ext +.... +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + api_key_header: + type: apiKey + description: Header api key description + name: api_key_header + in: header + api_key_cookie: + type: apiKey + description: Cookie api key description + name: api_key_cookie + in: cookie + api_key_query: + type: apiKey + description: Query api key description + name: api_key_query + in: query +---- + +===== Basic Authentication + +Citrus supports Basic Authentication by specifying the following properties per authenticated TestAPI: + +.Properties +[source,properties,indent=0,role="primary"] +---- +extpetstore.basic.username=extUser +extpetstore.basic.password=extPassword +---- + +The properties must be prefixed with the API prefix in lower case. + +If present, these values will automatically be added as authorization headers to each call of relevant operations. + +In addition, it is possible to set the values on the operation action builder, possibly overwriting the above defaults: + +.Java +[source,java,indent=0,role="primary"] +---- + runner.when(extPetApi + .sendGetPetByIdWithBasicAuthentication$("${petId}", "true") + .basicAuthUsername("admin") + .basicAuthPassword("topSecret") + .fork(true)); +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + +---- + +===== Bearer Authentication + +For bearer authentication, a bearer token may be specified using the following property: + +.Properties +[source,properties,indent=0,role="primary"] +---- +extpetstore.bearer.token=defaultBearerToken +---- + +The property must be prefixed with the API prefix in lower case. + +If present, this value will automatically be added as `Authorization Bearer Header` to each call of relevant operations. + +In addition, it is possible to set this value on the operation action builder, possibly overwriting the above default: + +.Java +[source,java,indent=0,role="primary"] +---- + runner.when(extPetApi + .sendGetPetByIdWithBasicAuthentication$("${petId}", "true") + .basicAuthBearer("bearerToken") + .fork(true)); +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + +---- + +==== API Key Authentication + +Citrus supports API Key Authentication by specifying the following properties per authenticated TestAPI: + +.Properties +[source,properties,indent=0,role="primary"] +---- +# Whether the api key should be Base64 encoded or not +extpetstore.base64-encode-api-key=true +extpetstore.api-key-query=defaultTopSecretQueryApiKey +extpetstore.api-key-header=defaultTopSecretHeaderApiKey +extpetstore.api-key-cookie=defaultTopSecretCookieApiKey +---- + +The properties must be prefixed with the API prefix in lower case. + +If present, these values will automatically be added as a query, header or cookie to each call of relevant operations. + +In addition, it is possible to set the values on the operation action builder, possibly overwriting the above defaults: + +.Java +[source,java,indent=0,role="primary"] +---- +runner.when(extPetApi + .sendGetPetByIdWithApiKeyAuthentication$("${petId}", "false") + .apiKeyHeader("TopSecretHeader") + .apiKeyCookie("TopSecretCookie") + .apiKeyQuery("TopSecretQuery") + .fork(true)); +---- + +Note that only one type of parameter (query, header or cookie) should be specified in a real world scenario. + +.XML +[source,xml,indent=0,role="secondary"] +---- + +---- + + +=== TestAPI Configuration and Usage Samples + +The Citrus TestAPI module includes numerous tests that serve as excellent starting points. + +- The https://github.com/citrusframework/citrus/tree/main/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest[TestApiGeneratorMojoIntegrationTest] +folder contains various Maven configurations for specifcation of TestAPI generation. + +- The https://github.com/citrusframework/citrus/tree/main/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/GeneratedRestApiIT.java[GeneratedRestApiIT] +is the primary integration test for the Citrus TestAPI in a REST environment. + +It includes over 100 tests written in both `Java DSL` and `XML`, covering all aspects of the generated TestAPI. + +The https://github.com/citrusframework/citrus/tree/main/test-api-generator/citrus-test-api-generator-core/src/test/resources/apis/petstore-extended-v3.yaml[ExtendedPetAPI] +provides a range of specific operations for testing, all of which are thoroughly covered in this test. + +- For SOAP integration, the https://github.com/citrusframework/citrus/tree/main/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/GeneratedSoapApiIT.java[GeneratedSoapApiIT] +serves as the main integration test for Citrus TestAPI in a SOAP environment. diff --git a/src/manual/index.adoc b/src/manual/index.adoc index 1666180857..84b6029c74 100644 --- a/src/manual/index.adoc +++ b/src/manual/index.adoc @@ -61,6 +61,8 @@ include::endpoint-restdocs.adoc[] include::endpoint-component.adoc[] include::endpoint-adapter.adoc[] +include::testapi.adoc[] + include::connectors.adoc[] include::connector-openapi.adoc[] include::connector-jbang.adoc[] diff --git a/test-api-generator/citrus-test-api-core/pom.xml b/test-api-generator/citrus-test-api-core/pom.xml new file mode 100644 index 0000000000..c8d9abe1e2 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + + citrus-test-api-generator + org.citrusframework + 4.6.0-SNAPSHOT + ../pom.xml + + + citrus-test-api-core + Citrus :: Test API Core + Citrus Test API Core + jar + + + + org.citrusframework + citrus-api + ${project.version} + + + org.citrusframework + citrus-http + ${project.version} + + + org.citrusframework + citrus-openapi + ${project.version} + + + org.citrusframework + citrus-spring + ${project.version} + + + org.citrusframework + citrus-ws + ${project.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + org.openapitools + jackson-databind-nullable + + + org.openapitools + openapi-generator + ${org.openapitools.version} + + + wsdl4j + wsdl4j + + + + + org.citrusframework + citrus-junit5 + ${project.version} + test + + + org.citrusframework + citrus-validation-json + ${project.version} + test + + + commons-fileupload + commons-fileupload + 1.5 + test + + + diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/ApiActionBuilderCustomizer.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/ApiActionBuilderCustomizer.java new file mode 100644 index 0000000000..534317aff3 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/ApiActionBuilderCustomizer.java @@ -0,0 +1,33 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi; + +import org.citrusframework.actions.ReceiveMessageAction.ReceiveMessageActionBuilder; +import org.citrusframework.actions.SendMessageAction.SendMessageActionBuilder; + +/** + * Implementors of this interface are used to customize the SendMessageActionBuilder with application specific information. E.g. cookies + * or transactionIds. + */ +public interface ApiActionBuilderCustomizer { + + default > void customizeRequestBuilder(GeneratedApiOperationInfo generatedApiOperationInfo, T builder) { + } + + default > void customizeResponseBuilder(GeneratedApiOperationInfo generatedApiOperationInfo, T builder) { + } +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/GeneratedApi.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/GeneratedApi.java new file mode 100644 index 0000000000..65d68a9b0e --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/GeneratedApi.java @@ -0,0 +1,70 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi; + +import org.citrusframework.endpoint.Endpoint; + +import java.util.List; +import java.util.Map; + +/** + * Interface representing a generated API from an OpenAPI specification. + * Provides methods to retrieve metadata about the API such as title, version, + * prefix, and information extensions. + */ +public interface GeneratedApi { + + /** + * Retrieves the title of the OpenAPI specification, as specified in the info section of the API. + * + * @return the title of the OpenAPI specification + */ + String getApiTitle(); + + /** + * Retrieves the version of the OpenAPI specification, as specified in the info section of the API. + * + * @return the version of the OpenAPI specification + */ + String getApiVersion(); + + /** + * Retrieves the prefix used for the API, as specified in the API generation configuration. + * + * @return the prefix used for the API + */ + String getApiPrefix(); + + /** + * Retrieves the specification extensions of the OpenAPI defined in the "info" section. + *

      + * Specification extensions, also known as vendor extensions, are custom key-value pairs used to describe extra + * functionality not covered by the standard OpenAPI Specification. These properties start with "x-". + * This method collects only the extensions defined in the "info" section of the API. + * + * @return a map containing the specification extensions defined in the "info" section of the API, + * where keys are extension names and values are extension values + */ + Map getApiInfoExtensions(); + + List getCustomizers(); + + /** + * Returns the endpoint of the generated api. + */ + Endpoint getEndpoint(); +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/GeneratedApiOperationInfo.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/GeneratedApiOperationInfo.java new file mode 100644 index 0000000000..c234611e89 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/GeneratedApiOperationInfo.java @@ -0,0 +1,52 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi; + +/** + * Interface representing information about a generated API operation as defined in an OpenAPI + * specification. It provides methods to access metadata related to the operation, including the + * operation name, HTTP method, and request path. + */ +public interface GeneratedApiOperationInfo { + + /** + * Retrieves the {@link GeneratedApi} which owns this operation. + * @return + */ + GeneratedApi getGeneratedApi(); + + /** + * Retrieves the name of the OpenAPI operation. + * + * @return the name of the OpenAPI operation + */ + String getOperationName(); + + /** + * Retrieves the HTTP method corresponding to the operation. + * + * @return the HTTP method specified in the OpenAPI operation + */ + String getMethod(); + + /** + * Retrieves the path corresponding to the operation. + * + * @return the path of the OpenAPI operation + */ + String getPath(); +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/OpenApiParameterFormatter.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/OpenApiParameterFormatter.java new file mode 100644 index 0000000000..9a4f2d3b9e --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/OpenApiParameterFormatter.java @@ -0,0 +1,231 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.citrusframework.exceptions.CitrusRuntimeException; + +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.joining; +import static org.citrusframework.openapi.testapi.ParameterStyle.DEEPOBJECT; +import static org.citrusframework.openapi.testapi.ParameterStyle.NONE; + +class OpenApiParameterFormatter { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final FormatParameters DEFAULT_FORMAT_PARAMETERS = new FormatParameters("", ","); + private static final FormatParameters DEFAULT_LABEL_FORMAT_PARAMETERS = new FormatParameters(".", ","); + private static final FormatParameters DEFAULT_LABEL_EXPLODED_PARAMETERS = new FormatParameters(".", "."); + + private OpenApiParameterFormatter() { + // Static access only. + } + + /** + * Formats a list of values as a single String based on the separator and other settings. + */ + static String formatArray(String parameterName, + Object parameterValue, + ParameterStyle parameterStyle, + boolean explode, + boolean isObject) { + List values = toList(parameterValue, isObject); + + if (NONE.equals(parameterStyle)) { + return parameterName + "="+ (parameterValue != null ? parameterValue.toString() : null); + } + + if (DEEPOBJECT.equals(parameterStyle)) { + return formatDeepObject(parameterName, values); + } + + FormatParameters formatParameters = determineFormatParameters(parameterName, parameterStyle, explode, isObject); + + if (isObject && explode) { + return formatParameters.prefix + explode(values, formatParameters.separator); + } else { + return formatParameters.prefix + values.stream() + .collect(joining(formatParameters.separator)); + } + } + + private static String formatDeepObject(String parameterName, List values) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < values.size(); i += 2) { + String key = values.get(i); + String value = values.get(i + 1); + builder.append("%s[%s]=%s".formatted(parameterName, key, value)); + builder.append("&"); + } + + if (!builder.isEmpty()) { + builder.deleteCharAt(builder.length() - 1); + } + + return builder.toString(); + } + + private static FormatParameters determineFormatParameters(String parameterName, + ParameterStyle parameterStyle, + boolean explode, + boolean isObject) { + return switch (parameterStyle) { + case MATRIX -> matrixFormatParameters(parameterName, explode, isObject); + case LABEL -> labelFormatParameters(explode); + case FORM -> formFormatParameters(parameterName, explode, isObject); + case NONE, SIMPLE, DEEPOBJECT -> DEFAULT_FORMAT_PARAMETERS; + }; + } + + private static FormatParameters formFormatParameters(String parameterName, boolean explode, boolean isObject) { + if (explode) { + if (isObject) { + return new FormatParameters("", "&"); + } + return new FormatParameters(parameterName + "=", "&" + parameterName + "="); + } else { + return new FormatParameters(parameterName + "=", ","); + } + } + + private static FormatParameters labelFormatParameters(boolean explode) { + return explode ? DEFAULT_LABEL_EXPLODED_PARAMETERS : DEFAULT_LABEL_FORMAT_PARAMETERS; + } + + private static FormatParameters matrixFormatParameters(String parameterName, boolean explode, boolean isObject) { + String prefix; + String separator = ","; + if (explode) { + if (isObject) { + prefix = ";"; + } else { + prefix = ";" + parameterName + "="; + } + separator = prefix; + } else { + prefix = ";" + parameterName + "="; + } + + return new FormatParameters(prefix, separator); + } + + private static String explode(List values, String delimiter) { + return IntStream.range(0, values.size() / 2) + .mapToObj(i -> values.get(2 * i) + "=" + values.get(2 * i + 1)) + .collect(joining(delimiter)); + } + + private static List toList(Object value, boolean isObject) { + if (value == null) { + return emptyList(); + } + + if (value.getClass().isArray()) { + List list = new ArrayList<>(); + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + Object singleValue = Array.get(value, i); + list.add(singleValue.toString()); + } + return list; + } else if (value instanceof List list) { + return list.stream().map(Object::toString).toList(); + } else if (value instanceof Map map) { + return map.entrySet().stream() + .flatMap(entry -> Stream.of(entry.getKey().toString(), entry.getValue().toString())) + .toList(); + } else if (isObject && value instanceof String jsonString) { + return toList(convertJsonToMap(jsonString), true); + } else if (isObject) { + return toList(convertBeanToMap(value), true); + } else { + return singletonList(value.toString()); + } + } + + public static Map convertJsonToMap(String jsonString) { + JsonNode rootNode; + try { + rootNode = objectMapper.readTree(jsonString); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Unable to convert jsonString to JSON object.", e); + } + + if (!rootNode.isObject()) { + throw new IllegalArgumentException("The provided string is not a valid JSON object."); + } + + return convertNodeToMap((ObjectNode) rootNode); + } + + private static Map convertNodeToMap(ObjectNode objectNode) { + Map resultMap = new TreeMap<>(); + + Iterator> fields = objectNode.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + JsonNode valueNode = field.getValue(); + + if (valueNode.isObject() || valueNode.isArray()) { + throw new IllegalArgumentException( + "Nested objects or arrays are not allowed in the JSON."); + } + resultMap.put(field.getKey(), valueNode.asText()); + } + + return resultMap; + } + + protected static Map convertBeanToMap(Object bean) { + Map map = new TreeMap<>(); + try { + for (PropertyDescriptor propertyDescriptor : Introspector.getBeanInfo( + bean.getClass(), Object.class).getPropertyDescriptors()) { + String propertyName = propertyDescriptor.getName(); + Object propertyValue = propertyDescriptor.getReadMethod().invoke(bean); + if (propertyValue != null) { + map.put(propertyName, propertyValue); + } + } + } catch (IntrospectionException | IllegalAccessException | InvocationTargetException e) { + throw new CitrusRuntimeException("Error converting bean to map: " + e.getMessage(), e); + } + return map; + } + + private record FormatParameters(String prefix, String separator) { + } +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/ParameterStyle.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/ParameterStyle.java new file mode 100644 index 0000000000..a74e1131b6 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/ParameterStyle.java @@ -0,0 +1,26 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi; + +public enum ParameterStyle { + NONE, + SIMPLE, + LABEL, + MATRIX, + FORM, + DEEPOBJECT +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/RestApiReceiveMessageActionBuilder.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/RestApiReceiveMessageActionBuilder.java new file mode 100644 index 0000000000..44379e0094 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/RestApiReceiveMessageActionBuilder.java @@ -0,0 +1,84 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi; + +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.actions.OpenApiClientResponseActionBuilder; +import org.citrusframework.openapi.actions.OpenApiSpecificationSource; + +import java.util.List; + +import static java.lang.String.format; +import static org.citrusframework.openapi.util.OpenApiUtils.createFullPathOperationIdentifier; + +public class RestApiReceiveMessageActionBuilder extends OpenApiClientResponseActionBuilder { + + private final GeneratedApi generatedApi; + + private final List customizers; + + public RestApiReceiveMessageActionBuilder(GeneratedApi generatedApi, + OpenApiSpecification openApiSpec, + String method, + String path, + String operationName, + String statusCode) { + super(new OpenApiSpecificationSource(openApiSpec), createFullPathOperationIdentifier(method, path), statusCode); + + this.generatedApi = generatedApi; + this.customizers = generatedApi.getCustomizers(); + + name(format("receive-%s", operationName)); + + } + + public RestApiReceiveMessageActionBuilder(GeneratedApi generatedApi, + OpenApiSpecification openApiSpec, + OpenApiClientResponseMessageBuilder messageBuilder, + HttpMessage httpMessage, + String method, + String path, + String operationName) { + super(new OpenApiSpecificationSource(openApiSpec), messageBuilder, httpMessage, createFullPathOperationIdentifier(method, path)); + + this.generatedApi = generatedApi; + this.customizers = generatedApi.getCustomizers(); + + name(format("receive-%s", operationName)); + } + + public GeneratedApi getGeneratedApi() { + return generatedApi; + } + + public List getCustomizers() { + return customizers; + } + + @Override + public ReceiveMessageAction doBuild() { + + // If no endpoint was set explicitly, use the default endpoint given by api + if (getEndpoint() == null && getEndpointUri() == null) { + endpoint(generatedApi.getEndpoint()); + } + + return super.doBuild(); + } +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/RestApiSendMessageActionBuilder.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/RestApiSendMessageActionBuilder.java new file mode 100644 index 0000000000..6aa56a26c4 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/RestApiSendMessageActionBuilder.java @@ -0,0 +1,314 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi; + +import static java.lang.String.format; +import static java.net.URLEncoder.encode; +import static org.citrusframework.openapi.testapi.OpenApiParameterFormatter.formatArray; +import static org.citrusframework.openapi.util.OpenApiUtils.createFullPathOperationIdentifier; +import static org.citrusframework.util.FileUtils.getDefaultCharset; +import static org.citrusframework.util.StringUtils.isEmpty; + +import jakarta.servlet.http.Cookie; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import org.citrusframework.actions.SendMessageAction; +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.message.Message; +import org.citrusframework.openapi.AutoFillType; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.actions.OpenApiClientRequestActionBuilder; +import org.citrusframework.openapi.actions.OpenApiSpecificationSource; +import org.citrusframework.spi.Resource; +import org.citrusframework.spi.Resources.ClasspathResource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +public class RestApiSendMessageActionBuilder extends OpenApiClientRequestActionBuilder { + + private final GeneratedApi generatedApi; + + private final List customizers; + + private final MultiValueMap formParameters = new LinkedMultiValueMap<>(); + + public RestApiSendMessageActionBuilder(GeneratedApi generatedApi, + OpenApiSpecification openApiSpec, + String method, + String path, + String operationName) { + this(generatedApi, openApiSpec, new HttpMessage(), method, path, operationName); + } + + public RestApiSendMessageActionBuilder(GeneratedApi generatedApi, + OpenApiSpecification openApiSpec, + HttpMessage httpMessage, String method, + String path, String operationName) { + this(generatedApi, openApiSpec, + new TestApiClientRequestMessageBuilder(httpMessage, + new OpenApiSpecificationSource(openApiSpec), + createFullPathOperationIdentifier(method, path)), + httpMessage, + method, + path, + operationName); + } + + public RestApiSendMessageActionBuilder(GeneratedApi generatedApi, + OpenApiSpecification openApiSpec, + TestApiClientRequestMessageBuilder messageBuilder, + HttpMessage httpMessage, + String method, + String path, + String operationName) { + super(new OpenApiSpecificationSource(openApiSpec), messageBuilder, httpMessage, createFullPathOperationIdentifier(method, path)); + + this.generatedApi = generatedApi; + this.customizers = generatedApi.getCustomizers(); + + httpMessage.path(path); + + name(format("send-%s:%s", generatedApi.getClass().getSimpleName().toLowerCase(), operationName)); + + getMessageBuilderSupport().header("citrus_open_api_operation_name", operationName); + getMessageBuilderSupport().header("citrus_open_api_method", method); + getMessageBuilderSupport().header("citrus_open_api_path", path); + } + + public GeneratedApi getGeneratedApi() { + return generatedApi; + } + + public List getCustomizers() { + return customizers; + } + + @Override + public final HttpClientRequestActionBuilder name(String name) { + return super.name(name); + } + + protected void pathParameter(String name, Object value, ParameterStyle parameterStyle, boolean explode, boolean isObject) { + ((TestApiClientRequestMessageBuilder) getMessageBuilderSupport().getMessageBuilder()).pathParameter(name, value, parameterStyle, explode, isObject); + } + + protected void formParameter(String name, Object value) { + setFormParameter(name, value); + } + + protected void setFormParameter(String name, Object value) { + if (value == null || isEmpty(value.toString())) { + return; + } + + setParameter(formParameters::add, name, value); + } + + protected void queryParameter(final String name, Object value) { + if (value == null || isEmpty(value.toString())) { + return; + } + + setParameter((paramName, paramValue) -> super.queryParam( + paramName.replace(" ", "%20"), + paramValue != null ? paramValue.toString() : null), name, value); + } + + protected void queryParameter(final String name, Object value, ParameterStyle parameterStyle, boolean explode, boolean isObject) { + if (value == null || isEmpty(value.toString())) { + return; + } + + String formatted = formatArray(name, value, parameterStyle, explode, isObject); + String[] queryParamValues = formatted.split("&"); + for (String queryParamValue : queryParamValues) { + String[] keyValue = queryParamValue.split("="); + queryParameter(keyValue[0], keyValue[1]); + } + } + + protected void headerParameter(String name, Object value) { + if (value == null || isEmpty(value.toString())) { + return; + } + + setParameter((paramName, paramValue) -> getMessageBuilderSupport().header(paramName, paramValue), name, value); + } + + protected void headerParameter(String name, Object value, ParameterStyle parameterStyle, boolean explode, boolean isObject) { + if (value == null || isEmpty(value.toString())) { + return; + } + + headerParameter(name, formatArray(name, value, parameterStyle, explode, isObject)); + } + + protected void cookieParameter(String name, Object value) { + if (value == null || isEmpty(value.toString())) { + return; + } + + setParameter((paramName, paramValue) -> getMessageBuilderSupport().cookie((new Cookie(paramName, paramValue != null ? paramValue.toString() : null))), name, value); + } + + protected void cookieParameter(String name, Object value, ParameterStyle parameterStyle, boolean explode, boolean isObject) { + if (value == null || isEmpty(value.toString())) { + return; + } + + String formatted = formatArray(name, value, parameterStyle, explode, isObject); + String[] keyValue = formatted.split("="); + + // URL Encoding is mandatory especially in the case of multiple values, as multiple values + // are separated by a comma and a comma is not a valid character in cookies. + cookieParameter(keyValue[0], keyValue[1]); + } + + private void setParameter(BiConsumer parameterConsumer, final String parameterName, Object parameterValue) { + if (parameterValue != null) { + if (byte[].class.isAssignableFrom(parameterValue.getClass())) { + // Pass through byte array + parameterConsumer.accept(parameterName, parameterValue); + } else if (parameterValue.getClass().isArray()) { + int length = Array.getLength(parameterValue); + for (int i = 0; i < length; i++) { + Object singleValue = Array.get(parameterValue, i); + parameterConsumer.accept(parameterName, singleValue); + } + } else if (parameterValue instanceof Collection collection) { + collection.forEach( + singleValue -> parameterConsumer.accept(parameterName, singleValue)); + } else { + parameterConsumer.accept(parameterName, parameterValue); + } + } + } + + @Override + public SendMessageAction doBuild() { + + // If no endpoint was set explicitly, use the default endpoint given by api + if (getEndpoint() == null && getEndpointUri() == null) { + if (generatedApi.getEndpoint() == null) { + throw new CitrusRuntimeException("No endpoint specified for action!"); + } + endpoint(generatedApi.getEndpoint()); + } + + if (!formParameters.isEmpty()) { + getMessageBuilderSupport().body(formParameters); + } + + return super.doBuild(); + } + + protected Object toBinary(Object object) { + if (object instanceof byte[] bytes) { + return bytes; + } else if (object instanceof Resource resource) { + return new ClasspathResource(resource.getLocation()); + }else if (object instanceof org.springframework.core.io.Resource resource) { + return resource; + } else if (object instanceof String location) { + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + return resolver.getResource(location); + } + + throw new IllegalArgumentException( + "Cannot convert object to binary. Only byte[], Resource, and String are supported: " + + object.getClass()); + } + + protected String getOrDefault(String value, String defaultValue, boolean base64Encode) { + if (isEmpty(value) && isEmpty(defaultValue)) { + return null; + } + + if (isEmpty(value)) { + value = defaultValue; + } + + if (base64Encode) { + value = "citrus:encodeBase64('" + value + "')"; + } + + return value; + } + + public static final class TestApiClientRequestMessageBuilder extends + OpenApiClientRequestMessageBuilder { + + private final Map pathParameters = new HashMap<>(); + + public TestApiClientRequestMessageBuilder(HttpMessage httpMessage, + OpenApiSpecificationSource openApiSpec, + String operationId) { + super(httpMessage, openApiSpec, operationId); + // Disable autofill by default, as the api enforces required parameters. + autoFill(AutoFillType.NONE); + } + + private static void encodeArrayStyleCookies(HttpMessage message) { + if (message.getCookies() != null && !message.getCookies().isEmpty()) { + for (Cookie cookie : message.getCookies()) { + if (cookie.getValue().contains(",")) { + cookie.setValue(encode(cookie.getValue(), getDefaultCharset())); + } + } + } + } + + public void pathParameter(String name, Object value, ParameterStyle parameterStyle, boolean explode, boolean isObject) { + if (value == null) { + throw new CitrusRuntimeException( + "Mandatory path parameter '%s' must not be null".formatted(name)); + } + + pathParameters.put(name, new ParameterData(name, value, parameterStyle, explode, isObject)); + } + + @Override + protected String getDefinedPathParameter(TestContext context, String name) { + ParameterData parameterData = pathParameters.get(name); + String formatted = name; + if (parameterData != null) { + formatted = formatArray(name, parameterData.value, parameterData.parameterStyle, + parameterData.explode, parameterData.isObject); + } + + return context.replaceDynamicContentInString(formatted); + } + + @Override + public Message build(TestContext context, String messageType) { + HttpMessage message = (HttpMessage) super.build(context, messageType); + encodeArrayStyleCookies(message); + return message; + } + } + + public record ParameterData(String name, Object value, ParameterStyle parameterStyle, boolean explode, boolean isObject) { + } +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/SoapApiReceiveMessageActionBuilder.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/SoapApiReceiveMessageActionBuilder.java new file mode 100644 index 0000000000..d466e562c9 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/SoapApiReceiveMessageActionBuilder.java @@ -0,0 +1,60 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi; + +import org.citrusframework.ws.actions.ReceiveSoapMessageAction; + +import java.util.List; + +import static java.lang.String.format; + +public class SoapApiReceiveMessageActionBuilder extends ReceiveSoapMessageAction.Builder { + + private final GeneratedApi generatedApi; + + private final List customizers; + + public SoapApiReceiveMessageActionBuilder(GeneratedApi generatedApi, String soapAction) { + super(); + + this.generatedApi = generatedApi; + this.customizers = generatedApi.getCustomizers(); + + endpoint(generatedApi.getEndpoint()); + + name(format("receive-%s:%s", generatedApi.getClass().getSimpleName().toLowerCase(), soapAction)); + } + + public GeneratedApi getGeneratedApi() { + return generatedApi; + } + + public List getCustomizers() { + return customizers; + } + + @Override + public ReceiveSoapMessageAction doBuild() { + + // If no endpoint was set explicitly, use the default endpoint given by api + if (getEndpoint() == null && getEndpointUri() == null) { + endpoint(generatedApi.getEndpoint()); + } + + return new ReceiveSoapMessageAction(this); + } +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/SoapApiSendMessageActionBuilder.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/SoapApiSendMessageActionBuilder.java new file mode 100644 index 0000000000..c3cdc19539 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/SoapApiSendMessageActionBuilder.java @@ -0,0 +1,60 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi; + +import static java.lang.String.format; + +import java.util.List; +import org.citrusframework.ws.actions.SendSoapMessageAction; +import org.citrusframework.ws.actions.SendSoapMessageAction.Builder; + +public class SoapApiSendMessageActionBuilder extends Builder { + + private final GeneratedApi generatedApi; + + private final List customizers; + + public SoapApiSendMessageActionBuilder(GeneratedApi generatedApi, String soapAction) { + super(); + + this.generatedApi = generatedApi; + this.customizers = generatedApi.getCustomizers(); + + getMessageBuilderSupport().soapAction(soapAction); + + name(format("send-%s:%s", generatedApi.getClass().getSimpleName().toLowerCase(),soapAction)); + } + + public GeneratedApi getGeneratedApi() { + return generatedApi; + } + + public List getCustomizers() { + return customizers; + } + + @Override + public SendSoapMessageAction doBuild() { + + // If no endpoint was set explicitly, use the default endpoint given by api + if (getEndpoint() == null && getEndpointUri() == null) { + endpoint(generatedApi.getEndpoint()); + } + + return super.doBuild(); + } +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/TestApiUtils.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/TestApiUtils.java new file mode 100644 index 0000000000..2a81248909 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/TestApiUtils.java @@ -0,0 +1,48 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi; + +import org.citrusframework.http.actions.HttpClientRequestActionBuilder.HttpMessageBuilderSupport; + +import static org.citrusframework.util.StringUtils.isEmpty; + +public final class TestApiUtils { + + private TestApiUtils() { + //prevent instantiation of utility class + } + + public static void addBasicAuthHeader(String username, String password, HttpMessageBuilderSupport messageBuilderSupport) { + if (!isEmpty(username) && !isEmpty(password)) { + messageBuilderSupport.header("Authorization", "Basic citrus:encodeBase64(" + username + ":" + password + ")"); + } + } + + public static String mapXmlAttributeNameToJavaPropertyName(String attributeName) { + if (isEmpty(attributeName)) { + return attributeName; + } + + if ("basicUsername".equals(attributeName)) { + return "withBasicAuthUsername"; + } else if ("basicPassword".equals(attributeName)) { + return "withBasicAuthPassword"; + } + + return attributeName; + } +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/RestApiReceiveMessageActionParser.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/RestApiReceiveMessageActionParser.java new file mode 100644 index 0000000000..fe0a9e83b4 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/RestApiReceiveMessageActionParser.java @@ -0,0 +1,216 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi.spring; + +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.config.xml.AbstractReceiveMessageActionFactoryBean; +import org.citrusframework.http.actions.HttpClientResponseActionBuilder; +import org.citrusframework.http.actions.HttpClientResponseActionBuilder.HttpMessageBuilderSupport; +import org.citrusframework.http.config.xml.HttpReceiveResponseActionParser; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.http.message.HttpMessageHeaders; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.actions.OpenApiClientResponseActionBuilder.OpenApiClientResponseMessageBuilder; +import org.citrusframework.openapi.actions.OpenApiSpecificationSource; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.citrusframework.openapi.testapi.RestApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.validation.OpenApiMessageValidationContext; +import org.citrusframework.util.StringUtils; +import org.citrusframework.validation.context.ValidationContext; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; + +import java.util.List; + +import static java.lang.Integer.parseInt; +import static org.citrusframework.openapi.validation.OpenApiMessageValidationContext.Builder.openApi; +import static org.citrusframework.util.StringUtils.hasText; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; + +/** + * Parses XML configuration for receiving API responses based on OpenAPI specifications. Extends + * {@link HttpReceiveResponseActionParser} to handle OpenAPI-specific response builders and + * validation. + */ +public class RestApiReceiveMessageActionParser extends HttpReceiveResponseActionParser { + + public static final String STATUS_CODE = "responseCode"; + /** + * The generated api bean class. + */ + private final Class apiBeanClass; + + /** + * The builder class for the receive message action. + */ + private final Class beanClass; + + /** + * The OpenAPI specification related to this parser. + */ + private final OpenApiSpecification openApiSpecification; + + /** + * The OpenAPI operationId associated with this parser. + */ + private final String operationId; + + private final String defaultApiEndpointName; + + public RestApiReceiveMessageActionParser(OpenApiSpecification openApiSpecification, + String operationId, + Class apiBeanClass, + Class beanClass, + String defaultApiEndpointName) { + this.openApiSpecification = openApiSpecification; + this.operationId = operationId; + this.apiBeanClass = apiBeanClass; + this.beanClass = beanClass; + this.defaultApiEndpointName = defaultApiEndpointName; + } + + @Override + protected BeanDefinitionBuilder createBeanDefinitionBuilder(Element element, + ParserContext parserContext) { + BeanDefinitionBuilder beanDefinitionBuilder = super.createBeanDefinitionBuilder(element, parserContext); + + // Remove the messageBuilder property and inject it directly into the action builder. + BeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); + OpenApiClientResponseMessageBuilder messageBuilder = (OpenApiClientResponseMessageBuilder) beanDefinition.getPropertyValues() + .get("messageBuilder"); + + String statusCodeString = element.getAttribute(STATUS_CODE); + messageBuilder.statusCode(statusCodeString); + + if (StringUtils.isNotEmpty(statusCodeString) && messageBuilder.getMessage() != null ) { + try { + HttpStatusCode httpStatusCode = HttpStatusCode.valueOf( + parseInt(statusCodeString)); + messageBuilder.getMessage().status(httpStatusCode); + } catch (Exception e) { + // Ignore + } + } + + beanDefinition.getPropertyValues().removePropertyValue("messageBuilder"); + + BeanDefinitionBuilder actionBuilder = genericBeanDefinition(beanClass); + actionBuilder.addConstructorArgValue(new RuntimeBeanReference(apiBeanClass)); + actionBuilder.addConstructorArgValue(messageBuilder); + + beanDefinitionBuilder.addConstructorArgValue(actionBuilder.getBeanDefinition()); + setDefaultEndpoint(beanDefinitionBuilder); + + // By default, the type is xml. This not a common case in rest, which is why we switch to json here, + // if no explicit type is specified. + Attr type = element.getAttributeNode("type"); + if (type == null) { + beanDefinitionBuilder.addPropertyValue("messageType", "json"); + } + + return beanDefinitionBuilder; + } + + /** + * Sets the default endpoint for the message if not already specified. + */ + protected void setDefaultEndpoint(BeanDefinitionBuilder beanDefinitionBuilder) { + if (!beanDefinitionBuilder.getBeanDefinition().getPropertyValues().contains("endpoint") + && !beanDefinitionBuilder.getBeanDefinition().getPropertyValues().contains("endpointUri")) { + beanDefinitionBuilder.addPropertyReference("endpoint", defaultApiEndpointName); + } + } + + @Override + protected Class> getMessageFactoryClass() { + return TestApiOpenApiClientReceiveActionBuilderFactoryBean.class; + } + + @Override + protected void validateEndpointConfiguration(Element element) { + // skip validation, as we support endpoint injection + } + + @Override + protected HttpMessageBuilder createMessageBuilder(HttpMessage httpMessage) { + return new OpenApiClientResponseMessageBuilder(httpMessage, + new OpenApiSpecificationSource(openApiSpecification), operationId, null); + } + + @Override + protected List parseValidationContexts(Element messageElement, + BeanDefinitionBuilder builder) { + List validationContexts = super.parseValidationContexts(messageElement, builder); + OpenApiMessageValidationContext openApiMessageValidationContext = getOpenApiMessageValidationContext(messageElement); + validationContexts.add(openApiMessageValidationContext); + return validationContexts; + } + + /** + * Constructs the OpenAPI message validation context based on the XML element. + */ + private OpenApiMessageValidationContext getOpenApiMessageValidationContext(Element messageElement) { + OpenApiMessageValidationContext.Builder context = openApi(openApiSpecification); + + if (messageElement != null) { + addSchemaInformationToValidationContext(messageElement, context); + } + + return context.build(); + } + + /** + * Factory bean for creating {@link ReceiveMessageAction} instances using the provided + * {@link RestApiReceiveMessageActionBuilder}. + */ + public static class TestApiOpenApiClientReceiveActionBuilderFactoryBean extends + AbstractReceiveMessageActionFactoryBean { + + private RestApiReceiveMessageActionBuilder builder; + + public TestApiOpenApiClientReceiveActionBuilderFactoryBean(RestApiReceiveMessageActionBuilder builder) { + this.builder = builder; + } + + @Override + public ReceiveMessageAction getObject() { + return builder.build(); + } + + @Override + public Class getObjectType() { + return SendMessageAction.class; + } + + @Override + public HttpClientResponseActionBuilder getBuilder() { + return builder; + } + + public void setBuilder(RestApiReceiveMessageActionBuilder builder) { + this.builder = builder; + } + } +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/RestApiSendMessageActionParser.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/RestApiSendMessageActionParser.java new file mode 100644 index 0000000000..82f8d1f1c2 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/RestApiSendMessageActionParser.java @@ -0,0 +1,410 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi.spring; + +import static java.lang.Boolean.parseBoolean; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.citrusframework.openapi.testapi.TestApiUtils.mapXmlAttributeNameToJavaPropertyName; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; +import static org.springframework.util.xml.DomUtils.getChildElementByTagName; + +import java.util.List; +import java.util.stream.Collectors; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.config.xml.AbstractSendMessageActionFactoryBean; +import org.citrusframework.config.xml.AbstractTestContainerFactoryBean; +import org.citrusframework.config.xml.AsyncParser.AsyncFactoryBean; +import org.citrusframework.config.xml.SequenceParser.SequenceFactoryBean; +import org.citrusframework.http.actions.HttpClientRequestActionBuilder; +import org.citrusframework.http.actions.HttpClientRequestActionBuilder.HttpMessageBuilderSupport; +import org.citrusframework.http.config.xml.HttpSendRequestActionParser; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.message.HttpMessageBuilder; +import org.citrusframework.openapi.AutoFillType; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.actions.OpenApiClientRequestActionBuilder.OpenApiClientRequestMessageBuilder; +import org.citrusframework.openapi.actions.OpenApiSpecificationSource; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.citrusframework.openapi.testapi.RestApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.testapi.RestApiSendMessageActionBuilder; +import org.citrusframework.openapi.testapi.RestApiSendMessageActionBuilder.TestApiClientRequestMessageBuilder; +import org.citrusframework.util.StringUtils; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Parses the XML configuration for sending API requests based on OpenAPI specifications. Extends + * {@link HttpSendRequestActionParser} to handle OpenAPI specific request and response builders. + */ +public class RestApiSendMessageActionParser extends HttpSendRequestActionParser { + + public static final String ENDPOINT_URI = "endpointUri"; + public static final String ENDPOINT = "endpoint"; + /** + * The generated api bean class. + */ + private final Class apiBeanClass; + + /** + * The builder class for the send message action. + */ + private final Class requestBeanClass; + + /** + * The builder class for the receive message action, required when using nested send/receive xml + * elements. + */ + private final Class receiveBeanClass; + + /** + * The OpenAPI specification that relates to the TestAPI classes. + */ + private final OpenApiSpecification openApiSpecification; + + /** + * The OpenAPI operationId, related to this parser + */ + private final String operationId; + + /** + * The OpenAPI operation path. + */ + private final String path; + private final String defaultEndpointName; + /** + * Constructor parameters for the requestBeanClass. + */ + private List constructorParameters = emptyList(); + /** + * Optional non constructor parameters for the requestBeanClass. + */ + private List nonConstructorParameters = emptyList(); + + public RestApiSendMessageActionParser( + OpenApiSpecification openApiSpecification, + String operationId, + String path, + Class apiBeanClass, + Class sendBeanClass, + Class receiveBeanClass, + String defaultEndpointName) { + this.openApiSpecification = openApiSpecification; + this.operationId = operationId; + this.path = path; + this.apiBeanClass = apiBeanClass; + this.requestBeanClass = sendBeanClass; + this.receiveBeanClass = receiveBeanClass; + this.defaultEndpointName = defaultEndpointName; + } + + private static List collectChildNodeContents(Element element, String parameterName) { + return DomUtils.getChildElementsByTagName(element, parameterName) + .stream() + .map(Node::getTextContent) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList()); // For further processing, list must not be immutable + } + + @Override + protected BeanDefinitionBuilder createBeanDefinitionBuilder(final Element element, + ParserContext parserContext) { + BeanDefinitionBuilder beanDefinitionBuilder = super.createBeanDefinitionBuilder(element, + parserContext); + + BeanDefinitionBuilder actionBuilder = createTestApiActionBuilder(element, + beanDefinitionBuilder); + beanDefinitionBuilder.addConstructorArgValue(actionBuilder.getBeanDefinition()); + + beanDefinitionBuilder.addPropertyValue("autoFill", element.getAttribute("autofill")); + + setDefaultEndpoint(beanDefinitionBuilder); + + Element receive = getChildElementByTagName(element, "receive"); + if (receive != null) { + boolean fork = parseBoolean(element.getAttribute("fork")); + return wrapSendAndReceiveActionInSequence(fork, receive, parserContext, + beanDefinitionBuilder); + } + + return beanDefinitionBuilder; + } + + private BeanDefinitionBuilder createTestApiActionBuilder(Element element, + BeanDefinitionBuilder beanDefinitionBuilder) { + BeanDefinitionBuilder actionBuilder = propagateMessageBuilderToActionBuilder( + beanDefinitionBuilder); + readConstructorParameters(element, actionBuilder); + readNonConstructorParameters(element, actionBuilder); + return actionBuilder; + } + + /** + * Handles the configuration for both sending and receiving actions when a nested + * element is present in the XML specification. It creates appropriate builders for both sending + * and receiving messages and adds them to a container that executes these actions in sequence + * or asynchronously, depending on the {@code fork} parameter. + */ + private BeanDefinitionBuilder wrapSendAndReceiveActionInSequence(boolean fork, + Element receive, + ParserContext parserContext, + BeanDefinitionBuilder sendActionBeanDefinitionBuilder) { + + Class> containerClass = createSequenceContainer( + fork); + + BeanDefinitionBuilder sequenceBuilder = genericBeanDefinition(containerClass); + + RestApiReceiveMessageActionParser receiveApiResponseActionParser = getRestApiReceiveMessageActionParser( + sendActionBeanDefinitionBuilder); + + BeanDefinition receiveResponseBeanDefinition = receiveApiResponseActionParser.parse(receive, + parserContext); + + // Nested elements do not have reasonable names. + String sendName = (String) sendActionBeanDefinitionBuilder.getBeanDefinition().getPropertyValues() + .get("name"); + if (sendName != null) { + String receiveName = sendName.replace(":send", ":receive"); + if (!sendName.equals(receiveName)) { + receiveResponseBeanDefinition.getPropertyValues().add("name", receiveName); + } + } + + ManagedList actions = new ManagedList<>(); + actions.add(sendActionBeanDefinitionBuilder.getBeanDefinition()); + actions.add(receiveResponseBeanDefinition); + + sequenceBuilder.addPropertyValue("actions", actions); + + return sequenceBuilder; + } + + protected Class> createSequenceContainer(boolean fork) { + return fork ? AsyncFactoryBean.class : SequenceFactoryBean.class; + } + + private RestApiReceiveMessageActionParser getRestApiReceiveMessageActionParser( + BeanDefinitionBuilder sendBuilder) { + + return new RestApiReceiveMessageActionParser(openApiSpecification, operationId, + apiBeanClass, receiveBeanClass, defaultEndpointName) { + @Override + protected void setDefaultEndpoint(BeanDefinitionBuilder beanDefinitionBuilder) { + BeanDefinition beanDefinition = sendBuilder.getBeanDefinition(); + PropertyValue endpoint = beanDefinition.getPropertyValues() + .getPropertyValue(ENDPOINT); + PropertyValue endpointUri = beanDefinition.getPropertyValues() + .getPropertyValue(ENDPOINT_URI); + + String receiveEndpointName = null; + String receiveEndpointUri = null; + + if (endpoint != null + && endpoint.getValue() instanceof BeanReference beanReference) { + receiveEndpointName = beanReference.getBeanName(); + } else if (endpointUri != null && endpointUri.getValue() instanceof String uri) { + receiveEndpointUri = uri; + } + + if (!beanDefinitionBuilder.getBeanDefinition().getPropertyValues() + .contains(ENDPOINT) && + !beanDefinitionBuilder.getBeanDefinition().getPropertyValues() + .contains(ENDPOINT_URI)) { + if (receiveEndpointName != null) { + beanDefinitionBuilder.addPropertyReference(ENDPOINT, receiveEndpointName); + } else if (receiveEndpointUri != null) { + beanDefinitionBuilder.addPropertyValue(ENDPOINT_URI, receiveEndpointUri); + } else { + beanDefinitionBuilder.addPropertyReference(ENDPOINT, defaultEndpointName); + } + } + } + }; + } + + /** + * Propagates the message builder created by the superclass into the specific send message + * action builder. + */ + private BeanDefinitionBuilder propagateMessageBuilderToActionBuilder( + BeanDefinitionBuilder beanDefinitionBuilder) { + BeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); + OpenApiClientRequestMessageBuilder messageBuilder = (OpenApiClientRequestMessageBuilder) beanDefinition.getPropertyValues() + .get("messageBuilder"); + beanDefinition.getPropertyValues().removePropertyValue("messageBuilder"); + + BeanDefinitionBuilder actionBuilder = genericBeanDefinition(requestBeanClass); + + actionBuilder.addConstructorArgValue(new RuntimeBeanReference(apiBeanClass)); + actionBuilder.addConstructorArgValue(messageBuilder); + + return actionBuilder; + } + + /** + * Reads constructor parameters from the XML element and adds them as constructor arguments to + * the provided {@link BeanDefinitionBuilder}. + */ + private void readConstructorParameters(Element element, BeanDefinitionBuilder actionBuilder) { + for (String parameterName : constructorParameters) { + if (element.hasAttribute(parameterName)) { + actionBuilder.addConstructorArgValue(element.getAttribute(parameterName)); + } else { + List values = collectChildNodeContents(element, parameterName); + actionBuilder.addConstructorArgValue(values); + } + } + } + + /** + * Reads non-constructor parameters from the XML element and adds them as properties to the + * provided {@link BeanDefinitionBuilder}. + */ + private void readNonConstructorParameters(Element element, + BeanDefinitionBuilder actionBuilder) { + for (String parameterName : nonConstructorParameters) { + if (isHandledBySuper(parameterName)) { + continue; + } + + // For java parameter types other that string, we need to use the non-typed java property + // with type string. These properties in java dsl are identified by a trailing '$'. In xml + // however, the trailing '$' is omitted. Thus, the respective names are prepared accordingly. + String attributeName = parameterName; + if (parameterName.endsWith("$")) { + attributeName = parameterName.substring(0, parameterName.length() - 1); + } + + Attr attribute = element.getAttributeNode(attributeName); + if (attribute != null) { + actionBuilder.addPropertyValue( + mapXmlAttributeNameToJavaPropertyName(parameterName), + attribute.getValue()); + } else { + List values = collectChildNodeContents(element, attributeName); + if (values != null && !values.isEmpty()) { + actionBuilder.addPropertyValue( + mapXmlAttributeNameToJavaPropertyName(parameterName), + values); + } + } + } + } + + /** + * Sets the default endpoint for TestApi actions, if not already specified. + */ + private void setDefaultEndpoint(BeanDefinitionBuilder beanDefinitionBuilder) { + if (!beanDefinitionBuilder.getBeanDefinition().getPropertyValues().contains(ENDPOINT) + && !beanDefinitionBuilder.getBeanDefinition().getPropertyValues() + .contains(ENDPOINT_URI)) { + beanDefinitionBuilder.addPropertyReference(ENDPOINT, defaultEndpointName); + } + } + + /** + * Checks if the property is handled by the superclass implementation. + * + * @param property The property name to check. + * @return True if handled by the superclass, false otherwise. + */ + private boolean isHandledBySuper(String property) { + return "body".equals(property); + } + + @Override + protected Class> getMessageFactoryClass() { + return TestApiOpenApiClientSendActionBuilderFactoryBean.class; + } + + @Override + protected void validateEndpointConfiguration(Element element) { + // skip validation, as we support endpoint injection + } + + @Override + protected Element getRequestElement(Element element) { + return element; + } + + @Override + protected HttpMessageBuilder createMessageBuilder(HttpMessage httpMessage) { + httpMessage.path(path); + return new TestApiClientRequestMessageBuilder(httpMessage, + new OpenApiSpecificationSource(openApiSpecification), operationId); + } + + public void setConstructorParameters(String... constructorParameters) { + this.constructorParameters = + constructorParameters != null ? asList(constructorParameters) + : emptyList(); + } + + public void setNonConstructorParameters(String... nonConstructorParameters) { + this.nonConstructorParameters = + nonConstructorParameters != null ? asList(nonConstructorParameters) + : emptyList(); + } + + /** + * Factory bean for creating {@link SendMessageAction} instances using the provided + * {@link RestApiSendMessageActionBuilder}. + */ + public static class TestApiOpenApiClientSendActionBuilderFactoryBean extends + AbstractSendMessageActionFactoryBean { + + private RestApiSendMessageActionBuilder builder; + + public TestApiOpenApiClientSendActionBuilderFactoryBean( + RestApiSendMessageActionBuilder builder) { + this.builder = builder; + } + + @Override + public SendMessageAction getObject() { + return builder.build(); + } + + @Override + public Class getObjectType() { + return SendMessageAction.class; + } + + @Override + public HttpClientRequestActionBuilder getBuilder() { + return builder; + } + + public void setBuilder(RestApiSendMessageActionBuilder builder) { + this.builder = builder; + } + + public void setAutoFill(AutoFillType autoFill) { + this.builder.autoFill(autoFill); + } + } +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/SoapApiReceiveMessageActionParser.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/SoapApiReceiveMessageActionParser.java new file mode 100644 index 0000000000..ed324d06e7 --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/SoapApiReceiveMessageActionParser.java @@ -0,0 +1,96 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi.spring; + +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.citrusframework.openapi.testapi.SoapApiReceiveMessageActionBuilder; +import org.citrusframework.ws.config.xml.ReceiveSoapMessageActionParser; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.w3c.dom.Element; + +import static org.citrusframework.util.StringUtils.hasText; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; + +public class SoapApiReceiveMessageActionParser extends ReceiveSoapMessageActionParser { + + /** + * The generated api bean class. + */ + private final Class apiBeanClass; + + /** + * The builder class for the receive message action. + */ + private final Class receiveBeanClass; + + private final String defaultEndpointName; + + public SoapApiReceiveMessageActionParser( + Class apiBeanClass, + Class beanClass, + String defaultEndpointName) { + this.apiBeanClass = apiBeanClass; + this.receiveBeanClass = beanClass; + this.defaultEndpointName = defaultEndpointName; + } + + @Override + protected BeanDefinitionBuilder parseComponent(Element element, ParserContext parserContext) { + BeanDefinitionBuilder beanDefinitionBuilder = super.parseComponent(element, parserContext); + + BeanDefinitionBuilder actionBuilder = createTestApiActionBuilder(); + beanDefinitionBuilder.addConstructorArgValue(actionBuilder.getBeanDefinition()); + + return beanDefinitionBuilder; + } + + @Override + protected String parseEndpoint(Element element) { + String endpointUri = element.getAttribute("endpoint"); + + if (!hasText(endpointUri)) { + endpointUri = defaultEndpointName; + } + + return endpointUri; + } + + private BeanDefinitionBuilder createTestApiActionBuilder() { + BeanDefinitionBuilder actionBuilder = genericBeanDefinition(receiveBeanClass); + actionBuilder.addConstructorArgValue(new RuntimeBeanReference(apiBeanClass)); + + return actionBuilder; + } + + @Override + protected Class getMessageFactoryClass() { + return TestApiSoapClientReceiveActionBuilderFactoryBean.class; + } + + /** + * Test action factory bean. + */ + public static class TestApiSoapClientReceiveActionBuilderFactoryBean extends + ReceiveSoapMessageActionFactoryBean { + + public TestApiSoapClientReceiveActionBuilderFactoryBean(SoapApiReceiveMessageActionBuilder builder) { + super(builder); + } + } +} diff --git a/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/SoapApiSendMessageActionParser.java b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/SoapApiSendMessageActionParser.java new file mode 100644 index 0000000000..cd982a260f --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/main/java/org/citrusframework/openapi/testapi/spring/SoapApiSendMessageActionParser.java @@ -0,0 +1,152 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.testapi.spring; + +import org.citrusframework.config.xml.AbstractTestContainerFactoryBean; +import org.citrusframework.config.xml.AsyncParser.AsyncFactoryBean; +import org.citrusframework.config.xml.SequenceParser.SequenceFactoryBean; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.citrusframework.openapi.testapi.SoapApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.testapi.SoapApiSendMessageActionBuilder; +import org.citrusframework.util.StringUtils; +import org.citrusframework.ws.config.xml.SendSoapMessageActionParser; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.xml.ParserContext; +import org.w3c.dom.Element; + +import static java.lang.Boolean.parseBoolean; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; +import static org.springframework.util.xml.DomUtils.getChildElementByTagName; + +public class SoapApiSendMessageActionParser extends SendSoapMessageActionParser { + + /** + * The generated api bean class. + */ + private final Class apiBeanClass; + + /** + * The builder class for the send message action. + */ + private final Class sendBeanClass; + + /** + * The builder class for the receive message action, required when using nested send/receive xml + * elements. + */ + private final Class receiveBeanClass; + + private final String defaultEndpointName; + + public SoapApiSendMessageActionParser( + Class apiBeanClass, + Class sendBeanClass, + Class receiveBeanClass, + String defaultEndpointName) { + this.apiBeanClass = apiBeanClass; + this.sendBeanClass = sendBeanClass; + this.receiveBeanClass = receiveBeanClass; + this.defaultEndpointName = defaultEndpointName; + } + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + BeanDefinition beanDefinition = super.parse(element, parserContext); + Element receive = getChildElementByTagName(element, "receive"); + if (receive != null) { + boolean fork = parseBoolean(element.getAttribute("fork")); + return wrapSendAndReceiveActionInSequence(fork, receive, parserContext, beanDefinition); + } + + return beanDefinition; + } + + @Override + public BeanDefinitionBuilder parseComponent(Element element, ParserContext parserContext) { + BeanDefinitionBuilder beanDefinitionBuilder = super.parseComponent(element, parserContext); + + BeanDefinitionBuilder actionBuilder = createTestApiActionBuilder(); + beanDefinitionBuilder.addConstructorArgValue(actionBuilder.getBeanDefinition()); + + return beanDefinitionBuilder; + } + + @Override + protected String parseEndpoint(Element element) { + String endpointUri = element.getAttribute("endpoint"); + + if (!StringUtils.hasText(endpointUri)) { + endpointUri = defaultEndpointName; + } + + return endpointUri; + } + + private BeanDefinitionBuilder createTestApiActionBuilder() { + BeanDefinitionBuilder actionBuilder = genericBeanDefinition(sendBeanClass); + actionBuilder.addConstructorArgValue(new RuntimeBeanReference(apiBeanClass)); + + return actionBuilder; + } + + @Override + protected Class getMessageFactoryClass() { + return TestApiSoapClientSendActionBuilderFactoryBean.class; + } + + /** + * Handles the configuration for both sending and receiving actions when a nested + * element is present in the XML specification. It creates appropriate builders for both sending + * and receiving messages and adds them to a container that executes these actions in sequence + * or asynchronously, depending on the {@code fork} parameter. + */ + private BeanDefinition wrapSendAndReceiveActionInSequence(boolean fork, + Element receive, + ParserContext parserContext, + BeanDefinition sendBeanDefinition) { + Class> containerClass = fork ? AsyncFactoryBean.class + : SequenceFactoryBean.class; + + BeanDefinitionBuilder sequenceBuilder = genericBeanDefinition(containerClass); + + SoapApiReceiveMessageActionParser receiveApiResponseActionParser = new SoapApiReceiveMessageActionParser( + apiBeanClass, receiveBeanClass, defaultEndpointName); + BeanDefinition receiveResponseBeanDefinition = receiveApiResponseActionParser.parse(receive, parserContext); + + ManagedList actions = new ManagedList<>(); + actions.add(sendBeanDefinition); + actions.add(receiveResponseBeanDefinition); + + sequenceBuilder.addPropertyValue("actions", actions); + + return sequenceBuilder.getBeanDefinition(); + } + + /** + * Test action factory bean. + */ + public static class TestApiSoapClientSendActionBuilderFactoryBean extends + SendSoapMessageActionFactoryBean { + + public TestApiSoapClientSendActionBuilderFactoryBean(SoapApiSendMessageActionBuilder builder) { + super(builder); + } + } +} diff --git a/test-api-generator/citrus-test-api-core/src/test/java/org/citrusframework/openapi/testapi/OpenApiParameterFormatterTest.java b/test-api-generator/citrus-test-api-core/src/test/java/org/citrusframework/openapi/testapi/OpenApiParameterFormatterTest.java new file mode 100644 index 0000000000..85e337659c --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/test/java/org/citrusframework/openapi/testapi/OpenApiParameterFormatterTest.java @@ -0,0 +1,84 @@ +package org.citrusframework.openapi.testapi; + +import org.citrusframework.openapi.testapi.RestApiSendMessageActionBuilder.ParameterData; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.citrusframework.openapi.testapi.OpenApiParameterFormatter.formatArray; +import static org.citrusframework.openapi.testapi.ParameterStyle.DEEPOBJECT; +import static org.citrusframework.openapi.testapi.ParameterStyle.FORM; +import static org.citrusframework.openapi.testapi.ParameterStyle.LABEL; +import static org.citrusframework.openapi.testapi.ParameterStyle.MATRIX; +import static org.citrusframework.openapi.testapi.ParameterStyle.SIMPLE; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class OpenApiParameterFormatterTest { + + private static final User USER = new User("admin", "Alex"); + private static final List LIST = List.of(3, 4, 5); + private static final Integer SINGLE = 5; + + static Stream format() { + return Stream.of( + arguments("Simple/non exploded/non object/single", new ParameterData("id", SINGLE, SIMPLE, false, false), "5"), + arguments("Simple/non exploded/non object/array", new ParameterData("id", LIST, SIMPLE, false, false), "3,4,5"), + arguments("Simple/non exploded/Object/single", new ParameterData("id", USER, SIMPLE, false, true), "firstName,Alex,role,admin"), + arguments("Simple/exploded/non object/single", new ParameterData("id", SINGLE, SIMPLE, true, false), "5"), + arguments("Simple/exploded/non object/array", new ParameterData("id", LIST, SIMPLE, true, false), "3,4,5"), + arguments("Simple/exploded/Object/single", new ParameterData("id", USER, SIMPLE, true, true), "firstName=Alex,role=admin"), + arguments("Label/non exploded/non object/single", new ParameterData("id", SINGLE, LABEL, false, false), ".5"), + arguments("Label/non exploded/non object/array", new ParameterData("id", LIST, LABEL, false, false), ".3,4,5"), + arguments("Label/non exploded/Object/single", new ParameterData("id", USER, LABEL, false, true), ".firstName,Alex,role,admin"), + arguments("Label/exploded/non object/single", new ParameterData("id", SINGLE, LABEL, true, false), ".5"), + arguments("Label/exploded/non object/array", new ParameterData("id", LIST, LABEL, true, false), ".3.4.5"), + arguments("Label/exploded/Object/single", new ParameterData("id", USER, LABEL, true, true), ".firstName=Alex.role=admin"), + arguments("Matrix/non exploded/non object/single", new ParameterData("id", SINGLE, MATRIX, false, false), ";id=5"), + arguments("Matrix/non exploded/non object/array", new ParameterData("id", LIST, MATRIX, false, false), ";id=3,4,5"), + arguments("Matrix/non exploded/Object/single", new ParameterData("id", USER, MATRIX, false, true), ";id=firstName,Alex,role,admin"), + arguments("Matrix/exploded/non object/single", new ParameterData("id", SINGLE, MATRIX, true, false), ";id=5"), + arguments("Matrix/exploded/non object/array", new ParameterData("id", LIST, MATRIX, true, false), ";id=3;id=4;id=5"), + arguments("Matrix/exploded/Object/single", new ParameterData("id", USER, MATRIX, true, true), ";firstName=Alex;role=admin"), + arguments("Form/non exploded/non object/single", new ParameterData("id", SINGLE, FORM, false, false), "id=5"), + arguments("Form/non exploded/non object/array", new ParameterData("id", LIST, FORM, false, false), "id=3,4,5"), + arguments("Form/non exploded/object/single", new ParameterData("id", USER, FORM, false, true), "id=firstName,Alex,role,admin"), + arguments("Form/exploded/non object/single", new ParameterData("id", SINGLE, FORM, true, false), "id=5"), + arguments("Form/exploded/non object/array", new ParameterData("id", LIST, FORM, true, false), "id=3&id=4&id=5"), + arguments("Form/exploded/object/single", new ParameterData("id", USER, FORM, true, true), "firstName=Alex&role=admin"), + arguments("DeepObject/exploded/object/single", new ParameterData("id", USER, + DEEPOBJECT, true, true), "id[firstName]=Alex&id[role]=admin") + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource + void format(String name, ParameterData parameterData, String expected) { + assertThat( + formatArray( + parameterData.name(), + parameterData.value(), + parameterData.parameterStyle(), + parameterData.explode(), + parameterData.isObject())) + .isEqualTo(expected); + } + + private record User(String role, String firstName) { + + // Used for formatting + @SuppressWarnings({"unused"}) + public String getRole() { + return role; + } + + // Used for formatting + @SuppressWarnings({"unused"}) + public String getFirstName() { + return firstName; + } + } +} diff --git a/test-api-generator/citrus-test-api-core/src/test/java/org/citrusframework/openapi/testapi/TestApiUtilsTest.java b/test-api-generator/citrus-test-api-core/src/test/java/org/citrusframework/openapi/testapi/TestApiUtilsTest.java new file mode 100644 index 0000000000..79d7ae2abe --- /dev/null +++ b/test-api-generator/citrus-test-api-core/src/test/java/org/citrusframework/openapi/testapi/TestApiUtilsTest.java @@ -0,0 +1,68 @@ +package org.citrusframework.openapi.testapi; + +import org.citrusframework.http.actions.HttpClientRequestActionBuilder.HttpMessageBuilderSupport; +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class TestApiUtilsTest { + + @Test + void shouldAddBasicAuthHeaderWhenUsernameAndPasswordAreProvided() { + // Given + String username = "user"; + String password = "pass"; + HttpMessageBuilderSupport messageBuilderSupport = mock(HttpMessageBuilderSupport.class); + + // When + TestApiUtils.addBasicAuthHeader(username, password, messageBuilderSupport); + + // Then + verify(messageBuilderSupport).header("Authorization", "Basic citrus:encodeBase64(user:pass)"); + } + + @Test + void shouldNotAddBasicAuthHeaderWhenUsernameIsEmpty() { + // Given + String username = ""; + String password = "pass"; + HttpMessageBuilderSupport messageBuilderSupport = mock(HttpMessageBuilderSupport.class); + + // When + TestApiUtils.addBasicAuthHeader(username, password, messageBuilderSupport); + + // Then + verify(messageBuilderSupport, never()).header(anyString(), anyString()); + } + + @Test + void shouldNotAddBasicAuthHeaderWhenPasswordIsEmpty() { + // Given + String username = "user"; + String password = ""; + HttpMessageBuilderSupport messageBuilderSupport = mock(HttpMessageBuilderSupport.class); + + // When + TestApiUtils.addBasicAuthHeader(username, password, messageBuilderSupport); + + // Then + verify(messageBuilderSupport, never()).header(anyString(), anyString()); + } + + @Test + void shouldNotAddBasicAuthHeaderWhenBothUsernameAndPasswordAreEmpty() { + // Given + String username = ""; + String password = ""; + HttpMessageBuilderSupport messageBuilderSupport = mock(HttpMessageBuilderSupport.class); + + // When + TestApiUtils.addBasicAuthHeader(username, password, messageBuilderSupport); + + // Then + verify(messageBuilderSupport, never()).header(anyString(), anyString()); + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/pom.xml b/test-api-generator/citrus-test-api-generator-core/pom.xml new file mode 100644 index 0000000000..cc21f89f67 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/pom.xml @@ -0,0 +1,289 @@ + + 4.0.0 + + citrus-test-api-generator + org.citrusframework + 4.6.0-SNAPSHOT + ../pom.xml + + + citrus-test-api-generator-core + jar + + Citrus :: Test API Generator :: Core + Generates a Citrus Test-API for OpenAPI and WSDL specifications. + + + ${project.build.directory}/openapi-java-resources + + + + + org.citrusframework + citrus-api + ${project.version} + + + org.citrusframework + citrus-http + ${project.version} + + + org.citrusframework + citrus-openapi + ${project.version} + + + org.citrusframework + citrus-spring + ${project.version} + + + org.citrusframework + citrus-ws + ${project.version} + + + org.citrusframework + citrus-test-api-core + ${project.version} + + + org.citrusframework + citrus-groovy + ${project.version} + test + + + + org.citrusframework + citrus-validation-groovy + ${project.version} + test + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + org.openapitools + openapi-generator + ${org.openapitools.version} + + + wsdl4j + wsdl4j + + + + + org.citrusframework + citrus-junit5 + ${project.version} + test + + + org.citrusframework + citrus-validation-json + ${project.version} + test + + + org.springframework.boot + spring-boot-test + ${spring.boot.version} + test + + + commons-fileupload + commons-fileupload + 1.5 + test + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + ${maven.helper.plugin.version} + + + add-generated-test-resources + generate-test-resources + + add-test-resource + + + + + ${project.build.directory}/generated-test-resources + ${project.build.outputDirectory} + + + + + + add-generated-test-classes + generate-test-sources + + add-test-source + + + + ${project.build.directory}/generated-test-sources + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven.dependency.plugin.version} + + + + org.openapitools + openapi-generator + ${org.openapitools.version} + + Java/*Annotation*.mustache,Java/*Model*.mustache,Java/model*.mustache,Java/pojo*.mustache,Java/enum_outer_doc.mustache,Java/nullable_var_annotations.mustache + + + + + ${openapi-java-folder} + + + + + unpack-java-templates + generate-resources + + unpack + + + + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven.resource.plugin.version} + + + prepare-java-templates + process-resources + + copy-resources + + + ${project.build.outputDirectory}/java-citrus + + + + ${openapi-java-folder}/Java + + + + *.mustache + + + + + + + + + + + org.openapitools + openapi-generator-maven-plugin + ${org.openapitools.version} + + + org.citrusframework + citrus-test-api-generator-core + ${project.version} + + + + + REST + generated-test-resources + generated-test-sources + true + + true + java-citrus + ${project.build.directory} + + + + generate-openapi-petstore-files + compile + + generate + + + ${project.basedir}/src/test/resources/apis/petstore-v3.yaml + + org.citrusframework.openapi.generator.rest.petstore + org.citrusframework.openapi.generator.rest.petstore.request + org.citrusframework.openapi.generator.rest.petstore.model + petStore + petstore.endpoint + + + + + generate-openapi-petstore-extended-files + compile + + generate + + + ${project.basedir}/src/test/resources/apis/petstore-extended-v3.yaml + + org.citrusframework.openapi.generator.rest.extpetstore + org.citrusframework.openapi.generator.rest.extpetstore.request + + org.citrusframework.openapi.generator.rest.extpetstore.model + + ExtPetStore + extpetstore.endpoint + + + + + generate-openapi-files-for-soap + compile + + generate + + + ${project.basedir}/src/test/resources/apis/BookService-generated.yaml + + SOAP + org.citrusframework.openapi.generator.soap.bookservice + org.citrusframework.openapi.generator.soap.bookservice.request + + org.citrusframework.openapi.generator.soap.bookservice.model + + BookService + bookstore.endpoint + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/java/org/citrusframework/openapi/generator/CitrusJavaCodegen.java b/test-api-generator/citrus-test-api-generator-core/src/main/java/org/citrusframework/openapi/generator/CitrusJavaCodegen.java new file mode 100644 index 0000000000..b060c3e7ba --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/java/org/citrusframework/openapi/generator/CitrusJavaCodegen.java @@ -0,0 +1,638 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.generator; + +import static java.lang.Boolean.TRUE; +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toMap; +import static org.citrusframework.util.ReflectionHelper.copyFields; +import static org.citrusframework.util.StringUtils.appendSegmentToUrlPath; +import static org.citrusframework.util.StringUtils.convertFirstChartToUpperCase; +import static org.openapitools.codegen.CliOption.newString; +import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER; +import static org.openapitools.codegen.utils.StringUtils.camelize; + +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.oas.models.servers.Server; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.openapi.testapi.RestApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.testapi.RestApiSendMessageActionBuilder; +import org.citrusframework.openapi.testapi.SoapApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.testapi.SoapApiSendMessageActionBuilder; +import org.openapitools.codegen.CodegenOperation; +import org.openapitools.codegen.CodegenParameter; +import org.openapitools.codegen.CodegenSecurity; +import org.openapitools.codegen.CodegenType; +import org.openapitools.codegen.SupportingFile; +import org.openapitools.codegen.languages.AbstractJavaCodegen; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.OperationMap; +import org.openapitools.codegen.model.OperationsMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CitrusJavaCodegen extends AbstractJavaCodegen { + + private static final Logger logger = LoggerFactory.getLogger(CitrusJavaCodegen.class); + + /** + * The root context path used as prefix to the OpenAPI paths. Note that this need to be + * explicitly be defined by configuration and may differ from any server node that may be + * specified in the OpenAPI. + */ + public static final String ROOT_CONTEXT_PATH = "rootContextPath"; + + public static final String CODEGEN_NAME = "java-citrus"; + public static final String API_TYPE_REST = "REST"; + public static final String API_TYPE_SOAP = "SOAP"; + public static final String API_ENDPOINT = "apiEndpoint"; + public static final String API_TYPE = "apiType"; + public static final String GENERATED_SCHEMA_FOLDER = "generatedSchemaFolder"; + public static final String PREFIX = "prefix"; + public static final String RESOURCE_FOLDER = "resourceFolder"; + public static final String TARGET_XMLNS_NAMESPACE = "targetXmlnsNamespace"; + public static final String REQUEST_BUILDER_CLASS = "requestBuilderClass"; + public static final String RESPONSE_BUILDER_CLASS = "responseBuilderClass"; + public static final String REQUEST_BUILDER_CLASS_NAME = "requestBuilderClassName"; + public static final String RESPONSE_BUILDER_CLASS_NAME = "responseBuilderClassName"; + + protected String apiPrefix = "Api"; + + protected String httpClient = API_ENDPOINT; + + protected String resourceFolder = projectFolder + "/resources"; + + protected String generatedSchemaFolder = "schema" + File.separator + "xsd"; + protected String targetXmlnsNamespace; + + protected String apiVersion = "1.0.0"; + + public CitrusJavaCodegen() { + super(); + + templateDir = CODEGEN_NAME; + + configureAdditionalProperties(); + configureReservedWords(); + configureCliOptions(); + configureTypeMappings(); + } + + private static void postProcessSecurityParameters( + CustomCodegenOperation customCodegenOperation) { + customCodegenOperation.hasApiKeyAuth = customCodegenOperation.authMethods.stream() + .anyMatch(codegenSecurity -> codegenSecurity.isApiKey); + + customCodegenOperation.authWithParameters = customCodegenOperation.hasApiKeyAuth; + for (CodegenSecurity codegenSecurity : customCodegenOperation.authMethods) { + if (TRUE.equals(codegenSecurity.isBasicBasic)) { + customCodegenOperation.optionalAndAuthParameterNames.add("basicAuthUsername"); + customCodegenOperation.optionalAndAuthParameterNames.add("basicAuthPassword"); + customCodegenOperation.authWithParameters = true; + } else if (TRUE.equals(codegenSecurity.isApiKey)) { + customCodegenOperation.optionalAndAuthParameterNames.add( + camelize(codegenSecurity.keyParamName, LOWERCASE_FIRST_LETTER)); + customCodegenOperation.authWithParameters = true; + } else if (TRUE.equals(codegenSecurity.isBasicBearer)) { + customCodegenOperation.optionalAndAuthParameterNames.add("basicAuthBearer"); + customCodegenOperation.authWithParameters = true; + } + } + } + + public String getApiPrefix() { + return apiPrefix; + } + + public String getHttpClient() { + return httpClient; + } + + public void setHttpClient(String httpClient) { + this.httpClient = httpClient; + } + + public String getResourceFolder() { + return resourceFolder; + } + + public void setResourceFolder(String resourceFolder) { + this.resourceFolder = resourceFolder; + } + + public String getGeneratedSchemaFolder() { + return generatedSchemaFolder; + } + + public void setGeneratedSchemaFolder(String generatedSchemaFolder) { + this.generatedSchemaFolder = generatedSchemaFolder; + } + + public String getTargetXmlnsNamespace() { + return targetXmlnsNamespace; + } + + public void setTargetXmlnsNamespace(String targetXmlnsNamespace) { + this.targetXmlnsNamespace = targetXmlnsNamespace; + } + + public String getApiVersion() { + return apiVersion; + } + + public void setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + } + + private void configureAdditionalProperties() { + additionalProperties.put("apiVersion", apiVersion); + additionalProperties.put(API_TYPE, API_TYPE_REST); + additionalProperties.put("useJakartaEe", true); + } + + private void configureReservedWords() { + Set reservedWordsTemp = reservedWords(); + reservedWordsTemp.addAll( + asList( + "name", + "description", + "httpClient", + "message", + "endpoint", + "validate", + "validator", + "validators", + "process", + "selector", + "transform", + "build", + "actor", + "process") + ); + setReservedWordsLowerCase(new ArrayList<>(reservedWordsTemp)); + } + + private void configureCliOptions() { + cliOptions.add( + newString(API_ENDPOINT, + "Which http client should be used (default " + httpClient + ").")); + cliOptions.add( + newString(API_TYPE, + "Specifies the type of API to be generated specify SOAP to generate a SOAP API. By default a REST API will be generated" + ) + ); + cliOptions.add( + newString(GENERATED_SCHEMA_FOLDER, + "The schema output directory (default " + generatedSchemaFolder + ").") + ); + cliOptions.add( + newString(PREFIX, + "Add a prefix before the name of the files. First character should be upper case (default " + + apiPrefix + ")." + ) + ); + cliOptions.add(newString(PREFIX, "The api prefix (default " + apiPrefix + ").")); + cliOptions.add( + newString(RESOURCE_FOLDER, + "Where the resource files are emitted (default " + resourceFolder + ").")); + cliOptions.add( + newString(TARGET_XMLNS_NAMESPACE, + "Xmlns namespace of the schema (default " + targetXmlnsNamespace + ").") + ); + } + + private void configureTypeMappings() { + this.typeMapping.put("binary", "org.citrusframework.spi.Resource"); + this.typeMapping.put("file", "org.citrusframework.spi.Resource"); + } + + /** + * Returns human-friendly help for the generator. Provide the consumer with help tips, + * parameters here + * + * @return A string value for the help message + */ + @Override + public String getHelp() { + return "Generates citrus api requests."; + } + + /** + * Configures a human-friendly name for the generator. This will be used by the generator to + * select the library with the -g flag. + * + * @return the human-friendly name for the generator + */ + @Override + public String getName() { + return CODEGEN_NAME; + } + + /** + * Configures the type of generator. + * + * @return the CodegenType for this generator + * @see org.openapitools.codegen.CodegenType + */ + @Override + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + @Override + public void processOpts() { + super.processOpts(); + setupApiPrefix(); + setupEndpoint(); + setupNamespace(); + setupFolders(); + setupApiType(); + addDefaultSupportingFiles(); + writeApiToResourceFolder(); + } + + private void setupEndpoint() { + additionalProperties.computeIfAbsent(API_ENDPOINT, k->apiPrefix+"Endpoint"); + this.setHttpClient(additionalProperties.get(API_ENDPOINT).toString()); + } + + private void setupApiPrefix() { + if (additionalProperties.containsKey(PREFIX)) { + apiPrefix = additionalProperties.get(PREFIX).toString(); + additionalProperties.put(PREFIX, apiPrefix); + additionalProperties.put(PREFIX + "LowerCase", apiPrefix.toLowerCase()); + } else { + logger.warn( + "Using empty prefix for code generation. A prefix can be configured using \"{}\" property.", + PREFIX); + apiPrefix = ""; + } + } + + private void setupNamespace() { + if (additionalProperties.containsKey(TARGET_XMLNS_NAMESPACE)) { + this.setTargetXmlnsNamespace( + additionalProperties.get(TARGET_XMLNS_NAMESPACE).toString()); + } else { + this.targetXmlnsNamespace = format( + "http://www.citrusframework.org/citrus-test-schema/%s-api", + apiPrefix.toLowerCase()); + } + additionalProperties.put(TARGET_XMLNS_NAMESPACE, targetXmlnsNamespace); + } + + private void setupFolders() { + if (additionalProperties.containsKey(GENERATED_SCHEMA_FOLDER)) { + this.setGeneratedSchemaFolder( + additionalProperties.get(GENERATED_SCHEMA_FOLDER).toString()); + } + additionalProperties.put(GENERATED_SCHEMA_FOLDER, generatedSchemaFolder); + + if (additionalProperties.containsKey(RESOURCE_FOLDER)) { + this.setResourceFolder(additionalProperties.get(RESOURCE_FOLDER).toString()); + } + additionalProperties.put(RESOURCE_FOLDER, resourceFolder); + + } + + private void setupApiType() { + Object apiType = additionalProperties.get(API_TYPE); + if (API_TYPE_REST.equals(apiType)) { + setupRestApiType(); + } else if (API_TYPE_SOAP.equals(apiType)) { + setupSoapApiType(); + } else { + throw new IllegalArgumentException(format("Unknown API_TYPE: '%s'", apiType)); + } + } + + private void setupSoapApiType() { + additionalProperties.put(REQUEST_BUILDER_CLASS, + SoapApiSendMessageActionBuilder.class.getName()); + additionalProperties.put(REQUEST_BUILDER_CLASS_NAME, + SoapApiSendMessageActionBuilder.class.getSimpleName()); + additionalProperties.put(RESPONSE_BUILDER_CLASS, + SoapApiReceiveMessageActionBuilder.class.getName()); + additionalProperties.put(RESPONSE_BUILDER_CLASS_NAME, + SoapApiReceiveMessageActionBuilder.class.getSimpleName()); + additionalProperties.put("isRest", false); + additionalProperties.put("isSoap", true); + addSoapSupportingFiles(); + } + + private void setupRestApiType() { + additionalProperties.put(REQUEST_BUILDER_CLASS, + RestApiSendMessageActionBuilder.class.getName()); + additionalProperties.put(REQUEST_BUILDER_CLASS_NAME, + RestApiSendMessageActionBuilder.class.getSimpleName()); + additionalProperties.put(RESPONSE_BUILDER_CLASS, + RestApiReceiveMessageActionBuilder.class.getName()); + additionalProperties.put(RESPONSE_BUILDER_CLASS_NAME, + RestApiReceiveMessageActionBuilder.class.getSimpleName()); + additionalProperties.put("isRest", true); + additionalProperties.put("isSoap", false); + + addRestSupportingFiles(); + } + + /** + * Save a copy of the source OpenAPI as a resource, allowing it to be registered as an OpenAPI + * repository. + */ + private void writeApiToResourceFolder() { + String directoryPath = appendSegmentToUrlPath(getOutputDir(), getResourceFolder()); + directoryPath = appendSegmentToUrlPath(directoryPath, + invokerPackage.replace('.', File.separatorChar)); + + String filename = getApiPrefix() + "_openApi.yaml"; + + File directory = new File(directoryPath); + if (!directory.exists() && !directory.mkdirs()) { + throw new CitrusRuntimeException("Unable to create directory for api resource!"); + } + + File file = new File(directory, filename); + + try (FileWriter writer = new FileWriter(file)) { + String yamlContent = Yaml.pretty(openAPI); + writer.write(yamlContent); + } catch (IOException e) { + throw new CitrusRuntimeException( + "Unable to write OpenAPI to resource folder: " + file.getAbsolutePath()); + } + } + + @Override + public void preprocessOpenAPI(OpenAPI openAPI) { + super.preprocessOpenAPI(openAPI); + + Info info = openAPI.getInfo(); + Map extensions = info.getExtensions(); + if (extensions != null) { + additionalProperties.putAll(extensions); + + Map infoExtensions = extensions.entrySet().stream() + .filter(entry -> entry.getKey().toUpperCase().startsWith("X-")) + .collect(toMap(Entry::getKey, Entry::getValue)); + additionalProperties.put("infoExtensions", infoExtensions); + } + } + + private void addRestSupportingFiles() { + supportingFiles.add(new SupportingFile("namespace_handler.mustache", springFolder(), + convertFirstChartToUpperCase(apiPrefix) + "NamespaceHandler.java")); + supportingFiles.add(new SupportingFile("schema.mustache", schemaFolder(), + apiPrefix.toLowerCase() + "-api.xsd")); + } + + private void addSoapSupportingFiles() { + // Remove the default api template file + apiTemplateFiles().remove("api.mustache"); + apiTemplateFiles().put("api_soap.mustache", ".java"); + + supportingFiles.add(new SupportingFile("namespace_handler_soap.mustache", springFolder(), + convertFirstChartToUpperCase(apiPrefix) + "NamespaceHandler.java")); + supportingFiles.add(new SupportingFile("schema_soap.mustache", schemaFolder(), + apiPrefix.toLowerCase() + "-api.xsd")); + } + + private void addDefaultSupportingFiles() { + supportingFiles.add(new SupportingFile("api_locator.mustache", invokerFolder(), + convertFirstChartToUpperCase(apiPrefix) + "OpenApi.java")); + supportingFiles.add(new SupportingFile("bean_configuration.mustache", springFolder(), + convertFirstChartToUpperCase(apiPrefix) + "BeanConfiguration.java")); + } + + @Override + public CodegenParameter fromRequestBody(RequestBody body, Set imports, + String bodyParameterName) { + CodegenParameter codegenParameter = super.fromRequestBody(body, imports, bodyParameterName); + return convertToCustomCodegenParameter(codegenParameter); + } + + @Override + public CodegenParameter fromFormProperty(String name, Schema propertySchema, + Set imports) { + CodegenParameter codegenParameter = super.fromFormProperty(name, propertySchema, imports); + return convertToCustomCodegenParameter(codegenParameter); + } + + @Override + public CodegenParameter fromParameter(Parameter parameter, Set imports) { + CodegenParameter codegenParameter = super.fromParameter(parameter, imports); + + if ("File".equals(codegenParameter.dataType)) { + codegenParameter.dataType = "Resource"; + } + + return convertToCustomCodegenParameter(codegenParameter); + } + + /** + * Converts given codegenParameter to a custom {@link CustomCodegenParameter} to provide + * additional derived properties. + */ + private CustomCodegenParameter convertToCustomCodegenParameter( + CodegenParameter codegenParameter) { + CustomCodegenParameter customCodegenParameter = new CustomCodegenParameter(); + copyFields(CodegenParameter.class, codegenParameter, customCodegenParameter); + + customCodegenParameter.isBaseTypeString = + codegenParameter.isString || "String".equals(codegenParameter.baseType); + + return customCodegenParameter; + } + + @Override + public CodegenOperation fromOperation(String path, + String httpMethod, + Operation operation, + List servers) { + CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers); + return convertToCustomCodegenOperation(op); + } + + /** + * Converts given codegenOperation to a custom {@link CustomCodegenOperation} to provide + * additional derived properties. + */ + private CustomCodegenOperation convertToCustomCodegenOperation( + CodegenOperation codegenOperation) { + CustomCodegenOperation customOperation = new CustomCodegenOperation(); + + copyFields(CodegenOperation.class, codegenOperation, customOperation); + + customOperation.requiredNonBodyParams.addAll(customOperation.requiredParams + .stream() + .filter(param -> !param.isBodyParam).toList()); + + customOperation.needsConstructorWithAllStringParameter = + !customOperation.requiredParams.isEmpty() && + customOperation.requiredParams + .stream() + .anyMatch(param -> !param.isBodyParam && !"String".equals(param.dataType)); + + if (customOperation.optionalParams != null) { + customOperation.optionalAndAuthParameterNames.addAll( + customOperation.optionalParams.stream() + .map(codegenParameter -> toVarName(codegenParameter.nameInCamelCase)) + .toList()); + } + + return customOperation; + } + + @Override + public OperationsMap postProcessOperationsWithModels(OperationsMap objs, + List allModels) { + OperationsMap operationsMap = super.postProcessOperationsWithModels(objs, allModels); + + OperationMap operations = objs.getOperations(); + List operationList = operations.getOperation(); + if (operationList != null) { + operationList.forEach(codegenOperation -> { + if (codegenOperation instanceof CustomCodegenOperation customCodegenOperation + && customCodegenOperation.authMethods != null) { + postProcessSecurityParameters(customCodegenOperation); + } + }); + } + + return operationsMap; + } + + static class CustomCodegenOperation extends CodegenOperation { + + private final List requiredNonBodyParams; + + /** + * List of all optional parameters plus all authentication specific parameter names. + */ + private final List optionalAndAuthParameterNames; + + private boolean needsConstructorWithAllStringParameter; + + private boolean authWithParameters; + + private boolean hasApiKeyAuth; + + public CustomCodegenOperation() { + super(); + + requiredNonBodyParams = new ArrayList<>(); + optionalAndAuthParameterNames = new ArrayList<>(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + CustomCodegenOperation that = (CustomCodegenOperation) o; + return needsConstructorWithAllStringParameter + == that.needsConstructorWithAllStringParameter + && hasApiKeyAuth == that.hasApiKeyAuth + && Objects.equals(requiredNonBodyParams, that.requiredNonBodyParams) + && Objects.equals(optionalAndAuthParameterNames, + that.optionalAndAuthParameterNames); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), + requiredNonBodyParams, + optionalAndAuthParameterNames, + needsConstructorWithAllStringParameter, + hasApiKeyAuth); + } + } + + static class CustomCodegenParameter extends CodegenParameter { + + boolean isBaseTypeString; + + public CustomCodegenParameter() { + super(); + } + + @Override + public CustomCodegenParameter copy() { + CodegenParameter copy = super.copy(); + CustomCodegenParameter customCopy = new CustomCodegenParameter(); + + copyFields(CodegenParameter.class, copy, customCopy); + customCopy.isBaseTypeString = isBaseTypeString; + + return customCopy; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + CustomCodegenParameter that = (CustomCodegenParameter) o; + return isBaseTypeString == that.isBaseTypeString; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), isBaseTypeString); + } + } + + public String invokerFolder() { + return (sourceFolder + File.separator + invokerPackage.replace('.', + File.separatorChar)).replace('/', File.separatorChar); + } + + public String springFolder() { + return invokerFolder() + File.separator + "spring"; + } + + public String schemaFolder() { + return resourceFolder + File.separator + generatedSchemaFolder; + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/java/org/citrusframework/openapi/generator/WsdlToOpenApiTransformer.java b/test-api-generator/citrus-test-api-generator-core/src/main/java/org/citrusframework/openapi/generator/WsdlToOpenApiTransformer.java new file mode 100644 index 0000000000..05bed786a9 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/java/org/citrusframework/openapi/generator/WsdlToOpenApiTransformer.java @@ -0,0 +1,275 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.generator; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import org.citrusframework.openapi.generator.exception.WsdlToOpenApiTransformationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; + +import javax.wsdl.Binding; +import javax.wsdl.BindingOperation; +import javax.wsdl.Definition; +import javax.wsdl.WSDLException; +import javax.wsdl.extensions.soap.SOAPOperation; +import javax.wsdl.factory.WSDLFactory; +import javax.wsdl.xml.WSDLReader; +import javax.xml.namespace.QName; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; +import static com.fasterxml.jackson.databind.MapperFeature.SORT_PROPERTIES_ALPHABETICALLY; +import static java.lang.String.format; +import static java.util.Collections.singletonList; + +/** + * Transforms a WSDL specification into a simple OpenAPI specification for usage with the OpenApiGenerator. + *

      + * This transformer primarily focuses on mapping WSDL bindings into OpenAPI operations. + * It converts SOAP operations described in the WSDL into corresponding HTTP POST operations + * in the OpenAPI format. However, it does not convert or map schema information, such as + * types or message definitions, from the WSDL. + *

      + * + *

      WSDL to OpenAPI Mapping

      + * The transformer processes the following WSDL elements and maps them to the OpenAPI specification: + *
        + *
      • WSDL Bindings: Mapped to OpenAPI paths and operations. Each binding operation is + * converted into a corresponding POST operation in OpenAPI.
      • + *
      • WSDL Operation Name: The operation name in the WSDL is used as the operation ID + * in OpenAPI and forms part of the path in the OpenAPI specification.
      • + *
      • Binding Name: The binding name (for example, "SoapBinding") is used to tag the operation + * in the OpenAPI specification, allowing operations to be grouped logically by their binding.
      • + *
      • WSDL Documentation: If available, the documentation from the WSDL is extracted and used + * as the description for the OpenAPI operation. This provides human-readable documentation for + * each operation in the OpenAPI spec.
      • + *
      + *

      + * The following elements of the WSDL are not mapped to the OpenAPI specification: + *

        + *
      • WSDL Types and Schema: The schema and type definitions from the WSDL are not included in the + * resulting OpenAPI specification. This transformer focuses solely on operations, not data models.
      • + *
      • WSDL Messages: The message parts (input/output) associated with operations are not included + * in the OpenAPI output. This transformation only extracts the operations without message payload details.
      • + *
      + * + *

      Usage Example

      + *
      + * {@code
      + * URI wsdlUri = new URI("http://example.com/my-service.wsdl");
      + * SimpleWsdlToOpenApiTransformer transformer = new SimpleWsdlToOpenApiTransformer(wsdlUri);
      + * String openApiYaml = transformer.transformToOpenApi();
      + * System.out.println(openApiYaml);
      + * }
      + * 
      + * + * @see io.swagger.v3.oas.models.OpenAPI + * @see io.swagger.v3.oas.models.Operation + * @see javax.wsdl.Definition + * @see javax.wsdl.Binding + * @see javax.wsdl.BindingOperation + */ +public class WsdlToOpenApiTransformer { + + private static final Logger logger = LoggerFactory.getLogger(WsdlToOpenApiTransformer.class); + + private static final YAMLMapper yamlMapper = (YAMLMapper) YAMLMapper.builder() + .enable(SORT_PROPERTIES_ALPHABETICALLY) + .build() + .setSerializationInclusion(NON_NULL); + + private final URI wsdlUri; + + public WsdlToOpenApiTransformer(URI wsdlUri) { + this.wsdlUri = wsdlUri; + } + + /** + * Transforms the wsdl of this transformer into a OpenApi yaml representation. + * + * @return the OpenApi yaml + * @throws WsdlToOpenApiTransformationException if the parsing fails + */ + public String transformToOpenApi() throws WsdlToOpenApiTransformationException { + try { + Definition wsdlDefinition = readWSDL(); + Map bindings = wsdlDefinition.getBindings(); + OpenAPI openAPI = transformToOpenApi(bindings); + return convertToYaml(openAPI); + } catch (Exception e) { + throw new WsdlToOpenApiTransformationException("Unable to parse wsdl", e); + } + } + + /** + * Performs the actual transformation from bindings into OpenApi operations. + */ + private OpenAPI transformToOpenApi(Map bindings) { + OpenAPI openAPI = new OpenAPI(); + openAPI.setInfo(createInfo()); + + Paths paths = new Paths(); + openAPI.setPaths(paths); + for (Entry entry : bindings.entrySet()) { + Object key = entry.getKey(); + Object value = entry.getValue(); + + if (key instanceof QName qName && value instanceof Binding binding) { + addOperations(openAPI, qName, binding); + } + } + + return openAPI; + } + + private Definition readWSDL() throws WSDLException { + logger.debug("Reading wsdl file from path: {}", wsdlUri); + + WSDLReader reader = WSDLFactory.newInstance().newWSDLReader(); + + // switch off the verbose mode + reader.setFeature("javax.wsdl.verbose", false); + reader.setFeature("javax.wsdl.importDocuments", true); + + if (logger.isDebugEnabled()) { + logger.debug("Reading the WSDL. Base uri is {}", wsdlUri); + } + + return reader.readWSDL(wsdlUri.toString()); + } + + private Info createInfo() { + Info info = new Info() + .title("Generated api from wsdl"); + + info.setDescription( + format( + "This api has been generated from the following wsdl '%s'. It's purpose is solely to serve as input for SOAP API generation. Note that only operations are extracted from the WSDL. No schema information whatsoever is generated!", + java.nio.file.Paths.get(wsdlUri).getFileName() + ) + ); + info.setVersion("1.0.0"); + + Contact contact = new Contact() + .name("org.citrusframework.openapi.generator.SimpleWsdlToOpenApiTransformer"); + info.setContact(contact); + + return info; + } + + private void addOperations(OpenAPI openApi, QName qName, Binding binding) { + String localPart = qName.getLocalPart(); + + String bindingApiName; + if (localPart.endsWith("SoapBinding")) { + bindingApiName = localPart.substring(0, localPart.length() - "SoapBinding".length()); + } else { + bindingApiName = localPart; + } + List bindingOperations = binding.getBindingOperations(); + for (Object operation : bindingOperations) { + if (operation instanceof BindingOperation bindingOperation) { + addOperation( + openApi.getPaths(), + bindingOperation.getName(), + retrieveOperationDescription(bindingOperation), + retrieveSoapAction(bindingOperation), + bindingApiName + ); + } + } + } + + private void addOperation(Paths paths, String name, String description, String soapAction, String tag) { + logger.debug("Adding operation to spec: {}", name); + + Operation postOperation = new Operation() + .operationId(name) + .description(description) + .summary(soapAction); + postOperation.tags(singletonList(tag)); + ApiResponses responses = new ApiResponses(); + ApiResponse apiResponse = new ApiResponse() + .description("Generic Response"); + responses.addApiResponse("default", apiResponse); + postOperation.responses(responses); + + PathItem pathItem = new PathItem() + .post(postOperation); + + paths.addPathItem("/" + name, pathItem); + } + + /** + * Retrieve the description of the bindingOperation via the documentation of the associated operation. + */ + private String retrieveOperationDescription(BindingOperation bindingOperation) { + StringBuilder description = new StringBuilder(); + javax.wsdl.Operation soapOperation = bindingOperation.getOperation(); + Element documentationElement = bindingOperation.getDocumentationElement(); + if (documentationElement != null) { + String documentationText = documentationElement.getTextContent().trim(); + description.append(format("%s", documentationText)); + } + + if (soapOperation != null) { + documentationElement = soapOperation.getDocumentationElement(); + if (documentationElement != null) { + String documentationText = documentationElement.getTextContent().trim(); + if (!description.isEmpty()) { + description.append(" "); + } + description.append(format("%s", documentationText)); + } + } + + return description.toString(); + } + + /** + * Retrieve the soap action. + */ + private String retrieveSoapAction(BindingOperation bindingOperation) { + String soapAction = ""; + + List extensibilityElements = bindingOperation.getExtensibilityElements(); + for (Object element : extensibilityElements) { + if (element instanceof SOAPOperation soapOperation) { + soapAction = soapOperation.getSoapActionURI(); + } + } + + return soapAction; + } + + private String convertToYaml(OpenAPI openAPI) throws JsonProcessingException { + return yamlMapper.writeValueAsString(openAPI); + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/java/org/citrusframework/openapi/generator/exception/WsdlToOpenApiTransformationException.java b/test-api-generator/citrus-test-api-generator-core/src/main/java/org/citrusframework/openapi/generator/exception/WsdlToOpenApiTransformationException.java new file mode 100644 index 0000000000..dd5946b512 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/java/org/citrusframework/openapi/generator/exception/WsdlToOpenApiTransformationException.java @@ -0,0 +1,24 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.openapi.generator.exception; + +public class WsdlToOpenApiTransformationException extends Exception { + + public WsdlToOpenApiTransformationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/test-api-generator/citrus-test-api-generator-core/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig new file mode 100644 index 0000000000..b63055e186 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -0,0 +1 @@ +org.citrusframework.openapi.generator.CitrusJavaCodegen \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api.mustache b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api.mustache new file mode 100644 index 0000000000..3c889c78b9 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api.mustache @@ -0,0 +1,648 @@ +{{! + + Copyright the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +}} +package {{package}}; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.citrusframework.util.StringUtils.isEmpty; +import static org.citrusframework.util.StringUtils.isNotEmpty; + +import static {{invokerPackage}}.{{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}OpenApi.{{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.net.URL; +import java.time.LocalDate; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.UUID; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +import org.citrusframework.openapi.testapi.GeneratedApiOperationInfo; +import org.citrusframework.openapi.testapi.ParameterStyle; +import {{requestBuilderClass}}; +import {{responseBuilderClass}}; +import org.citrusframework.openapi.testapi.TestApiUtils; +import org.citrusframework.spi.Resource; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; + +import {{invokerPackage}}.{{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}OpenApi; +{{#imports}} +import {{import}}; +{{/imports}} + +@SuppressWarnings("unused") +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} +public class {{classname}} implements GeneratedApi +{ + + @Value("${" + "{{#lambda.lowercase}}{{prefix}}{{/lambda.lowercase}}.base64-encode-api-key:#{false}}") + private boolean base64EncodeApiKey; + {{#authMethods}} + {{#isBasicBasic}} + + @Value("${" + "{{#lambda.lowercase}}{{prefix}}{{/lambda.lowercase}}.basic.username:#{null}}") + private String basicUsername; + + @Value("${" + "{{#lambda.lowercase}}{{prefix}}{{/lambda.lowercase}}.basic.password:#{null}}") + private String basicPassword; + {{/isBasicBasic}} + {{#isBasicBearer}} + + @Value("${" + "{{#lambda.lowercase}}{{prefix}}{{/lambda.lowercase}}.bearer.token:#{null}}") + private String basicAuthBearer; + {{/isBasicBearer}} + {{#isApiKey}} + + @Value("${" + "{{#lambda.lowercase}}{{prefix}}{{/lambda.lowercase}}.{{#lambda.kebabcase}}{{keyParamName}}{{/lambda.kebabcase}}:#{null}}") + private String default{{#lambda.titlecase}}{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}{{/lambda.titlecase}}; + {{/isApiKey}} + {{/authMethods}} + + private final List customizers; + + /** + * An optional default endpoint which will be passed into the requests. + */ + private final Endpoint defaultEndpoint; + + public {{classname}}(@Nullable Endpoint defaultEndpoint) { + this(defaultEndpoint, emptyList()); + } + + public {{classname}}(@Nullable Endpoint defaultEndpoint, @Nullable List customizers) { + this.defaultEndpoint = defaultEndpoint; + this.customizers = customizers; + } + + public static {{classname}} {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}(Endpoint defaultEndpoint) { + return new {{classname}}(defaultEndpoint); + } + + @Override + public String getApiTitle() { + return "{{appName}}"; + } + + @Override + public String getApiVersion() { + return "{{appVersion}}"; + } + + @Override + public String getApiPrefix() { + return "{{prefix}}"; + } + + @Override + public Map getApiInfoExtensions() { + {{#infoExtensions}} + Map infoExtensionMap = new HashMap<>(); + {{#entrySet}} + infoExtensionMap.put("{{key}}", "{{value}}"); + {{/entrySet}} + return infoExtensionMap; + {{/infoExtensions}} + {{^infoExtensions}} + return emptyMap(); + {{/infoExtensions}} + } + + @Override + @Nullable + public Endpoint getEndpoint() { + return defaultEndpoint; + } + + @Override + public List getCustomizers() { + return customizers; + } + +{{#operations}} + {{#operation}} + /** + * Builder with type safe required parameters. + */ + public {{operationIdCamelCase}}SendActionBuilder send{{operationIdCamelCase}}({{#requiredNonBodyParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/requiredNonBodyParams}}) { + {{#authWithParameters}} + {{operationIdCamelCase}}SendActionBuilder builder = new {{operationIdCamelCase}}SendActionBuilder(this{{#requiredNonBodyParams}}, {{paramName}}{{/requiredNonBodyParams}}); + {{#hasApiKeyAuth}} + builder.setBase64EncodeApiKey(base64EncodeApiKey); + {{/hasApiKeyAuth}} + {{#isBasicBasic}} + builder.setBasicAuthUsername(basicUsername); + builder.setBasicAuthPassword(basicPassword); + {{/isBasicBasic}} + {{#isBasicBearer}} + builder.setBasicAuthBearer(basicAuthBearer); + {{/isBasicBearer}} + {{#isApiKey}} + builder.set{{#lambda.titlecase}}{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}{{/lambda.titlecase}}(default{{#lambda.titlecase}}{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}{{/lambda.titlecase}}); + {{/isApiKey}} + return builder; + {{/authWithParameters}} + {{^authWithParameters}} + return new {{operationIdCamelCase}}SendActionBuilder(this{{#requiredNonBodyParams}}, {{paramName}}{{/requiredNonBodyParams}}); + {{/authWithParameters}} + } + {{#needsConstructorWithAllStringParameter}} + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public {{operationIdCamelCase}}SendActionBuilder send{{operationIdCamelCase}}$({{#requiredNonBodyParams}}{{^isArray}}String {{paramName}}Expression{{/isArray}}{{#isArray}}List {{paramName}}Expression{{/isArray}}{{^-last}}, {{/-last}} {{/requiredNonBodyParams}}) { + {{#authWithParameters}} + {{operationIdCamelCase}}SendActionBuilder builder = new {{operationIdCamelCase}}SendActionBuilder({{#requiredNonBodyParams}}{{paramName}}Expression, {{/requiredNonBodyParams}}this); + {{#hasApiKeyAuth}} + builder.setBase64EncodeApiKey(base64EncodeApiKey); + {{/hasApiKeyAuth}} + {{#authMethods}} + {{#isBasicBasic}} + builder.setBasicAuthUsername(basicUsername); + builder.setBasicAuthPassword(basicPassword); + {{/isBasicBasic}} + {{#isBasicBearer}} + builder.setBasicAuthBearer(basicAuthBearer); + {{/isBasicBearer}} + {{#isApiKey}} + builder.set{{#lambda.titlecase}}{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}{{/lambda.titlecase}}(default{{#lambda.titlecase}}{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}{{/lambda.titlecase}}); + {{/isApiKey}} + {{/authMethods}} + return builder; + {{/authWithParameters}} + {{^authWithParameters}} + return new {{operationIdCamelCase}}SendActionBuilder({{#requiredNonBodyParams}}{{paramName}}Expression, {{/requiredNonBodyParams}}this); + {{/authWithParameters}} + } + {{/needsConstructorWithAllStringParameter}} + + public {{operationIdCamelCase}}ReceiveActionBuilder receive{{operationIdCamelCase}}(@NotNull HttpStatus statusCode) { + return new {{operationIdCamelCase}}ReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public {{operationIdCamelCase}}ReceiveActionBuilder receive{{operationIdCamelCase}}(@NotNull String statusCode) { + return new {{operationIdCamelCase}}ReceiveActionBuilder(this, statusCode); + } + + {{/operation}} +{{/operations}} +{{#operations}} + {{#operation}} + public static class {{operationIdCamelCase}}SendActionBuilder extends + {{requestBuilderClassName}} implements GeneratedApiOperationInfo { + + private static final String METHOD = "{{httpMethod}}"; + + private static final String ENDPOINT = "{{#rootContextPath}}{{rootContextPath}}{{/rootContextPath}}{{^neglectBasePath}}{{basePathWithoutHost}}{{/neglectBasePath}}{{path}}"; + + private static final String OPERATION_NAME = "{{#operationIdOriginal}}{{operationId}}{{/operationIdOriginal}}{{^operationIdOriginal}}{{#lambda.uppercase}}{{httpMethod}}{{/lambda.uppercase}}_{{#rootContextPath}}{{rootContextPath}}{{/rootContextPath}}{{^neglectBasePath}}{{basePathWithoutHost}}{{/neglectBasePath}}{{path}}{{/operationIdOriginal}}"; + {{#hasApiKeyAuth}} + + @Value("${" + "{{#lambda.lowercase}}{{prefix}}{{/lambda.lowercase}}.base64-encode-api-key:#{false}}") + private boolean base64EncodeApiKey; + {{/hasApiKeyAuth}} + {{#authMethods}} + {{#isBasicBasic}} + + @Value("${" + "{{#lambda.lowercase}}{{prefix}}{{/lambda.lowercase}}.basic.username:#{null}}") + private String defaultBasicUsername; + + private String basicUsername; + + @Value("${" + "{{#lambda.lowercase}}{{prefix}}{{/lambda.lowercase}}.basic.password:#{null}}") + private String defaultBasicPassword; + + private String basicPassword; + {{/isBasicBasic}} + {{#isBasicBearer}} + + @Value("${" + "{{#lambda.lowercase}}{{prefix}}{{/lambda.lowercase}}.bearer.token:#{null}}") + private String defaultBasicAuthBearer; + + private String basicAuthBearer; + {{/isBasicBearer}} + {{#isApiKey}} + + @Value("${" + "{{#lambda.lowercase}}{{prefix}}{{/lambda.lowercase}}.{{#lambda.kebabcase}}{{keyParamName}}{{/lambda.kebabcase}}:#{null}}") + private String default{{#lambda.titlecase}}{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}{{/lambda.titlecase}}; + + private String {{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}; + {{/isApiKey}} + {{/authMethods}} + + /** + * Constructor with type safe required parameters. + */ + public {{operationIdCamelCase}}SendActionBuilder({{classname}} {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}{{#requiredNonBodyParams}}, {{{dataType}}} {{paramName}}{{/requiredNonBodyParams}}) { + super({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}, {{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification, METHOD, ENDPOINT, OPERATION_NAME); + {{#requiredNonBodyParams}} + {{#isQueryParam}} + queryParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isQueryParam}} + {{#isPathParam}} + pathParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isPathParam}} + {{#isHeaderParam}} + headerParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isHeaderParam}} + {{#isFormParam}} + formParameter("{{baseName}}", {{#isBinary}}toBinary({{paramName}}){{/isBinary}} {{^isBinary}}{{paramName}}{{/isBinary}}); + {{/isFormParam}} + {{#isCookieParam}} + cookieParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isCookieParam}} + {{/requiredNonBodyParams}} + } + {{#needsConstructorWithAllStringParameter}} + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + {{! + Note that the change in the order of parameters is intentional to be able to differentiate + constructors with Collections parameters which would otherwise have the same erasure type. + }} + public {{operationIdCamelCase}}SendActionBuilder({{#requiredNonBodyParams}}{{^isArray}}String {{paramName}}Expression{{/isArray}}{{#isArray}}List {{paramName}}Expression{{/isArray}}, {{/requiredNonBodyParams}}{{classname}} {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}) { + super({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}, {{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification, METHOD, ENDPOINT, OPERATION_NAME); + {{#requiredNonBodyParams}} + {{#isQueryParam}} + queryParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isQueryParam}} + {{#isPathParam}} + pathParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isPathParam}} + {{#isHeaderParam}} + headerParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isHeaderParam}} + {{#isFormParam}} + formParameter("{{baseName}}", {{#isBinary}}toBinary({{paramName}}Expression){{/isBinary}} {{^isBinary}}{{paramName}}Expression{{/isBinary}}); + {{/isFormParam}} + {{#isCookieParam}} + cookieParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isCookieParam}} + {{/requiredNonBodyParams}} + } + {{/needsConstructorWithAllStringParameter}} + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + {{#requiredNonBodyParams}} + {{#-first}} + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + {{/-first}} + {{/requiredNonBodyParams}} + public {{operationIdCamelCase}}SendActionBuilder({{classname}} {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}, TestApiClientRequestMessageBuilder messageBuilder{{#requiredNonBodyParams}}, {{^isArray}}String {{paramName}}Expression{{/isArray}}{{#isArray}}List {{paramName}}{{/isArray}}{{/requiredNonBodyParams}}) { + super({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}, {{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + {{#requiredNonBodyParams}} + {{#isQueryParam}} + queryParameter("{{baseName}}", {{paramName}}{{^isArray}}Expression{{/isArray}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isQueryParam}} + {{#isPathParam}} + pathParameter("{{baseName}}", {{paramName}}{{^isArray}}Expression{{/isArray}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isPathParam}} + {{#isHeaderParam}} + headerParameter("{{baseName}}", {{paramName}}{{^isArray}}Expression{{/isArray}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isHeaderParam}} + {{#isFormParam}} + formParameter("{{baseName}}", {{#isBinary}}toBinary({{paramName}}{{^isArray}}Expression{{/isArray}}){{/isBinary}} {{^isBinary}}{{paramName}}{{^isArray}}Expression{{/isArray}}{{/isBinary}}); + {{/isFormParam}} + {{#isCookieParam}} + cookieParameter("{{baseName}}", {{paramName}}{{^isArray}}Expression{{/isArray}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isCookieParam}} + {{/requiredNonBodyParams}} + } + {{#requiredNonBodyParams}} + + {{! + Type safe setting of parameter, using builder pattern. + }} + public {{operationIdCamelCase}}SendActionBuilder {{paramName}}({{#isArray}}{{baseType}}...{{/isArray}}{{^isArray}}{{dataType}} {{/isArray}}{{paramName}}) { + {{#isQueryParam}} + queryParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isQueryParam}} + {{#isPathParam}} + pathParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isPathParam}} + {{#isHeaderParam}} + headerParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isHeaderParam}} + {{#isFormParam}} + formParameter("{{baseName}}", {{#isBinary}}toBinary({{paramName}}){{/isBinary}} {{^isBinary}}{{paramName}}{{/isBinary}}); + {{/isFormParam}} + {{#isCookieParam}} + cookieParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isCookieParam}} + return this; + } + {{^isBaseTypeString}} + + {{! + Non type safe setting of parameter, using builder pattern. + }} + public {{operationIdCamelCase}}SendActionBuilder {{paramName}}(String{{#isArray}}...{{/isArray}}{{^isArray}} {{/isArray}}{{paramName}}Expression) { + {{#isQueryParam}} + queryParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isQueryParam}} + {{#isPathParam}} + pathParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isPathParam}} + {{#isHeaderParam}} + headerParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isHeaderParam}} + {{#isFormParam}} + formParameter("{{baseName}}", {{#isBinary}}toBinary({{paramName}}Expression){{/isBinary}} {{^isBinary}}{{paramName}}Expression{{/isBinary}}); + {{/isFormParam}} + {{#isCookieParam}} + cookieParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isCookieParam}} + return this; + } + {{/isBaseTypeString}} + {{/requiredNonBodyParams}} + {{#optionalParams}} + + {{! + Type safe setting of the parameter value. + Depending on the parameter type (query, path, header, form, or cookie), + the appropriate method is invoked to set the parameter. + }} + public {{operationIdCamelCase}}SendActionBuilder {{paramName}}({{#isArray}}{{baseType}}...{{/isArray}}{{^isArray}}{{dataType}} {{/isArray}}{{paramName}}) { + {{#isQueryParam}} + queryParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isQueryParam}} + {{#isPathParam}} + pathParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isPathParam}} + {{#isHeaderParam}} + headerParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isHeaderParam}} + {{#isFormParam}} + formParameter("{{baseName}}", {{#isBinary}}toBinary({{paramName}}){{/isBinary}} {{^isBinary}}{{paramName}}{{/isBinary}}); + {{/isFormParam}} + {{#isCookieParam}} + cookieParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isCookieParam}} + return this; + } + + {{! + Type safe setting of the parameter value. + These setters are used by SendApiRequestActionParser and ReceiveApiResponseActionParser + to inject attributes into the request or response. + Depending on the parameter type (query, path, header, form, or cookie), + the appropriate method is invoked to set the parameter. + }} + public void set{{#lambda.titlecase}}{{paramName}}{{/lambda.titlecase}}({{#isArray}}{{baseType}}...{{/isArray}}{{^isArray}}{{dataType}} {{/isArray}}{{paramName}}) { + {{#isQueryParam}} + queryParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isQueryParam}} + {{#isPathParam}} + pathParameter("{{baseName}}", {{paramName}}); + {{/isPathParam}} + {{#isHeaderParam}} + headerParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isHeaderParam}} + {{#isFormParam}} + formParameter("{{baseName}}", {{#isBinary}}toBinary({{paramName}}){{/isBinary}} {{^isBinary}}{{paramName}}{{/isBinary}}); + {{/isFormParam}} + {{#isCookieParam}} + cookieParameter("{{baseName}}", {{paramName}}, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isCookieParam}} + } + {{^isBaseTypeString}} + + {{! + Configures a request action by setting the value of a parameter + (query, path, header, form, or cookie) as a dynamic string expression, + supporting citrus variables. This method handles different parameter types + based on the context and updates the respective parameter accordingly. + Supports arrays if the parameter is marked as such. + }} + public {{operationIdCamelCase}}SendActionBuilder {{paramName}}(String{{#isArray}}...{{/isArray}}{{^isArray}} {{/isArray}}{{paramName}}Expression) { + {{#isQueryParam}} + queryParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isQueryParam}} + {{#isPathParam}} + pathParameter("{{baseName}}", {{paramName}}Expression); + {{/isPathParam}} + {{#isHeaderParam}} + headerParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isHeaderParam}} + {{#isFormParam}} + formParameter("{{baseName}}", {{#isBinary}}toBinary({{paramName}}Expression){{/isBinary}} {{^isBinary}}{{paramName}}Expression{{/isBinary}}); + {{/isFormParam}} + {{#isCookieParam}} + cookieParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isCookieParam}} + return this; + } + + {{! + Sets the value of the parameter as a string expression. + These setters are used by SendApiRequestActionParser and ReceiveApiResponseActionParser + to inject attributes into the request or response. + Depending on the parameter type (query, path, header, form, or cookie), + the appropriate method is invoked to set the parameter. + If the parameter is marked as an array, multiple values are supported. + }} + public void set{{#lambda.titlecase}}{{paramName}}{{/lambda.titlecase}}(String{{#isArray}}...{{/isArray}}{{^isArray}} {{/isArray}}{{paramName}}Expression) { + {{#isQueryParam}} + queryParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isQueryParam}} + {{#isPathParam}} + pathParameter("{{baseName}}", {{paramName}}Expression); + {{/isPathParam}} + {{#isHeaderParam}} + headerParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isHeaderParam}} + {{#isFormParam}} + formParameter("{{baseName}}", {{#isBinary}}toBinary({{paramName}}Expression){{/isBinary}} {{^isBinary}}{{paramName}}Expression{{/isBinary}}); + {{/isFormParam}} + {{#isCookieParam}} + cookieParameter("{{baseName}}", {{paramName}}Expression, {{#style}}ParameterStyle.{{#lambda.uppercase}}{{style}}{{/lambda.uppercase}}{{/style}}{{^style}}ParameterStyle.NONE{{/style}}, {{isExplode}}, {{#schema.isModel}}{{schema.isModel}}{{/schema.isModel}}{{^schema.isModel}}false{{/schema.isModel}}); + {{/isCookieParam}} + } + {{/isBaseTypeString}} + {{/optionalParams}} + {{#hasApiKeyAuth}} + + public void setBase64EncodeApiKey(boolean encode) { + this.base64EncodeApiKey = encode; + } + {{/hasApiKeyAuth}} + {{#authMethods}} + {{#isBasicBasic}} + + public {{operationIdCamelCase}}SendActionBuilder basicAuthUsername(String basicUsername) { + this.basicUsername = basicUsername; + return this; + } + + public void setBasicAuthUsername(String basicUsername) { + this.basicUsername = basicUsername; + } + + public {{operationIdCamelCase}}SendActionBuilder basicAuthPassword(String password) { + this.basicPassword = password; + return this; + } + + public void setBasicAuthPassword(String password) { + this.basicPassword = password; + } + + protected void addBasicAuthHeader(String basicUsername, String basicPassword, + HttpMessageBuilderSupport messageBuilderSupport) { + TestApiUtils.addBasicAuthHeader( + isNotEmpty(basicUsername) ? basicUsername : defaultBasicUsername, + isNotEmpty(basicPassword) ? basicPassword : defaultBasicPassword, + messageBuilderSupport); + } + {{/isBasicBasic}} + {{#isBasicBearer}} + + public {{operationIdCamelCase}}SendActionBuilder basicAuthBearer(String basicAuthBearer) { + this.basicAuthBearer = basicAuthBearer; + return this; + } + + public void setBasicAuthBearer(String basicAuthBearer) { + this.basicAuthBearer = basicAuthBearer; + } + {{/isBasicBearer}} + {{#isApiKey}} + + public {{operationIdCamelCase}}SendActionBuilder {{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}(String {{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}) { + this.{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}} = {{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}; + return this; + } + + public void set{{#lambda.titlecase}}{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}{{/lambda.titlecase}}(String {{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}) { + this.{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}} = {{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}; + } + {{/isApiKey}} + {{/authMethods}} + + @Override + public SendMessageAction doBuild() { + {{#authMethods}} + {{#isBasicBasic}} + addBasicAuthHeader(basicUsername, basicPassword, getMessageBuilderSupport()); + {{/isBasicBasic}} + {{#isBasicBearer}} + if (!isEmpty(basicAuthBearer) || !isEmpty(defaultBasicAuthBearer)) { + headerParameter("Authorization", "Bearer " +getOrDefault(basicAuthBearer, defaultBasicAuthBearer, true)); + } + {{/isBasicBearer}} + {{/authMethods}} + {{#authMethods}} + {{#isApiKey}} + {{#isKeyInHeader}} + headerParameter("{{keyParamName}}", getOrDefault({{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}, default{{#lambda.titlecase}}{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}{{/lambda.titlecase}}, base64EncodeApiKey)); + {{/isKeyInHeader}} + {{#isKeyInQuery}} + queryParameter("{{keyParamName}}", getOrDefault({{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}, default{{#lambda.titlecase}}{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}{{/lambda.titlecase}}, base64EncodeApiKey)); + {{/isKeyInQuery}} + {{#isKeyInCookie}} + cookieParameter("{{keyParamName}}", getOrDefault({{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}, default{{#lambda.titlecase}}{{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}}{{/lambda.titlecase}}, base64EncodeApiKey)); + {{/isKeyInCookie}} + {{/isApiKey}} + {{/authMethods}} + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class {{operationIdCamelCase}}ReceiveActionBuilder extends + {{responseBuilderClassName}} implements GeneratedApiOperationInfo { + + private static final String METHOD = "{{httpMethod}}"; + + private static final String ENDPOINT = "{{#rootContextPath}}{{rootContextPath}}{{/rootContextPath}}{{^neglectBasePath}}{{basePathWithoutHost}}{{/neglectBasePath}}{{path}}"; + + private static final String OPERATION_NAME = "{{#operationIdOriginal}}{{operationId}}{{/operationIdOriginal}}{{^operationIdOriginal}}{{#lambda.uppercase}}{{httpMethod}}{{/lambda.uppercase}}_{{#rootContextPath}}{{rootContextPath}}{{/rootContextPath}}{{^neglectBasePath}}{{basePathWithoutHost}}{{/neglectBasePath}}{{path}}{{/operationIdOriginal}}"; + + public {{operationIdCamelCase}}ReceiveActionBuilder({{classname}} {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}, String statusCode) { + super({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}, {{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public {{operationIdCamelCase}}ReceiveActionBuilder({{classname}} {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}, OpenApiClientResponseMessageBuilder messageBuilder) { + super({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}, {{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + {{^-last}} + + {{/-last}} + {{/operation}} +{{/operations}} +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_doc.mustache b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_doc.mustache new file mode 100644 index 0000000000..f8737ed4d9 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_doc.mustache @@ -0,0 +1 @@ +# not in use diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_locator.mustache b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_locator.mustache new file mode 100644 index 0000000000..522f3a188e --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_locator.mustache @@ -0,0 +1,27 @@ +{{! + + Copyright the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +}} +package {{invokerPackage}}; + +import org.citrusframework.openapi.OpenApiSpecification; + +public class {{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}OpenApi { + + public static final OpenApiSpecification {{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification = OpenApiSpecification + .from({{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}OpenApi.class.getResource("{{prefix}}_openApi.yaml")){{#neglectBasePath}}.neglectBasePath({{neglectBasePath}}){{/neglectBasePath}}{{#rootContextPath}}.withRootContext("{{rootContextPath}}"){{/rootContextPath}}; + +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_soap.mustache b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_soap.mustache new file mode 100644 index 0000000000..d0a65618da --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_soap.mustache @@ -0,0 +1,190 @@ +{{! + + Copyright the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +}} +package {{package}}; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; + +import jakarta.annotation.Nullable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.citrusframework.openapi.testapi.GeneratedApiOperationInfo; +import org.citrusframework.ws.actions.ReceiveSoapMessageAction; +import org.citrusframework.ws.actions.SendSoapMessageAction; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.citrusframework.openapi.testapi.SoapApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.testapi.SoapApiSendMessageActionBuilder; + +@SuppressWarnings("unused") +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} +public class {{classname}} implements GeneratedApi +{ + + /** + * An optional default endpoint which will be passed into the requests. + */ + private final Endpoint defaultEndpoint; + + private final List customizers; + + public {{classname}}(@Nullable Endpoint defaultEndpoint) { + this(defaultEndpoint, emptyList()); + } + + public {{classname}}(@Nullable Endpoint defaultEndpoint, @Nullable List customizers) { + this.defaultEndpoint = defaultEndpoint; + this.customizers = customizers; + } + + public static {{classname}} {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}(Endpoint defaultEndpoint) { + return new {{classname}}(defaultEndpoint); + } + + @Override + public String getApiTitle() { + return "{{appName}}"; + } + + @Override + public String getApiVersion() { + return "{{appVersion}}"; + } + + @Override + public String getApiPrefix() { + return "{{prefix}}"; + } + + @Override + public Map getApiInfoExtensions() { + {{#infoExtensions}} + Map infoExtensionMap = new HashMap<>(); + {{#entrySet}} + infoExtensionMap.put("{{key}}", "{{value}}"); + {{/entrySet}} + return infoExtensionMap; + {{/infoExtensions}} + {{^infoExtensions}} + return emptyMap(); + {{/infoExtensions}} + } + + @Override + @Nullable + public Endpoint getEndpoint() { + return defaultEndpoint; + } + + @Override + public List getCustomizers() { + return customizers; + } + +{{#operations}} + {{#operation}} + public {{operationIdCamelCase}}SendActionBuilder send{{operationIdCamelCase}}({{#requiredNonBodyParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/requiredNonBodyParams}}) { + return new {{operationIdCamelCase}}SendActionBuilder(this); + } + + public {{operationIdCamelCase}}ReceiveActionBuilder receive{{operationIdCamelCase}}() { + return new {{operationIdCamelCase}}ReceiveActionBuilder(this); + } + + {{/operation}} +{{/operations}} +{{#operations}} + {{#operation}} + public static class {{operationIdCamelCase}}SendActionBuilder extends {{requestBuilderClassName}} implements + GeneratedApiOperationInfo { + + private static final String SOAP_ACTION = "{{summary}}"; + + public {{operationIdCamelCase}}SendActionBuilder({{classname}} {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}) { + super({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}, SOAP_ACTION); + } + + @Override + public String getOperationName() { + return SOAP_ACTION; + } + + @Override + public String getMethod() { + return "POST"; + } + + @Override + public String getPath() { + return SOAP_ACTION; + } + + @Override + public SendSoapMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class {{operationIdCamelCase}}ReceiveActionBuilder extends {{responseBuilderClassName}} implements + GeneratedApiOperationInfo { + + private static final String SOAP_ACTION = "{{summary}}"; + + public {{operationIdCamelCase}}ReceiveActionBuilder({{classname}} {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}) { + super({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}, SOAP_ACTION); + } + + @Override + public String getOperationName() { + return SOAP_ACTION; + } + + @Override + public String getMethod() { + return "POST"; + } + + @Override + public String getPath() { + return SOAP_ACTION; + } + + @Override + public ReceiveSoapMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + {{^-last}} + + {{/-last}} + {{/operation}} +{{/operations}} +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_test.mustache b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_test.mustache new file mode 100644 index 0000000000..d5341fea2c --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/api_test.mustache @@ -0,0 +1 @@ +// not in use diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/bean_configuration.mustache b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/bean_configuration.mustache new file mode 100644 index 0000000000..47798c712d --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/bean_configuration.mustache @@ -0,0 +1,58 @@ +{{! + + Copyright the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +}} +package {{invokerPackage}}.spring; + +import static {{invokerPackage}}.{{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}OpenApi.{{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification; + +import java.util.List; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.OpenApiRepository; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +{{#apiInfo}} +{{#apis}} +import {{package}}.{{classname}}; +{{/apis}} +{{/apiInfo}} +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import {{invokerPackage}}.{{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}OpenApi; + +@Configuration +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} +public class {{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}BeanConfiguration { + + @Bean + public OpenApiRepository {{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}OpenApiRepository() { + var openApiRepository = new OpenApiRepository(); + openApiRepository.getOpenApiSpecifications().add({{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification); + return openApiRepository; + } + +{{#apiInfo}} + {{#apis}} + @Bean(name="{{classname}}") + public {{classname}} {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}(@Autowired(required = false) @Qualifier("{{apiEndpoint}}") Endpoint defaultEndpoint, @Autowired(required = false) List customizers) { + return new {{classname}}(defaultEndpoint, customizers); + } + + {{/apis}} +{{/apiInfo}} +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/licenseInfo.mustache b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/licenseInfo.mustache new file mode 100644 index 0000000000..c719a5151a --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/licenseInfo.mustache @@ -0,0 +1,15 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/namespace_handler.mustache b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/namespace_handler.mustache new file mode 100644 index 0000000000..40c3e5e3d1 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/namespace_handler.mustache @@ -0,0 +1,77 @@ +{{! + + Copyright the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +}} +package {{invokerPackage}}.spring; + +import static {{invokerPackage}}.{{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}OpenApi.{{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification; + +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.RestApiSendMessageActionBuilder; +import org.citrusframework.openapi.testapi.RestApiReceiveMessageActionBuilder; +{{#apiInfo}} +{{#apis}} +import {{package}}.{{classname}}; +{{/apis}} +{{/apiInfo}} +import org.citrusframework.openapi.testapi.spring.RestApiReceiveMessageActionParser; +import org.citrusframework.openapi.testapi.spring.RestApiSendMessageActionParser; +import {{invokerPackage}}.{{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}OpenApi; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} +public class {{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}NamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + {{#apiInfo}} + {{#apis}} + {{#operations}} + {{#operation}} + + registerOperationParsers({{classname}}.class,"{{#lambda.kebabcase}}{{operationId}}{{/lambda.kebabcase}}", "{{#operationIdOriginal}}{{operationId}}{{/operationIdOriginal}}{{^operationIdOriginal}}{{#lambda.uppercase}}{{httpMethod}}{{/lambda.uppercase}}_{{#rootContextPath}}{{rootContextPath}}{{/rootContextPath}}{{^neglectBasePath}}{{basePathWithoutHost}}{{/neglectBasePath}}{{path}}{{/operationIdOriginal}}", "{{path}}", + {{classname}}.{{operationIdCamelCase}}SendActionBuilder.class, + {{classname}}.{{operationIdCamelCase}}ReceiveActionBuilder.class, + new String[]{ {{#requiredNonBodyParams}}"{{paramName}}{{^isString}}{{/isString}}"{{^-last}}, {{/-last}}{{/requiredNonBodyParams}} }, + new String[]{ {{#optionalAndAuthParameterNames}}"{{.}}"{{^-last}}, {{/-last}}{{/optionalAndAuthParameterNames}} }); + {{/operation}} + {{/operations}} + {{/apis}} + {{/apiInfo}} + } + + private void registerOperationParsers(Class apiClass, String elementName, String operationName, String path, + Class sendBeanClass, + Class receiveBeanClass, + String[] constructorParameters, + String[] nonConstructorParameters) { + + RestApiSendMessageActionParser sendParser = new RestApiSendMessageActionParser({{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification, operationName, + path, + apiClass, + sendBeanClass, + receiveBeanClass, + "{{apiEndpoint}}"); + sendParser.setConstructorParameters(constructorParameters); + sendParser.setNonConstructorParameters(nonConstructorParameters); + registerBeanDefinitionParser("send-"+elementName, sendParser); + + RestApiReceiveMessageActionParser receiveParser = new RestApiReceiveMessageActionParser({{#lambda.camelcase}}{{prefix}}{{/lambda.camelcase}}Specification, + operationName, apiClass, receiveBeanClass, "{{apiEndpoint}}"); + registerBeanDefinitionParser("receive-"+elementName, receiveParser); + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/namespace_handler_soap.mustache b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/namespace_handler_soap.mustache new file mode 100644 index 0000000000..3c8b9b088f --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/namespace_handler_soap.mustache @@ -0,0 +1,68 @@ +{{! + + Copyright the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +}} +package {{invokerPackage}}.spring; + +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.SoapApiSendMessageActionBuilder; +import org.citrusframework.openapi.testapi.SoapApiReceiveMessageActionBuilder; +{{#apiInfo}} +{{#apis}} +import {{package}}.{{classname}}; +{{/apis}} +{{/apiInfo}} +import org.citrusframework.openapi.testapi.spring.SoapApiReceiveMessageActionParser; +import org.citrusframework.openapi.testapi.spring.SoapApiSendMessageActionParser; +import {{invokerPackage}}.{{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}OpenApi; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} +public class {{#lambda.titlecase}}{{prefix}}{{/lambda.titlecase}}NamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + {{#apiInfo}} + {{#apis}} + {{#operations}} + {{#operation}} + + registerOperationParsers({{classname}}.class,"{{#lambda.kebabcase}}{{operationId}}{{/lambda.kebabcase}}", + {{classname}}.{{operationIdCamelCase}}SendActionBuilder.class, + {{classname}}.{{operationIdCamelCase}}ReceiveActionBuilder.class); + {{/operation}} + {{/operations}} + {{/apis}} + {{/apiInfo}} + } + + private void registerOperationParsers(Class apiClass, String elementName, + Class sendBeanClass, + Class receiveBeanClass) { + + SoapApiSendMessageActionParser sendParser = new SoapApiSendMessageActionParser( + apiClass, + sendBeanClass, + receiveBeanClass, + "{{apiEndpoint}}"); + registerBeanDefinitionParser("send-"+elementName, sendParser); + + SoapApiReceiveMessageActionParser receiveParser = new SoapApiReceiveMessageActionParser( + apiClass, receiveBeanClass, "{{apiEndpoint}}"); + registerBeanDefinitionParser("receive-"+elementName, receiveParser); + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/schema.mustache b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/schema.mustache new file mode 100644 index 0000000000..54ab56c2f7 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/schema.mustache @@ -0,0 +1,425 @@ +{{! + + Copyright the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{^isMultipart}} + + + + {{^required}}Optional {{/required}}Body - {{summary}}{{#description}} +

      {{description}}

      {{/description}} +
      +
      +
      + {{/isMultipart}} +
      +
      +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{#apiInfo}} + {{#apis}} + {{#operations}} + {{#operation}} + + + + + {{notes}} + {{operationId}} +

      {{httpMethod}} {{{path}}}

      +
        + {{#queryParams}} +
      • {{paramName}} {{description}}
      • + {{/queryParams}} + {{#pathParams}} +
      • {{baseName}} {{description}}
      • + {{/pathParams}} + {{#bodyParams}} +
      • Body: {{description}}
      • + {{/bodyParams}} + {{#authMethods}} + {{#isBasicBasic}} +
      • basicAuthUsername http basic authentication username
      • +
      • basicAuthPassword http basic authentication password
      • + {{/isBasicBasic}} + {{#isBasicBearer}} +
      • basicAuthBearer http basic authentication bearer token
      • + {{/isBasicBearer}} + {{#isApiKey}} +
      • {{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}} {{#lambda.camelcase}}{{keyParamName}}{{/lambda.camelcase}} authentication token
      • + {{/isApiKey}} + {{/authMethods}} + {{#isMultipart}} + {{#formParams}} +
      • {{paramName}} {{description}}
      • + {{/formParams}} + {{/isMultipart}} +
      +
      +
      + + + + {{#queryParams}} + {{#isArray}} + + {{#description}} + + {{description}} + + {{/description}} + + {{/isArray}} + {{/queryParams}} + {{#headerParams}} + {{#isArray}} + + {{#description}} + + {{description}} + + {{/description}} + + {{/isArray}} + {{/headerParams}} + {{#pathParams}} + {{#isArray}} + + {{#description}} + + {{description}} + + {{/description}} + + {{/isArray}} + {{/pathParams}} + {{#formParams}} + {{#isArray}} + + {{#description}} + + {{description}} + + {{/description}} + + {{/isArray}} + {{/formParams}} + {{#cookieParams}} + {{#isArray}} + + {{#description}} + + {{description}} + + {{/description}} + + {{/isArray}} + {{/cookieParams}} + + + {{#queryParams}} + {{^isArray}} + + {{#description}} + + {{description}} + + {{/description}} + + {{/isArray}} + {{/queryParams}} + {{#headerParams}} + {{^isArray}} + + {{#description}} + + {{description}} + + {{/description}} + + {{/isArray}} + {{/headerParams}} + {{#pathParams}} + {{^isArray}} + + {{#description}} + + {{description}} + + {{/description}} + + {{/isArray}} + {{/pathParams}} + {{#formParams}} + {{^isArray}} + + {{#description}} + + {{description}} + + {{/description}} + + {{/isArray}} + {{/formParams}} + {{#cookieParams}} + {{^isArray}} + + {{#description}} + + {{description}} + + {{/description}} + + {{/isArray}} + {{/cookieParams}} + {{#authMethods}} + {{#isBasicBasic}} + + + http basic authentication username + + + + + http basic authentication password + + + {{/isBasicBasic}} + {{#isBasicBearer}} + + + http basic authentication bearer token + + + {{/isBasicBearer}} + {{#isApiKey}} + + {{#description}} + + {{description}} + + {{/description}} + + {{/isApiKey}} + {{/authMethods}} + + +
      + + + + + {{notes}} + {{operationId}} +

      {{httpMethod}} {{{path}}}

      +
        + {{#queryParams}} +
      • {{paramName}} {{description}}
      • + {{/queryParams}} + {{#pathParams}} +
      • {{baseName}} {{description}}
      • + {{/pathParams}} + {{#bodyParams}} +
      • Body: {{description}}
      • + {{/bodyParams}} + {{#isMultipart}} + {{#formParams}} +
      • {{paramName}} {{description}}
      • + {{/formParams}} + {{/isMultipart}} +
      +
      +
      + + + {{#responses}} + {{#-first}} + + + + An enumeration of all specified API response codes. + + + + + {{/-first}} + + + + {{#message}} + Message: {{message}}
      + {{/message}} + {{#dataType}} + Datatype: {{dataType}}
      + {{/dataType}} +
      +
      +
      + {{#-last}} +
      +
      +
      + {{/-last}} + {{/responses}} + +
      +
      +
      + {{/operation}} + {{/operations}} + {{/apis}} + {{#apis}} + {{#operations}} + {{#operation}} + + + + {{/operation}} + {{/operations}} + {{/apis}} + {{/apiInfo}} +
      diff --git a/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/schema_soap.mustache b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/schema_soap.mustache new file mode 100644 index 0000000000..39fac4680c --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/main/resources/java-citrus/schema_soap.mustache @@ -0,0 +1,72 @@ +{{! + + Copyright the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +}} + + + + + + {{#apiInfo}} + {{#apis}} + {{#operations}} + {{#operation}} + + + + {{notes}} + + + + + + + + + + + + + + + {{notes}} + + + + + + + {{/operation}} + {{/operations}} + {{/apis}} + {{#apis}} + {{#operations}} + {{#operation}} + + + + {{/operation}} + {{/operations}} + {{/apis}} + {{/apiInfo}} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/CitrusJavaCodegenTest.java b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/CitrusJavaCodegenTest.java new file mode 100644 index 0000000000..5e12e63c91 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/CitrusJavaCodegenTest.java @@ -0,0 +1,199 @@ +package org.citrusframework.openapi.generator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.citrusframework.openapi.generator.CitrusJavaCodegen.CODEGEN_NAME; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.servers.Server; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.citrusframework.openapi.generator.CitrusJavaCodegen.CustomCodegenOperation; +import org.citrusframework.openapi.generator.CitrusJavaCodegen.CustomCodegenParameter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.openapitools.codegen.CodegenConfigLoader; +import org.openapitools.codegen.CodegenOperation; +import org.openapitools.codegen.CodegenParameter; +import org.openapitools.codegen.CodegenProperty; + +/** + * This test validates the code generation process. + */ +class CitrusJavaCodegenTest { + + private CitrusJavaCodegen codegen; + + @BeforeEach + void setUp() { + codegen = new CitrusJavaCodegen(); + } + + @Test + void retrieveGeneratorBySpi() { + CitrusJavaCodegen codegen = (CitrusJavaCodegen) CodegenConfigLoader.forName("java-citrus"); + assertThat(codegen).isNotNull(); + } + + @Test + void arePredefinedValuesNotEmptyTest() { + CitrusJavaCodegen codegen = new CitrusJavaCodegen(); + + assertThat(codegen.getName()).isEqualTo(CODEGEN_NAME); + assertThat(codegen.getHelp()).isNotEmpty(); + assertThat(codegen.getHttpClient()).isNotEmpty(); + assertThat(codegen.getApiPrefix()).isNotEmpty(); + assertThat(codegen.getTargetXmlnsNamespace()).isNull(); + assertThat(codegen.getGeneratedSchemaFolder()).isNotEmpty(); + } + + @Test + void testGetName() { + assertThat(codegen.getName()).isEqualTo("java-citrus"); + } + + @Test + void testGetHelp() { + String helpMessage = codegen.getHelp(); + assertThat(helpMessage).isEqualTo("Generates citrus api requests."); + } + + @Test + void testAdditionalPropertiesConfiguration() { + assertThat(codegen.additionalProperties()) + .containsEntry("apiVersion", "1.0.0") + .containsEntry(CitrusJavaCodegen.API_TYPE, CitrusJavaCodegen.API_TYPE_REST) + .containsEntry("useJakartaEe", true); + } + + @Test + void testReservedWordsConfiguration() { + assertThat(codegen.reservedWords()) + .contains("name", "description", "httpclient") + .doesNotContain("nonReservedWord"); + } + + @Test + void testTypeMappings() { + assertThat(codegen.typeMapping()) + .containsEntry("binary", "org.citrusframework.spi.Resource") + .containsEntry("file", "org.citrusframework.spi.Resource"); + } + + @Test + void testProcessOptsWithApiType() { + codegen.additionalProperties().put(CitrusJavaCodegen.API_TYPE, "XXX"); + + assertThatThrownBy(() -> codegen.processOpts()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown API_TYPE: 'XXX'"); + } + + @Test + void testProcessOptsValidApiType() { + codegen.additionalProperties().put(CitrusJavaCodegen.API_TYPE, CitrusJavaCodegen.API_TYPE_REST); + codegen.setOutputDir("target"); + + assertDoesNotThrow(() -> codegen.processOpts()); + + assertThat(codegen.additionalProperties()) + .containsEntry(CitrusJavaCodegen.API_TYPE, CitrusJavaCodegen.API_TYPE_REST); + } + + @Test + void testPreprocessOpenAPI() { + OpenAPI openAPI = new OpenAPI(); + Info info = new Info(); + Map extensions = new HashMap<>(); + extensions.put("x-api-owner", "citrus-framework"); + extensions.put("x-api-version", "2.0.0"); + info.setExtensions(extensions); + openAPI.setInfo(info); + + codegen.preprocessOpenAPI(openAPI); + + assertThat(codegen.additionalProperties()) + .containsEntry("x-api-owner", "citrus-framework") + .containsEntry("x-api-version", "2.0.0") + .containsEntry("infoExtensions", extensions); + } + + @Test + void testFromProperty() { + // Mock schema + Schema schema = Mockito.mock(Schema.class); + + // Call fromProperty and verify conversion + CodegenProperty codegenProperty = codegen.fromProperty("name", schema, true); + assertThat(codegenProperty) + .isInstanceOf(CodegenProperty.class) + .hasFieldOrPropertyWithValue("name", "_name") + .hasFieldOrPropertyWithValue("required", true); + } + + @Test + void testFromFormProperty() { + Schema schema = Mockito.mock(Schema.class); + + @SuppressWarnings("unchecked") + Set imports = Mockito.mock(Set.class); + + CodegenParameter codegenParameter = codegen.fromFormProperty("formParam", schema, imports); + assertThat(codegenParameter) + .isInstanceOf(CustomCodegenParameter.class) + .hasFieldOrPropertyWithValue("paramName", "formParam"); + } + + @Test + void testFromParameter() { + Parameter parameter = Mockito.mock(Parameter.class); + + @SuppressWarnings("unchecked") + Set imports = Mockito.mock(Set.class); + + CodegenParameter codegenParameter = codegen.fromParameter(parameter, imports); + assertThat(codegenParameter) + .isInstanceOf(CustomCodegenParameter.class); + } + + @Test + void testFromOperation() { + Operation operation = Mockito.mock(Operation.class); + List servers = Collections.emptyList(); + + CodegenOperation codegenOperation = codegen.fromOperation("/path/segment1/{param1}/segment3/{param2}", "GET", operation, servers); + assertThat(codegenOperation) + .isInstanceOf(CustomCodegenOperation.class) + .hasFieldOrPropertyWithValue("httpMethod", "GET") + .hasFieldOrPropertyWithValue("path", "/path/segment1/{param1}/segment3/{param2}") + .hasFieldOrPropertyWithValue("operationId", "pathSegment1Param1Segment3Param2GET") + .hasFieldOrPropertyWithValue("operationIdOriginal", null); + } + + @Test + void shouldReturnCorrectInvokerFolder() { + String expectedPath = "src/main/java/org/openapitools"; + assertThat(codegen.invokerFolder().replace('\\', '/')).isEqualTo(expectedPath); + } + + @Test + void shouldReturnCorrectSpringFolder() { + String expectedPath = "src/main/java/org/openapitools/spring"; + assertThat(codegen.springFolder().replace('\\', '/')).isEqualTo(expectedPath); + } + + @Test + void shouldReturnCorrectSchemaFolder() { + String expectedPath = "src/main/resources/schema/xsd"; + assertThat(codegen.schemaFolder().replace('\\', '/')).isEqualTo(expectedPath); + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/ExpectedCodeGenIT.java b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/ExpectedCodeGenIT.java new file mode 100644 index 0000000000..815819decd --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/ExpectedCodeGenIT.java @@ -0,0 +1,148 @@ +package org.citrusframework.openapi.generator; + +import org.apache.commons.lang3.stream.Streams; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static java.nio.file.Files.readString; +import static java.nio.file.Files.walk; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This test case is designed to validate the consistency of the code generation process and detect + * any discrepancies between the generated API files and the reference files stored in + * '/ExpectedCodeGenIT/expectedgen/'. It compares the results of API generation against the + * reference files, and a failure indicates potential changes in mustache templates or code + * generation logic. + *

      + * If this test fails, it is essential to review the code generation process and underlying + * templates carefully. If the changes are intentional and verified, update the reference files by + * copying the generated API sources to the '/ExpectedCodeGenIT/expectedgen/' directory. To ensure + * accurate copying, without unwanted code formatting, use a simple File Explorer instead of relying + * on IDE-based operations. + */ +class ExpectedCodeGenIT { + + public static final String BASE_PACKAGE = "org/citrusframework/openapi/generator"; + + static Stream getResourcesForRest() throws IOException { + return geClassResourcesIgnoringInnerClasses(BASE_PACKAGE + "/rest"); + } + + static Stream getResourcesForSoap() throws IOException { + return geClassResourcesIgnoringInnerClasses(BASE_PACKAGE + "/soap/bookservice"); + } + + private static Stream geClassResourcesIgnoringInnerClasses(String path) throws IOException { + return Streams.of(new PathMatchingResourcePatternResolver().getResources(path + "/**/*.class")) + .filter(resource -> { + try { + return !resource.getURI().toString().contains("$"); + } catch (Exception e) { + throw new CitrusRuntimeException("Unable to retrieve URL from resource!"); + } + } + ).map(Arguments::arguments); + } + + private static long countFilesRecursively(Path dir) throws IOException { + try (Stream walk = walk(dir)) { + return walk.filter(Files::isRegularFile).count(); + } + } + + /** + * Get the absolute path to the test resources directory. + */ + static String getAbsoluteTestResourcePath(String pathToFileInTestResources) { + URL resourceUrl = CitrusJavaCodegenTest.class.getClassLoader().getResource(pathToFileInTestResources); + assert resourceUrl != null; + File inputSpecFile = new File(resourceUrl.getFile()); + return inputSpecFile.getAbsolutePath(); + } + + /** + * Get the absolute path to the project's target directory. + */ + static String getAbsoluteTargetDirectoryPath(String pathToFileInTargetDirectory) { + String projectBaseDir = System.getProperty("user.dir"); // Base directory of the project + File outputDirFile = new File(projectBaseDir, "target/" + pathToFileInTargetDirectory); + return outputDirFile.getAbsolutePath(); + } + + @Test + void noAdditionalFiles() throws IOException { + long expectedFileCount = countFilesRecursively( + Path.of(getAbsoluteTestResourcePath( + BASE_PACKAGE + "/ExpectedCodeGenIT/expectedgen/rest"))); + long actualFileCount = countFilesRecursively( + Path.of(getAbsoluteTargetDirectoryPath( + "generated-test-sources/" + BASE_PACKAGE + "/rest"))); + + assertEquals(expectedFileCount, actualFileCount, + "Directories do not have the same number of files."); + } + + @ParameterizedTest + @MethodSource("getResourcesForRest") + void testGeneratedFiles(Resource resource) throws IOException { + File classFile = resource.getFile(); + String absolutePath = classFile.getAbsolutePath(); + String javaFilePath = absolutePath + .replace("test-classes", "generated-test-sources") + .replace(".class", ".java"); + + assertFileContent(new File(javaFilePath), "rest"); + } + + @ParameterizedTest + @MethodSource("getResourcesForSoap") + void testGeneratedSoapFiles(Resource resource) throws IOException { + File classFile = resource.getFile(); + String absolutePath = classFile.getAbsolutePath(); + + String javaFilePath = absolutePath + .replace("test-classes", "generated-test-sources") + .replace(".class", ".java"); + + assertFileContent(new File(javaFilePath), "soap"); + } + + /* + * NOTE: when changes have been performed to mustache templates, the expected files need to be updated. + * Be aware that file content may change according to IDE formatting rules if the files are copied via IDE. + * Files should therefore be copied using a file explorer which ensures that content of files does not change. + */ + private void assertFileContent(File file, String apiDir) throws IOException { + assertThat(file).exists(); + + String expectedFilePath = BASE_PACKAGE + "/ExpectedCodeGenIT/expectedgen/" + file.getAbsolutePath().substring(file.getAbsolutePath().indexOf(apiDir)); + ClassPathResource classPathResource = new ClassPathResource(expectedFilePath); + + String actualContent = readString(file.toPath()); + String expectedContent = readString(classPathResource.getFile().toPath()); + + // Replace "Generated" with a placeholder + String generatedAnnotationPattern = "@jakarta\\.annotation\\.Generated\\(.*?\\)"; + String placeholder = "@jakarta.annotation.Generated(value = \"org.citrusframework.openapi.generator.JavaCitrusCodegen\", date = \"TIMESTAMP\", comments = \"Generator version: VERSION\")"; + + actualContent = actualContent.replaceAll(generatedAnnotationPattern, placeholder); + expectedContent = expectedContent.replaceAll(generatedAnnotationPattern, placeholder); + + assertThat(actualContent).isEqualTo(expectedContent); + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/GeneratedRestApiIT.java b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/GeneratedRestApiIT.java new file mode 100644 index 0000000000..00e6a91c93 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/GeneratedRestApiIT.java @@ -0,0 +1,3485 @@ +package org.citrusframework.openapi.generator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.citrusframework.http.actions.HttpActionBuilder.http; +import static org.citrusframework.openapi.generator.util.MultipartConverter.multipartMessageToMap; +import static org.citrusframework.util.FileUtils.copyToByteArray; +import static org.citrusframework.util.FileUtils.readToString; +import static org.citrusframework.util.SocketUtils.findAvailableTcpPort; +import static org.citrusframework.validation.json.JsonPathMessageValidationContext.Builder.jsonPath; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE; +import static org.springframework.http.MediaType.APPLICATION_PDF_VALUE; + +import jakarta.servlet.http.Cookie; +import java.io.IOException; +import java.math.BigDecimal; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.assertj.core.api.Assertions; +import org.citrusframework.TestActor; +import org.citrusframework.TestCaseRunner; +import org.citrusframework.actions.ReceiveMessageAction.ReceiveMessageActionBuilder; +import org.citrusframework.actions.SendMessageAction.SendMessageActionBuilder; +import org.citrusframework.annotations.CitrusResource; +import org.citrusframework.annotations.CitrusTest; +import org.citrusframework.annotations.CitrusTestSource; +import org.citrusframework.common.TestLoader; +import org.citrusframework.config.CitrusSpringConfig; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.MessageTimeoutException; +import org.citrusframework.exceptions.TestCaseFailedException; +import org.citrusframework.exceptions.ValidationException; +import org.citrusframework.http.actions.HttpClientRequestActionBuilder; +import org.citrusframework.http.actions.HttpClientResponseActionBuilder; +import org.citrusframework.http.actions.HttpServerRequestActionBuilder.HttpMessageBuilderSupport; +import org.citrusframework.http.client.HttpClient; +import org.citrusframework.http.client.HttpClientBuilder; +import org.citrusframework.http.message.HttpMessage; +import org.citrusframework.http.server.HttpServer; +import org.citrusframework.http.server.HttpServerBuilder; +import org.citrusframework.junit.jupiter.spring.CitrusSpringExtension; +import org.citrusframework.message.Message; +import org.citrusframework.openapi.generator.GeneratedRestApiIT.Config; +import org.citrusframework.openapi.generator.rest.extpetstore.model.PetIdentifier; +import org.citrusframework.openapi.generator.rest.extpetstore.request.ExtPetApi; +import org.citrusframework.openapi.generator.rest.extpetstore.spring.ExtPetStoreBeanConfiguration; +import org.citrusframework.openapi.generator.rest.petstore.request.PetApi; +import org.citrusframework.openapi.generator.rest.petstore.request.PetApi.GetPetByIdReceiveActionBuilder; +import org.citrusframework.openapi.generator.rest.petstore.spring.PetStoreBeanConfiguration; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +import org.citrusframework.openapi.testapi.GeneratedApiOperationInfo; +import org.citrusframework.spi.Resources; +import org.citrusframework.validation.json.JsonPathVariableExtractor; +import org.citrusframework.validation.script.ScriptValidationContext; +import org.citrusframework.variable.GlobalVariables; +import org.citrusframework.variable.MessageHeaderVariableExtractor; +import org.citrusframework.ws.endpoint.builder.WebServiceEndpoints; +import org.citrusframework.ws.server.WebServiceServer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * This integration test class for the generated TestAPI aims to comprehensively test all aspects of + * accessing the API using both Java and XML. In addition to serving as a test suite, it also acts + * as a reference example. + * + *

      Therefore, each test is designed to be self-contained and straightforward, allowing + * anyone reviewing the code to easily grasp the purpose and context of the test without needing to + * rely on shared setup or utility methods. + */ + +@ExtendWith(CitrusSpringExtension.class) +@SpringBootTest( + classes = { + PetStoreBeanConfiguration.class, + ExtPetStoreBeanConfiguration.class, + CitrusSpringConfig.class, + Config.class}, + properties = { + "extpetstore.basic.username=extUser", + "extpetstore.basic.password=extPassword", + "extpetstore.bearer.token=defaultBearerToken", + "extpetstore.api-key-query=defaultTopSecretQueryApiKey", + "extpetstore.api-key-header=defaultTopSecretHeaderApiKey", + "extpetstore.api-key-cookie=defaultTopSecretCookieApiKey", + "extpetstore.base64-encode-api-key=true" + }) +class GeneratedRestApiIT { + + public static final List PET_ID_LIST = List.of(1, 2); + public static final List PET_ID_AS_STRING_LIST = List.of("1", "2"); + public static final List PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST = List.of("${one}", + "${two}"); + + public static final PetIdentifier PET_IDENTIFIER = new PetIdentifier()._name("Louis") + .alias("Alexander"); + public static final String PET_IDENTIFIER_AS_STRING = """ + {"alias":"Alexander","name":"Louis"}"""; + + @Autowired + private HttpServer httpServer; + + @Autowired + private HttpServer otherHttpServer; + + @Autowired + private PetApi petApi; + + @Autowired + private ExtPetApi extPetApi; + + @Autowired + private TestActor petStoreActor; + + @Autowired + private HttpClient otherApplicationServiceClient; + + @TestConfiguration + public static class Config { + + private final int port = findAvailableTcpPort(8080); + + private final int otherPort = findAvailableTcpPort(8081); + + private final int wsPort = findAvailableTcpPort(8090); + + /** + * Main http client for accessing the main http server. + */ + @Bean(name = {"petstore.endpoint", "extpetstore.endpoint"}) + public HttpClient applicationServiceClient() { + return new HttpClientBuilder() + .requestUrl("http://localhost:%d".formatted(port)) + .handleCookies(true) + .build(); + } + + /** + * Http client accessing "other" server, see configuration of other server bean below. + */ + @Bean + public HttpClient otherApplicationServiceClient() { + return new HttpClientBuilder() + .requestUrl("http://localhost:%d".formatted(otherPort)) + .build(); + } + + /** + * A sample actor used to test actor configuration and functionality. + */ + @Bean + public TestActor petStoreActor() { + TestActor petStoreActor = new TestActor(); + petStoreActor.setName("PetStoreActor"); + petStoreActor.setDisabled(true); + return petStoreActor; + } + + /** + * Http server for mocking server side messaging and asserting data being send from test api + * requests. + */ + @Bean + public HttpServer httpServer() { + return new HttpServerBuilder() + .port(port) + .timeout(5000L) + .autoStart(true) + .defaultStatus(NO_CONTENT) + .handleCookies(true) + .handleHandleSemicolonPathContent(true) + .build(); + } + + @Bean + public WebServiceServer soapServer() { + return WebServiceEndpoints.soap().server() + .port(wsPort) + .timeout(5000) + .autoStart(true) + .build(); + } + + /** + * A second http server. Mainly for tests that assert, that the default endpoint + * configuration can be overridden by an explicit endpoint. + */ + @Bean + public HttpServer otherHttpServer() { + return new HttpServerBuilder() + .port(otherPort) + .timeout(5000L) + .autoStart(true) + .defaultStatus(NO_CONTENT) + .build(); + } + + /* + * Global variables, that make the ports available within the test context. Mainly + * used to access to port for explicit endpoint configuration tests. + */ + @Bean + public GlobalVariables globalVariables() { + return new GlobalVariables.Builder() + .variable("petstoreApplicationPort", port) + .variable("otherPetstoreApplicationPort", otherPort).build(); + } + + @Bean + public ApiActionBuilderCustomizer petApiCustomizer() { + return new ApiActionBuilderCustomizer() { + + @Override + public > void customizeRequestBuilder( + GeneratedApiOperationInfo generatedApiOperationInfo, T builder) { + ApiActionBuilderCustomizer.super.customizeRequestBuilder( + generatedApiOperationInfo, + builder); + } + + @Override + public > void customizeResponseBuilder( + GeneratedApiOperationInfo generatedApiOperationInfo, T builder) { + ApiActionBuilderCustomizer.super.customizeResponseBuilder( + generatedApiOperationInfo, + builder); + } + + }; + } + } + + @Nested + class OperationWithoutOperationId { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withoutOperationIdTest") + void xml() { + } + + @Test + void java_operation_without_operationId(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(extPetApi.sendPetWithoutOperationIdPetIdGet(1234) + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/without-operation-id/1234") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.when(extPetApi + .receivePetWithoutOperationIdPetIdGet(OK)); + + } + } + /** + * Demonstrates usage of parameter serialization according to + * ... + */ + @Nested + class ParameterSerialization { + + @Nested + class PathParameter { + + @Nested + class SimpleStyle { + + @Nested + class Array { + + @Test + void java_single_value(@CitrusResource TestCaseRunner runner) { + + runner.when(extPetApi.sendGetPetWithSimpleStyleArray(List.of(1)) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/simple/1") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleArray("200")); + } + + @Test + void java_array_value(@CitrusResource TestCaseRunner runner) { + + runner.when(extPetApi.sendGetPetWithSimpleStyleArray(PET_ID_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/simple/1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleArray("200")); + } + + @Test + void java_array_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithSimpleStyleArray$(PET_ID_AS_STRING_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/simple/1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleArray("200")); + } + + @Test + void java_array_value_non_type_safe_with_variables( + @CitrusResource TestCaseRunner runner) { + runner.variable("one", "1"); + runner.variable("two", "2"); + + runner.when( + extPetApi.sendGetPetWithSimpleStyleArray$( + PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/simple/1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleArray("200")); + } + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithSimpleStyleObject(PET_IDENTIFIER) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/simple/object/alias,Alexander,name,Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleObject("200")); + } + + @Test + void java_object_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when( + extPetApi.sendGetPetWithSimpleStyleObject$(PET_IDENTIFIER_AS_STRING) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/simple/object/alias,Alexander,name,Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleObject("200")); + } + } + + @Nested + class ExplodedArray { + + @Test + void java_single_value(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "citrus:randomNumber(10)"); + + runner.when(extPetApi.sendGetPetWithSimpleStyleArrayExploded(List.of(1)) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/simple/exploded/1") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleArrayExploded("200")); + } + + @Test + void java_array_value(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "citrus:randomNumber(10)"); + + runner.when(extPetApi.sendGetPetWithSimpleStyleArrayExploded(PET_ID_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/simple/exploded/1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleArrayExploded("200")); + } + + @Test + void java_array_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when( + extPetApi.sendGetPetWithSimpleStyleArrayExploded$(PET_ID_AS_STRING_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/simple/exploded/1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleArrayExploded("200")); + } + + @Test + void java_array_value_non_type_safe_with_variables( + @CitrusResource TestCaseRunner runner) { + runner.variable("one", "1"); + runner.variable("two", "2"); + + runner.when( + extPetApi.sendGetPetWithSimpleStyleArrayExploded$( + PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/simple/exploded/1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleArrayExploded("200")); + } + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithSimpleStyleObjectExploded( + PET_IDENTIFIER) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get( + "/api/v3/ext/pet/simple/exploded/object/alias=Alexander,name=Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleObjectExploded("200")); + } + + @Test + void java_object_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithSimpleStyleObjectExploded$( + PET_IDENTIFIER_AS_STRING) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get( + "/api/v3/ext/pet/simple/exploded/object/alias=Alexander,name=Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleObjectExploded("200")); + } + } + } + + @Nested + class LabelStyle { + + @Nested + class Array { + + /** + * Non exploded representation is currently not supported by validator. + * Therefore, we get a validation exception. The other tests use disabled + * request validation to overcome this issue. + */ + @Test + void throws_request_validation_exception( + @CitrusResource TestCaseRunner runner) { + HttpClientRequestActionBuilder builder = extPetApi.sendGetPetWithLabelStyleArray( + PET_ID_LIST) + .fork(false); + + assertThatThrownBy(() -> runner.when(builder)) + .isInstanceOf(TestCaseFailedException.class) + .hasCauseInstanceOf(ValidationException.class) + .hasMessageContaining( + "ERROR - Instance type (string) does not match any allowed primitive type (allowed: [\"integer\"]): []"); + } + + @Test + void java_single_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithLabelStyleArray(List.of(1)) + .schemaValidation(true) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/label/.1") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleArray("200")); + } + + @Test + void java_array_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithLabelStyleArray(PET_ID_LIST) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/label/.1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleArray("200")); + } + + @Test + void java_array_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithLabelStyleArray$(PET_ID_AS_STRING_LIST) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/label/.1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleArray("200")); + } + + @Test + void java_array_value_non_type_safe_with_variables( + @CitrusResource TestCaseRunner runner) { + runner.variable("one", "1"); + runner.variable("two", "2"); + + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when( + extPetApi.sendGetPetWithLabelStyleArray$( + PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/label/.1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleArray("200")); + } + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithLabelStyleObject(PET_IDENTIFIER) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/label/object/.alias,Alexander,name,Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleObject("200")); + } + + @Test + void java_object_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithLabelStyleObject$(""" + {"name":"Louis","alias":"Alexander"} + """) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/label/object/.alias,Alexander,name,Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleObject("200")); + } + } + + @Nested + class ExplodedArray { + + @Test + void java_single_value(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "citrus:randomNumber(10)"); + + runner.when(extPetApi.sendGetPetWithLabelStyleArrayExploded(List.of(1)) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/label/exploded/.1") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleArrayExploded("200")); + } + + @Test + void java_array_value(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "citrus:randomNumber(10)"); + + runner.when(extPetApi.sendGetPetWithLabelStyleArrayExploded(PET_ID_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/label/exploded/.1.2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleArrayExploded("200")); + } + + @Test + void java_array_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when( + extPetApi.sendGetPetWithLabelStyleArrayExploded$(PET_ID_AS_STRING_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/label/exploded/.1.2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleArrayExploded("200")); + } + + @Test + void java_array_value_non_type_safe_with_variables( + @CitrusResource TestCaseRunner runner) { + runner.variable("one", "1"); + runner.variable("two", "2"); + + runner.when( + extPetApi.sendGetPetWithLabelStyleArrayExploded$( + PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/label/exploded/.1.2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleArrayExploded("200")); + } + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithLabelStyleObjectExploded$(""" + {"name":"Louis","alias":"Alexander"} + """) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get( + "/api/v3/ext/pet/label/exploded/object/.alias=Alexander.name=Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleObjectExploded("200")); + } + + @Test + void java_object_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithLabelStyleObjectExploded( + PET_IDENTIFIER) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get( + "/api/v3/ext/pet/label/exploded/object/.alias=Alexander.name=Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithLabelStyleObjectExploded("200")); + } + } + } + + @Nested + class MatrixStyle { + + @Nested + class Array { + + @Test + void java_single_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithMatrixStyleArray(List.of(1)) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/matrix/;petId=1") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleArray("200")); + } + + @Test + void java_array_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithMatrixStyleArray(PET_ID_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/matrix/;petId=1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleArray("200")); + } + + @Test + void java_array_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithMatrixStyleArray$(PET_ID_AS_STRING_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/matrix/;petId=1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleArray("200")); + } + + @Test + void java_array_value_non_type_safe_with_variables( + @CitrusResource TestCaseRunner runner) { + runner.variable("one", "1"); + runner.variable("two", "2"); + + runner.when( + extPetApi.sendGetPetWithMatrixStyleArray$( + PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/matrix/;petId=1,2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleArray("200")); + } + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithMatrixStyleObject( + PET_IDENTIFIER) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/matrix/object/;petId=alias,Alexander,name,Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleObject("200")); + } + + @Test + void java_object_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithMatrixStyleObject$( + PET_IDENTIFIER_AS_STRING) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/matrix/object/;petId=alias,Alexander,name,Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleObject("200")); + } + } + + @Nested + class ExplodedArray { + + @Test + void java_single_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithMatrixStyleArrayExploded(List.of(1)) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/matrix/exploded/;petId=1") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleArrayExploded("200")); + } + + @Test + void java_array_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithMatrixStyleArrayExploded(PET_ID_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/matrix/exploded/;petId=1;petId=2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleArrayExploded("200")); + } + + @Test + void java_array_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when( + extPetApi.sendGetPetWithMatrixStyleArrayExploded$(PET_ID_AS_STRING_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/matrix/exploded/;petId=1;petId=2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleArrayExploded("200")); + } + + @Test + void java_array_value_non_type_safe_with_variables( + @CitrusResource TestCaseRunner runner) { + runner.variable("one", "1"); + runner.variable("two", "2"); + + runner.when( + extPetApi.sendGetPetWithMatrixStyleArrayExploded$( + PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/matrix/exploded/;petId=1;petId=2") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleArrayExploded("200")); + } + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithMatrixStyleObjectExploded( + PET_IDENTIFIER) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get( + "/api/v3/ext/pet/matrix/exploded/object/;alias=Alexander;name=Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleObject("200")); + } + + @Test + void java_object_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithMatrixStyleObjectExploded$( + PET_IDENTIFIER_AS_STRING) + .schemaValidation(false) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get( + "/api/v3/ext/pet/matrix/exploded/object/;alias=Alexander;name=Louis") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithMatrixStyleObject("200")); + } + } + } + } + + @Nested + class HeaderParameter { + + @Nested + class SimpleStyle { + + @Nested + class Array { + + @Test + void java_single_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithSimpleStyleHeader(List.of(1)) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple") + .message().header("petId", "1")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleHeader("200")); + } + + @Test + void java_array_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithSimpleStyleHeader(PET_ID_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple") + .message().header("petId", "1,2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleHeader("200")); + } + + @Test + void java_array_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when( + extPetApi.sendGetPetWithSimpleStyleHeader$(PET_ID_AS_STRING_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple") + .message().header("petId", "1,2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleHeader("200")); + } + + @Test + void java_array_value_non_type_safe_with_variables( + @CitrusResource TestCaseRunner runner) { + runner.variable("one", "1"); + runner.variable("two", "2"); + + runner.when(extPetApi.sendGetPetWithSimpleStyleHeader$( + PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple") + .message().header("petId", "1,2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleHeader("200")); + } + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi + .sendGetPetWithSimpleStyleObjectHeader( + PET_IDENTIFIER) + .schemaValidation(false) + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple/object") + .message().header("petId", "alias,Alexander,name,Louis")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleExplodedHeader("200")); + } + + @Test + void java_object_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi + .sendGetPetWithSimpleStyleObjectHeader$( + PET_IDENTIFIER_AS_STRING) + .schemaValidation(false) + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple/object") + .message().header("petId", "alias,Alexander,name,Louis")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleExplodedHeader("200")); + } + } + + @Nested + class ArrayExploded { + + @Test + void java_single_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithSimpleStyleExplodedHeader(List.of(1)) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple/exploded") + .message().header("petId", "1")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleExplodedHeader("200")); + } + + @Test + void java_array_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithSimpleStyleExplodedHeader(PET_ID_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple/exploded") + .message().header("petId", "1,2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleExplodedHeader("200")); + } + + @Test + void java_array_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithSimpleStyleExplodedHeader$( + PET_ID_AS_STRING_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple/exploded") + .message().header("petId", "1,2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleExplodedHeader("200")); + } + + @Test + void java_array_value_non_type_safe_with_variables( + @CitrusResource TestCaseRunner runner) { + runner.variable("one", "1"); + runner.variable("two", "2"); + + runner.when(extPetApi.sendGetPetWithSimpleStyleExplodedHeader$( + PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple/exploded") + .message().header("petId", "1,2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleExplodedHeader("200")); + } + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi + .sendGetPetWithSimpleStyleExplodedObjectHeader( + PET_IDENTIFIER) + .schemaValidation(false) + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple/exploded/object") + .message().header("petId", "alias=Alexander,name=Louis")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when( + extPetApi.receiveGetPetWithSimpleStyleExplodedObjectHeader("200")); + } + + @Test + void java_object_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi + .sendGetPetWithSimpleStyleExplodedObjectHeader$( + PET_IDENTIFIER_AS_STRING) + .schemaValidation(false) + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/header/simple/exploded/object") + .message().header("petId", "alias=Alexander,name=Louis")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when( + extPetApi.receiveGetPetWithSimpleStyleExplodedObjectHeader("200")); + } + } + } + } + + @Nested + class QueryParameter { + + @Nested + class FormStyle { + + @Nested + class Array { + + @Test + void java_single_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithFormStyleQuery(List.of(1)) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form") + .message().queryParam("petId", "1")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleQuery("200")); + } + + @Test + void java_arrya_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithFormStyleQuery(PET_ID_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form") + .message().queryParam("petId", "1,2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleQuery("200")); + } + + @Test + void java_arrya_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithFormStyleQuery$(PET_ID_AS_STRING_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form") + .message().queryParam("petId", "1,2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleQuery("200")); + } + + @Test + void java_arrya_value_non_type_safe_with_variables( + @CitrusResource TestCaseRunner runner) { + runner.variable("one", "1"); + runner.variable("two", "2"); + + runner.when(extPetApi.sendGetPetWithFormStyleQuery$( + PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form") + .message().queryParam("petId", "1,2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleQuery("200")); + } + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi + .sendGetPetWithFormStyleObjectQuery( + PET_IDENTIFIER) + .schemaValidation(false) + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form/object") + .message().queryParam("petId", "alias,Alexander,name,Louis")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleObjectQuery("200")); + } + + @Test + void java_object_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi + .sendGetPetWithFormStyleObjectQuery$( + PET_IDENTIFIER_AS_STRING) + .schemaValidation(false) + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form/object") + .message().queryParam("petId", "alias,Alexander,name,Louis")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleObjectQuery("200")); + } + } + + @Nested + class ArrayExploded { + + @Test + void java_single_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithFormStyleExplodedQuery(List.of(1)) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form/exploded") + .message().queryParam("petId", "1")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleQuery("200")); + } + + @Test + void java_array_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithFormStyleExplodedQuery(PET_ID_LIST) + .fork(true)); + + // Note that citrus currently fails to validate a query parameter array. Thus, we + // assert against the query_params header. + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form/exploded") + .message() + .queryParam("petId", "1") + .queryParam("petId", "2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleQuery("200")); + } + + @Test + void java_array_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when( + extPetApi.sendGetPetWithFormStyleExplodedQuery$(PET_ID_AS_STRING_LIST) + .fork(true)); + + // Note that citrus currently fails to validate a query parameter array. Thus, we + // assert against the query_params header. + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form/exploded") + .message() + .queryParam("petId", "1") + .queryParam("petId", "2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleQuery("200")); + } + + @Test + void java_array_value_non_type_safe_with_variables( + @CitrusResource TestCaseRunner runner) { + runner.variable("one", "1"); + runner.variable("two", "2"); + + runner.when(extPetApi.sendGetPetWithFormStyleExplodedQuery$( + PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST) + .fork(true)); + + // Note that citrus currently fails to validate a query parameter array. Thus, we + // assert against the query_params header. + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form/exploded") + .message() + .queryParam("petId", "1") + .queryParam("petId", "2")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleQuery("200")); + } + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi + .sendGetPetWithFormStyleExplodedObjectQuery( + PET_IDENTIFIER) + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form/exploded/object") + .message() + .queryParam("alias", "Alexander") + .queryParam("name", "Louis")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleObjectQuery("200")); + } + + @Test + void java_object_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi + .sendGetPetWithFormStyleExplodedObjectQuery$( + PET_IDENTIFIER_AS_STRING) + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/form/exploded/object") + .message() + .queryParam("alias", "Alexander") + .queryParam("name", "Louis")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleObjectQuery("200")); + } + } + } + + @Nested + class DeepObjectStyleExploded { + + @Nested + class ArrayExploded { + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithDeepObjectTypeQuery(PET_IDENTIFIER) + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/deep/object") + .message() + .queryParam("petId[alias]", "Alexander") + .queryParam("petId[name]", "Louis")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithDeepObjectTypeQuery("200")); + } + + @Test + void java_object_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when( + extPetApi.sendGetPetWithDeepObjectTypeQuery$(PET_IDENTIFIER_AS_STRING) + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/query/deep/object") + .message() + .queryParam("petId[alias]", "Alexander") + .queryParam("petId[name]", "Louis")); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithDeepObjectTypeQuery("200")); + } + } + } + } + + @Nested + class CookieParameter { + + @Nested + class FormStyle { + + @Nested + class Array { + + @Test + void java_single_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithFormStyleCookie(List.of(1)) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/cookie/form") + .message().cookie(new Cookie("petId", "1"))); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleCookie("200")); + } + + @Test + void java_array_value(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithFormStyleCookie(PET_ID_LIST) + .fork(true)); + + // Cookies may not contain "," which is used to separate array values. + // Therefore, cookies are URL encoded and reach the server accordingly. + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/cookie/form") + .message().cookie(new Cookie("petId", "1%2C2"))); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleCookie("200")); + } + + @Test + void java_array_value_non_type_safe(@CitrusResource TestCaseRunner runner) { + runner.when(extPetApi.sendGetPetWithFormStyleCookie$(PET_ID_AS_STRING_LIST) + .fork(true)); + + // Cookies may not contain "," which is used to separate array values. + // Therefore, cookies are URL encoded and reach the server accordingly. + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/cookie/form") + .message().cookie(new Cookie("petId", "1%2C2"))); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleCookie("200")); + } + + @Test + void java_array_value_non_type_safe_with_variables( + @CitrusResource TestCaseRunner runner) { + runner.variable("one", "1"); + runner.variable("two", "2"); + + runner.when(extPetApi.sendGetPetWithFormStyleCookie$( + PET_ID_WITH_VARIABLE_EXPRESSIONS_LIST) + .fork(true)); + + // Cookies may not contain "," which is used to separate array values. + // Therefore, cookies are URL encoded and reach the server accordingly. + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/cookie/form") + .message().cookie(new Cookie("petId", "1%2C2"))); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleCookie("200")); + } + + @Test + void java_object_value(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithFormObjectStyleCookie( + PET_IDENTIFIER) + .schemaValidation(false) + .fork(true)); + + // Cookies may not contain "," which is used to separate array values. + // Therefore, cookies are URL encoded and reach the server accordingly. + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/cookie/form/object") + .message() + .cookie(new Cookie("petId", "alias%2CAlexander%2Cname%2CLouis"))); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleCookie("200")); + } + + @Test + void java_object_value_none_type(@CitrusResource TestCaseRunner runner) { + // Note that we need to disable oas validation here, as validation is + // currently not supported with the chosen serialization approach. + runner.when(extPetApi.sendGetPetWithFormObjectStyleCookie$( + PET_IDENTIFIER_AS_STRING) + .schemaValidation(false) + .fork(true)); + + // Cookies may not contain "," which is used to separate array values. + // Therefore, cookies are URL encoded and reach the server accordingly. + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/cookie/form/object") + .message() + .cookie(new Cookie("petId", "alias%2CAlexander%2Cname%2CLouis"))); + + runner.then(http().server(httpServer) + .send() + .response(OK).message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithFormStyleCookie("200")); + } + } + } + } + + /** + * Demonstrates testing of array data in query parameters. + */ + @Nested + class Combined { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withArrayQueryDataTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + runner.variable("nick1", "Wind"); + runner.variable("nick2", "Storm"); + runner.variable("tag2", "tag2Value"); + + runner.when(extPetApi + .sendUpdatePetWithArrayQueryData$("${petId}", "Thunder", "sold", + List.of("tag1", "${tag2}"), + List.of("${nick1}", "${nick2}"), "header1") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .put("/api/v3/ext/pet/1234") + .message() + .validate(ScriptValidationContext.Builder.groovy().script(""" + assert receivedMessage.getHeader("sampleStringHeader") == header1 + org.assertj.core.api.Assertions.assertThat(((org.citrusframework.http.message.HttpMessage)receivedMessage).getQueryParams()).containsExactlyInAnyOrderEntriesOf( + java.util.Map.of( + "tags", java.util.List.of("tag1", "tag2Value"), + "name", java.util.List.of("Thunder"), + "nicknames", java.util.List.of("Wind", "Storm"), + "status", java.util.List.of("sold") + """)) + .validate((message, context) -> { + assertThat(message.getHeader("sampleStringHeader")).isEqualTo("header1"); + assertThat( + ((HttpMessage) message).getQueryParams()).containsExactlyInAnyOrderEntriesOf( + Map.of( + "tags", List.of("tag1", "tag2Value"), + "name", List.of("Thunder"), + "nicknames", List.of("Wind", "Storm"), + "status", List.of("sold") + ) + ); + })); + + runner.then(http().server(httpServer) + .send() + .response(OK)); + + runner.when(extPetApi + .receiveUpdatePetWithArrayQueryData(OK) + .message()); + } + } + } + + @Nested + class ParameterThatNeedUrlEncoding { + + @Test + void java_parameter_with_url_encoding(@CitrusResource TestCaseRunner runner) { + + runner.when(extPetApi.sendGetPetWithParametersRequiringEncoding(1) + .queryID(4) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/parameter-with-url-encoding-required/1") + .message()); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_JSON_VALUE) + .body("[]")); + + runner.when(extPetApi.receiveGetPetWithSimpleStyleArray("200")); + } + } + + /** + * Demonstrates the usage of form data. + */ + @Nested + class FormData { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withFormDataTest") + void xml() { + } + + @Test + void updatePetWithForm_java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "citrus:randomNumber(10)"); + + runner.when(petApi.sendUpdatePetWithForm$("${petId}") + ._name("Tom") + .status("sold") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .post("/api/v3/pet/${petId}") + .message() + .queryParam("name", "Tom") + .queryParam("status", "sold")); + + runner.then(http().server(httpServer) + .send() + .response(OK)); + + runner.when(petApi.receiveUpdatePetWithForm("200")); + } + } + + /** + * Demonstrates the usage of validation is disablement in API requests and responses. + */ + @Nested + class DisabledValidation { + + /** + * Test scenarios where response validation is disabled for the API requests. + */ + @Nested + class ResponseValidationDisabled { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withResponseValidationDisabledTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/invalidGetPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.when(petApi + .receiveGetPetById(OK) + .schemaValidation(false)); + } + } + } + + /** + * Contains test cases for scenarios where validation failures are expected. + */ + @Nested + class ValidationFailures { + + /** + * Tests where validation fails due to an incorrect reason phrase in the API response. + */ + @Nested + class FailOnReasonPhrase { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withFailOnReasonPhraseTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + HttpClientResponseActionBuilder.HttpMessageBuilderSupport builder = petApi + .receiveGetPetById(OK).message().reasonPhrase("Almost OK"); + assertThatThrownBy(() -> runner.when(builder)).isInstanceOf( + TestCaseFailedException.class) + .hasMessageContaining( + "Values not equal for header element 'citrus_http_reason_phrase', expected 'Almost OK' but was 'OK'") + .hasCauseInstanceOf(ValidationException.class); + } + } + + /** + * Tests where validation fails due to an incorrect HTTP status in the API response. + */ + @Nested + class FailOnStatus { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withFailOnStatusTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + GetPetByIdReceiveActionBuilder getPetByIdResponseActionBuilder = petApi + .receiveGetPetById("201"); + assertThatThrownBy(() -> runner.when(getPetByIdResponseActionBuilder)).isInstanceOf( + TestCaseFailedException.class) + .hasMessageContaining( + "Values not equal for header element 'citrus_http_reason_phrase', expected 'CREATED' but was 'OK'") + .hasCauseInstanceOf(ValidationException.class); + } + } + + /** + * Tests where validation fails due to an incorrect HTTP version in the API response. + */ + @Nested + class FailOnVersion { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withFailOnVersionTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + HttpClientResponseActionBuilder.HttpMessageBuilderSupport builder = petApi + .receiveGetPetById(OK).message().version("HTTP/1.0"); + assertThatThrownBy(() -> runner.when(builder)).isInstanceOf( + TestCaseFailedException.class) + .hasMessageContaining( + "Values not equal for header element 'citrus_http_version', expected 'HTTP/1.0' but was 'HTTP/1.1'") + .hasCauseInstanceOf(ValidationException.class); + } + } + + /** + * Tests where validation fails due to an invalid response body. + */ + @Nested + class FailOnInvalidResponse { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withFailOnInvalidResponseTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/invalidGetPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + GetPetByIdReceiveActionBuilder getPetByIdResponseActionBuilder = petApi.receiveGetPetById( + OK); + assertThatThrownBy(() -> runner.when(getPetByIdResponseActionBuilder)).isInstanceOf( + TestCaseFailedException.class) + .hasMessageContaining("Object has missing required properties ([\"name\"]): []") + .hasCauseInstanceOf(ValidationException.class); + } + } + + /** + * Tests where validation fails due to an incorrect body resource during validation. + */ + @Nested + class FailOnBodyResourceValidation { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withFailOnBodyResourceValidationTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + HttpClientResponseActionBuilder.HttpMessageBuilderSupport builder = petApi + .receiveGetPetById(OK).message().body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/thisAintNoPed.json")); + assertThatThrownBy(() -> runner.when(builder)).isInstanceOf( + TestCaseFailedException.class) + .hasMessageContaining( + "Number of entries is not equal in element: '$', expected '[description]' but was '[photoUrls, name, id, category, tags, status]'") + .hasCauseInstanceOf(ValidationException.class); + } + } + + /** + * Tests where validation fails due to incorrect data in the response body. + */ + @Nested + class FailOnBodyDataValidation { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withFailOnBodyDataValidationTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + HttpClientResponseActionBuilder.HttpMessageBuilderSupport builder = petApi + .receiveGetPetById(OK).message().body(""" + {"description": "no pet"}"""); + assertThatThrownBy(() -> runner.when(builder)).isInstanceOf( + TestCaseFailedException.class) + .hasMessageContaining( + "Number of entries is not equal in element: '$', expected '[description]' but was '[photoUrls, name, id, category, tags, status]'") + .hasCauseInstanceOf(ValidationException.class); + } + } + + /** + * Tests where validation fails due to invalid JSON path expressions in the response body. + */ + @Nested + class FailOnJsonPathInvalid { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withFailOnJsonPathInvalidTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + HttpClientResponseActionBuilder.HttpMessageBuilderSupport builder = petApi + .receiveGetPetById(OK).message() + .validate(jsonPath().expression("$.name", "unknown")); + assertThatThrownBy(() -> runner.when(builder)) + .isInstanceOf(TestCaseFailedException.class) + .hasMessageContaining( + "Values not equal for element '$.name', expected 'unknown' but was") + .hasCauseInstanceOf(ValidationException.class); + } + } + } + + /** + * Demonstrates the usage of a TestActor to control execution of test actions. + */ + @Nested + class Actor { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withActorTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .actor(petStoreActor) + .fork(true)); + + HttpMessageBuilderSupport builder = http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@"); + + assertThatThrownBy(() -> runner.$(builder)) + .isInstanceOf(TestCaseFailedException.class) + .hasCauseInstanceOf(MessageTimeoutException.class) + .hasMessageContaining( + "Action timeout after 5000 milliseconds. Failed to receive message on endpoint: 'httpServer.inbound'"); + } + } + + /** + * Demonstrates the usage of a specific URI for sending HTTP requests in tests. + */ + @Nested + class SpecificUri { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withSpecificUriTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .uri("http://localhost:${petstoreApplicationPort}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.then(petApi.receiveGetPetById(OK) + .message() + .validate(jsonPath().expression("$.name", "@matches('hasso|cutie|fluffy')@")) + ); + } + } + + /** + * Demonstrates the usage of a specific endpoint for sending HTTP requests in tests. + */ + @Nested + class SpecificEndpoint { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withSpecificEndpointTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}").endpoint(otherApplicationServiceClient) + .fork(true)); + + runner.then(http().server(otherHttpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(otherHttpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.then(petApi.receiveGetPetById(OK).endpoint(otherApplicationServiceClient) + .message() + .validate(jsonPath().expression("$.name", "@matches('hasso|cutie|fluffy')@")) + ); + } + } + + @Nested + class SpecialFormat { + + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + UUID uuid = UUID.randomUUID(); + runner.when(extPetApi.sendGetPetByUuid(uuid) + .endpoint(otherApplicationServiceClient) + .fork(true)); + + runner.then(http().server(otherHttpServer) + .receive() + .get("/api/v3/ext/pet/simple/object/uuid/" + uuid) + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(otherHttpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.then(petApi.receiveGetPetById(OK).endpoint(otherApplicationServiceClient) + .message() + .validate(jsonPath().expression("$.name", "@matches('hasso|cutie|fluffy')@")) + ); + } + } + + /** + * Demonstrates the usage of a nested element within an XML configuration for + * testing. + */ + @Nested + class NestedReceiveInXml { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withNestedReceiveInXmlWithDefaultClientTest") + void withNestedReceiveInXmlWithDefaultClientTest() { + } + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withNestedReceiveInXmlWithOtherClientTest") + void withNestedReceiveInXmlWithOtherClientTest() { + } + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withNestedReceiveInXmlWithUriTest") + void withNestedReceiveInXmlWithUriTest() { + } + + } + + /** + * Demonstrates the usage of different authentication mechanisms for testing. + */ + @Nested + class Security { + + @Nested + class BasicAuthentication { + + /** + * Demonstrates basic authentication using global credentials from properties. + */ + @Nested + class BasicAuthenticationFromProperties { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withBasicAuthenticationFromPropertiesTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(extPetApi + .sendGetPetByIdWithBasicAuthentication$("${petId}", "true") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/secure-basic/pet/${petId}") + .message() + .header("Authorization", "Basic ZXh0VXNlcjpleHRQYXNzd29yZA==")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.when(extPetApi + .receiveGetPetByIdWithBasicAuthentication(OK) + .message()); + } + } + + /** + * Demonstrates specific basic authentication with custom credentials. + */ + @Nested + class WithBasicAuthenticationOverridingProperties { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withBasicAuthenticationOverridingPropertiesTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(extPetApi + .sendGetPetByIdWithBasicAuthentication$("${petId}", "true") + .basicAuthUsername("admin") + .basicAuthPassword("topSecret") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/secure-basic/pet/${petId}") + .message().header("Authorization", "Basic YWRtaW46dG9wU2VjcmV0")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.when(extPetApi + .receiveGetPetByIdWithBasicAuthentication(OK) + .message()); + } + } + } + + @Nested + class BearerAuthentication { + + /** + * Demonstrates bearer authentication using a global token from properties. + */ + @Nested + class WithBasicBearerAuthenticationFromProperties { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withBasicBearerAuthenticationFromPropertiesTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(extPetApi + .sendGetPetByIdWithBearerAuthentication$("${petId}", "true") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/secure-bearer/pet/${petId}") + .message() + .header("Authorization", "Bearer ZGVmYXVsdEJlYXJlclRva2Vu")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.when(extPetApi + .receiveGetPetByIdWithBearerAuthentication(OK) + .message()); + } + } + + /** + * Demonstrates bearer authentication using specific token. + */ + @Nested + class WithBasicBearerAuthenticationOverridingProperties { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withBasicBearerAuthenticationOverridingPropertiesTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(extPetApi + .sendGetPetByIdWithBearerAuthentication$("${petId}", "true") + .basicAuthBearer("bearerToken") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/secure-bearer/pet/${petId}") + .message().header("Authorization", "Bearer YmVhcmVyVG9rZW4=")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.when(extPetApi + .receiveGetPetByIdWithBearerAuthentication(OK) + .message()); + } + } + } + + @Nested + class ApiKeyAuthentication { + + /** + * Demonstrates API key authentication using default values from properties. + */ + @Nested + class ApiKeyAuthenticationFromProperties { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withApiKeysFromPropertiesTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(extPetApi + .sendGetPetByIdWithApiKeyAuthentication$("${petId}", "false") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/secure-api-key/pet/${petId}") + .message() + .header("api_key_header", + "citrus:encodeBase64('defaultTopSecretHeaderApiKey')") + .cookie(new Cookie("api_key_cookie", + "citrus:encodeBase64('defaultTopSecretCookieApiKey')")) + .queryParam("api_key_query", + "citrus:encodeBase64('defaultTopSecretQueryApiKey')") + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/invalidGetPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.when(extPetApi + .receiveGetPetByIdWithApiKeyAuthentication(OK) + .schemaValidation(false)); + } + } + + /** + * Demonstrates API key authentication with custom values overriding properties. + */ + @Nested + class ApiKeyAuthenticationOverridingProperties { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withApiKeysOverridingPropertiesTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + runner.variable("apiKeyHeader", "TopSecretHeader"); + runner.variable("apiKeyCookie", "TopSecretCookie"); + runner.variable("apiKeyQuery", "TopSecretQuery"); + + runner.when(extPetApi + .sendGetPetByIdWithApiKeyAuthentication$("${petId}", "false") + .apiKeyHeader("${apiKeyHeader}") + .apiKeyCookie("${apiKeyCookie}") + .apiKeyQuery("${apiKeyQuery}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/secure-api-key/pet/${petId}") + .message() + .header("api_key_header", "citrus:encodeBase64('TopSecretHeader')") + .cookie( + new Cookie("api_key_cookie", "citrus:encodeBase64('TopSecretCookie')")) + .queryParam("api_key_query", "citrus:encodeBase64('TopSecretQuery')") + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/invalidGetPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.when(extPetApi + .receiveGetPetByIdWithApiKeyAuthentication(OK) + .schemaValidation(false)); + } + } + } + } + + /** + * Demonstrates testing of multipart requests. + */ + @Nested + class Multipart { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withMultipartTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) throws IOException { + byte[] templateData = copyToByteArray( + Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationTemplate.bin")); + String additionalData = readToString(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationAdditionalData.json")); + + runner.when(extPetApi.sendGenerateVaccinationReport$( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationTemplate.bin", + "1") + .additionalData(additionalData) + .optIntVal(100) + .optBoolVal(true) + .optStringVal("a") + .optNumberVal(BigDecimal.valueOf(1L)) + .optDateVal(LocalDate.of(2024, 12, 1)) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .post("/api/v3/ext/pet/vaccination/status-report") + .message() + .validate((message, context) -> + Assertions.assertThat(multipartMessageToMap((HttpMessage) message)) + .containsExactlyInAnyOrderEntriesOf(Map.of( + "additionalData", additionalData, + "reqIntVal", "1", + "template", templateData, + "optIntVal", "100", + "optBoolVal", "true", + "optDateVal", "[2024,12,1]", + "optNumberVal", "1", + "optStringVal", "a")) + ) + ); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .contentType(APPLICATION_PDF_VALUE) + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationReport.pdf"))); + + runner.then(extPetApi.receiveGenerateVaccinationReport(OK)); + } + } + + /** + * Demonstrates testing of requests using additional, non-API query parameters, headers, and + * cookies. + */ + @Nested + class NonApiQueryParameters { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withNonApiQueryParamTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "citrus:randomNumber(10)"); + + runner.when(petApi.sendUpdatePet() + .message().body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .queryParam("nonApiQueryParam", "nonApiQueryParamValue") + .header("nonApiHeader", "nonApiHeaderValue") + .cookie(new Cookie("nonApiCookie", "nonApiCookieValue")) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .put("/api/v3/pet") + .message() + .queryParam("nonApiQueryParam", "nonApiQueryParamValue") + .header("nonApiHeader", "nonApiHeaderValue") + .cookie(new Cookie("nonApiCookie", "nonApiCookieValue")) + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response_validation.json"))); + + runner.then(http().server(httpServer) + .send() + .response(OK)); + + runner.when(petApi.receiveUpdatePetWithForm("200")); + } + } + + /** + * Demonstrates testing of form URL-encoded data in requests. + */ + @Nested + class FormUrlEncoded { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withFormUrlEncodedTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + runner.variable("nick1", "Wind"); + runner.variable("nick2", "Storm"); + runner.variable("tag2", "tag2Value"); + + runner.when(extPetApi + .sendUpdatePetWithFormUrlEncoded$("${petId}", "Thunder", "sold", "5", + List.of("tag1", "${tag2}")) + .nicknames( + "${nick1}", + "${nick2}", + URLEncoder.encode("Wei{:/?#[]@!$&'()*+,;=%\"<>^`{|}~ }rd", + StandardCharsets.UTF_8) + ) + .owners("2") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .put("/api/v3/ext/pet/form/1234") + .message() + .contentType(APPLICATION_FORM_URLENCODED_VALUE) + .validate((Message message, TestContext context) -> + { + Object messagePayload = message.getPayload(); + assertThat(messagePayload).isInstanceOf(Map.class); + //noinspection unchecked + assertThat( + (Map>) messagePayload).containsExactlyInAnyOrderEntriesOf( + Map.of( + "tags", List.of("tag1", "tag2Value"), + "nicknames", List.of("Wind", "Storm", + "Wei%7B%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29*%2B%2C%3B%3D%25%22%3C%3E%5E%60%7B%7C%7D%7E+%7Drd"), + "status", List.of("sold"), + "age", List.of("5"), + "owners", List.of("2"), + "name", List.of("Thunder"))); + } + )); + + runner.then(http().server(httpServer) + .send() + .response(OK)); + + runner.when(extPetApi + .receiveUpdatePetWithFormUrlEncoded(OK) + .message()); + } + } + + /** + * Demonstrates testing of requests using the type-safe Java DSL. + */ + @Nested + class TypeSafeJavaDsl { + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + runner.variable("nick1", "Wind"); + runner.variable("nick2", "Storm"); + runner.variable("tag2", "tag2Value"); + + runner.when(extPetApi + .sendUpdatePetWithFormUrlEncoded(1234L, "Thunder", "sold", 5, + List.of("tag1", "${tag2}")) + .nicknames( + "${nick1}", + "${nick2}", + URLEncoder.encode("Wei{:/?#[]@!$&'()*+,;=%\"<>^`{|}~ }rd", + StandardCharsets.UTF_8) + ) + .owners("2") + .fork(true)); + runner.then(http().server(httpServer) + .receive() + .put("/api/v3/ext/pet/form/1234") + .message() + .contentType(APPLICATION_FORM_URLENCODED_VALUE) + .validate((Message message, TestContext context) -> { + Object messagePayload = message.getPayload(); + assertThat(messagePayload).isInstanceOf(Map.class); + //noinspection unchecked + assertThat( + (Map>) messagePayload).containsExactlyInAnyOrderEntriesOf( + Map.of( + "tags", List.of("tag1", "tag2Value"), + "nicknames", List.of("Wind", "Storm", + "Wei%7B%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29*%2B%2C%3B%3D%25%22%3C%3E%5E%60%7B%7C%7D%7E+%7Drd"), + "status", List.of("sold"), + "age", List.of("5"), + "owners", List.of("2"), + "name", List.of("Thunder"))); + } + )); + + runner.then(http().server(httpServer) + .send() + .response(OK)); + + runner.when(extPetApi + .receiveUpdatePetWithFormUrlEncoded(OK) + .message()); + } + } + + /** + * Demonstrates testing of plain text bodies in requests. + */ + @Nested + class BodyAsPlainText { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withBodyAsPlainTextTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "citrus:randomNumber(10)"); + + runner.when(petApi.sendUpdatePet() + .message().body(""" + { + "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')" + } + """) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .put("/api/v3/pet") + .message().body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response_validation.json"))); + + runner.then(http().server(httpServer) + .send() + .response(OK)); + + runner.when(petApi.receiveUpdatePetWithForm("200")); + } + } + + /** + * Demonstrates testing of request bodies read from resources. + */ + @Nested + class BodyFromResource { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withBodyFromResourceTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "citrus:randomNumber(10)"); + + runner.when(petApi.sendUpdatePet() + .message().body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .put("/api/v3/pet") + .message().body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response_validation.json"))); + + runner.then(http().server(httpServer) + .send() + .response(OK)); + + runner.when(petApi.receiveUpdatePetWithForm("200")); + } + } + + /** + * Demonstrates testing of control response bodies from plain text. + */ + @Nested + class ReceiveBodyFromPlainText { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withReceiveBodyFromPlainTextTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .cookie(new Cookie("NonApiCookie", "nonApiCookieValue")) + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.then(petApi.receiveGetPetById(OK) + .message() + .body(""" + { + "id": ${petId}, + "name": "@matches('hasso|cutie|fluffy')@", + "category": { + "id": ${petId}, + "name": "@matches('dog|cat|fish')@" + }, + "photoUrls": [ + "http://localhost:8080/photos/${petId}" + ], + "tags": [ + { + "id": ${petId}, + "name": "generated" + } + ], + "status": "@matches('available|pending|sold')@" + } + """) + ); + } + } + + /** + * Demonstrates testing of control response bodies from resource. + */ + @Nested + class ReceiveBodyFromResource { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withReceiveBodyFromResourceTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .cookie(new Cookie("NonApiCookie", "nonApiCookieValue")) + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.then(petApi.receiveGetPetById(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response_validation.json")) + ); + } + } + + /** + * Demonstrates testing of received non-API cookies. + */ + @Nested + class ReceiveNonApiCookie { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withReceiveNonApiCookieTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .cookie(new Cookie("NonApiCookie", "nonApiCookieValue")) + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.then(petApi.receiveGetPetById(OK) + .message() + .cookie(new Cookie("NonApiCookie", "nonApiCookieValue")) + ); + } + } + + /** + * Demonstrates validation of response by JSON path validation. + */ + @Nested + class JsonPathValidation { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withJsonPathValidationTest") + void xml() { + } + + @Test + @CitrusTest + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.then(petApi.receiveGetPetById(OK) + .message() + .validate(jsonPath().expression("$.name", "@matches('hasso|cutie|fluffy')@")) + ); + } + } + + /** + * Demonstrates extraction of response data using JSON path. + */ + @Nested + class JsonPathExtraction { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withJsonPathExtractionTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner, @CitrusResource TestContext context) { + runner.variable("petId", "1234"); + + runner.when(petApi + .sendGetPetById$("${petId}") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/pet/${petId}") + .message() + .accept("@contains('application/json')@")); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.when(petApi + .receiveGetPetById(OK) + .message() + .extract(MessageHeaderVariableExtractor.Builder.fromHeaders() + .expression("Content-Type", "varContentType")) + .extract(JsonPathVariableExtractor.Builder.fromJsonPath() + .expression("$.name", "varName"))) + ; + + assertThat(context.getVariable("varContentType")).isEqualTo(APPLICATION_JSON_VALUE); + assertThat(context.getVariable("varName")).matches("hasso|cutie|fluffy"); + } + } + + /** + * Demonstrates testing of API cookies in requests. + */ + @Nested + class ApiCookie { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withApiCookieTest") + void xml() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + runner.variable("petId", "1234"); + + runner.when(extPetApi + .sendGetPetWithCookie$("${petId}", "cookieValue") + .optTrxId("trxId") + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .get("/api/v3/ext/pet/${petId}") + .message() + .cookie(new Cookie("session_id", "cookieValue")) + .cookie(new Cookie("opt_trx_id", "trxId")) + ); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(Resources.create( + "classpath:org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json")) + .contentType(APPLICATION_JSON_VALUE)); + + runner.when(extPetApi + .receiveGetPetWithCookie(OK) + .message()); + } + } + + /** + * Demonstrates testing of file uploads and validations of the response. + */ + @Nested + class FileUpload { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withFileUploadTest") + void uploadFile_xml() { + } + + @Test + @CitrusTest + void uploadFile_java(@CitrusResource TestCaseRunner runner) { + String additionalMetadata = "myMeta"; + String file = "filedata"; + + runner.variable("petId", "1234"); + + runner.when(petApi.sendUploadFile$("${petId}") + .additionalMetadata(additionalMetadata) + .message() + .body(file) + .fork(true)); + + runner.then(http().server(httpServer) + .receive() + .post("/api/v3/pet/${petId}/uploadImage") + .message() + .contentType(APPLICATION_OCTET_STREAM_VALUE) + .queryParam("additionalMetadata", "myMeta") + .validate((message, context) -> { + Object payload = message.getPayload(); + assertThat(payload).isInstanceOf(byte[].class); + assertThat(new String((byte[]) payload, StandardCharsets.UTF_8)).isEqualTo( + "filedata"); + }) + ); + + runner.then(http().server(httpServer) + .send() + .response(OK) + .message() + .body(""" + {"code": 12, "type":"post-image-ok", "message":"image successfully uploaded"} + """) + .contentType(APPLICATION_JSON_VALUE)); + + runner.then(petApi + .receiveUploadFile(OK) + .message() + .validate(jsonPath().expression("$.code", "12")) + .validate(jsonPath().expression("$.message", "image successfully uploaded"))); + } + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/GeneratedSoapApiIT.java b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/GeneratedSoapApiIT.java new file mode 100644 index 0000000000..afe766a7e3 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/GeneratedSoapApiIT.java @@ -0,0 +1,119 @@ +package org.citrusframework.openapi.generator; + +import org.citrusframework.TestCaseRunner; +import org.citrusframework.annotations.CitrusResource; +import org.citrusframework.annotations.CitrusTestSource; +import org.citrusframework.common.TestLoader; +import org.citrusframework.config.CitrusSpringConfig; +import org.citrusframework.junit.jupiter.spring.CitrusSpringExtension; +import org.citrusframework.openapi.generator.GeneratedSoapApiIT.Config; +import org.citrusframework.openapi.generator.soap.bookservice.request.BookServiceSoapApi; +import org.citrusframework.openapi.generator.soap.bookservice.spring.BookServiceBeanConfiguration; +import org.citrusframework.ws.client.WebServiceClient; +import org.citrusframework.ws.client.WebServiceClientBuilder; +import org.citrusframework.ws.endpoint.builder.WebServiceEndpoints; +import org.citrusframework.ws.server.WebServiceServer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import static org.citrusframework.util.SocketUtils.findAvailableTcpPort; +import static org.citrusframework.ws.actions.SoapActionBuilder.soap; + +/** + * This integration test class for the generated TestAPI aims to comprehensively test all aspects of + * accessing the API using both Java and XML. In addition to serving as a test suite, it also acts + * as a reference example. + * + *

      Therefore, each test is designed to be self-contained and straightforward, allowing + * anyone reviewing the code to easily grasp the purpose and context of the test without needing to + * rely on shared setup or utility methods. + */ + +@ExtendWith(CitrusSpringExtension.class) +@SpringBootTest(classes = {BookServiceBeanConfiguration.class, CitrusSpringConfig.class, Config.class}) +class GeneratedSoapApiIT { + + @Autowired + private WebServiceServer soapServer; + + @Autowired + private BookServiceSoapApi bookServiceSoapApi; + + @TestConfiguration + public static class Config { + + private final int wsPort = findAvailableTcpPort(8090); + + @Bean(name = {"bookstore.endpoint"}) + public WebServiceClient soapClient() { + return new WebServiceClientBuilder() + .defaultUri("http://localhost:%d".formatted(wsPort)) + .build(); + } + + @Bean + public WebServiceServer soapServer() { + return WebServiceEndpoints.soap().server() + .port(wsPort) + .timeout(5000) + .autoStart(true) + .build(); + } + + } + + @Nested + class SoapApi { + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withSoapTest") + void xml() { + } + + @Test + @CitrusTestSource(type = TestLoader.SPRING, packageName = "org.citrusframework.openapi.generator.GeneratedApiTest", name = "withNestedReceiveDefaultClientSoapTest") + void withNestedReceiveInXmlWithDefaultClientTest() { + } + + @Test + void java(@CitrusResource TestCaseRunner runner) { + String request = """ + + + Lord of the Rings + J.R.R. Tolkien + + + """; + runner.when(bookServiceSoapApi.sendAddBook().fork(true).message().body(request)); + + runner.then(soap().server(soapServer) + .receive() + .message() + .body(request)); + + String response = """ + + + Lord of the Rings + J.R.R. Tolkien + + + """; + + runner.then(soap().server(soapServer) + .send() + .message() + .body(response)); + + runner.then(bookServiceSoapApi.receiveAddBook() + .message() + .body(response)); + } + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/GeneratedSpringBeanConfigurationIT.java b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/GeneratedSpringBeanConfigurationIT.java new file mode 100644 index 0000000000..af2b7e5543 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/GeneratedSpringBeanConfigurationIT.java @@ -0,0 +1,72 @@ +package org.citrusframework.openapi.generator; + +import org.citrusframework.annotations.CitrusResource; +import org.citrusframework.annotations.CitrusTest; +import org.citrusframework.config.CitrusSpringConfig; +import org.citrusframework.context.TestContext; +import org.citrusframework.http.client.HttpClient; +import org.citrusframework.http.client.HttpEndpointConfiguration; +import org.citrusframework.junit.jupiter.spring.CitrusSpringSupport; +import org.citrusframework.openapi.generator.GeneratedSpringBeanConfigurationIT.ClientConfiguration; +import org.citrusframework.openapi.generator.rest.petstore.request.PetApi; +import org.citrusframework.openapi.generator.rest.petstore.request.StoreApi; +import org.citrusframework.openapi.generator.rest.petstore.request.UserApi; +import org.citrusframework.openapi.generator.rest.petstore.spring.PetStoreBeanConfiguration; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +@CitrusSpringSupport +@ContextConfiguration(classes = {CitrusSpringConfig.class, ClientConfiguration.class, PetStoreBeanConfiguration.class}) +class GeneratedSpringBeanConfigurationIT { + + @Test + @CitrusTest + void petStoreOpenApiRepositoryIsAvailable(@CitrusResource TestContext testContext) { + var petStoreOpenApiRepository = testContext.getReferenceResolver() + .resolve("petStoreOpenApiRepository"); + assertThat(petStoreOpenApiRepository) + .isNotNull(); + } + + @Test + @CitrusTest + void petApiRepositoryIsAvailable(@CitrusResource TestContext testContext) { + var petApi = testContext.getReferenceResolver() + .resolve(PetApi.class); + assertThat(petApi) + .isNotNull(); + } + + @Test + @CitrusTest + void storeApiRepositoryIsAvailable(@CitrusResource TestContext testContext) { + var storeApi = testContext.getReferenceResolver() + .resolve(StoreApi.class); + assertThat(storeApi) + .isNotNull(); + } + + @Test + @CitrusTest + void userApiRepositoryIsAvailable(@CitrusResource TestContext testContext) { + var userApi = testContext.getReferenceResolver() + .resolve(UserApi.class); + assertThat(userApi) + .isNotNull(); + } + + @TestConfiguration + public static class ClientConfiguration { + + @Bean(name = {"petstore.endpoint"}) + public HttpClient applicationServiceClient() { + var config = new HttpEndpointConfiguration(); + config.setRequestUrl("http://localhost:9000"); + return new HttpClient(config); + } + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/WsdlToOpenApiTransformerTest.java b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/WsdlToOpenApiTransformerTest.java new file mode 100644 index 0000000000..e8f6544109 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/WsdlToOpenApiTransformerTest.java @@ -0,0 +1,30 @@ +package org.citrusframework.openapi.generator; + +import org.citrusframework.openapi.generator.exception.WsdlToOpenApiTransformationException; +import org.citrusframework.spi.Resource; +import org.citrusframework.spi.Resources.ClasspathResource; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.citrusframework.util.FileUtils.readToString; + +class WsdlToOpenApiTransformerTest { + + @Test + void testTransform() throws WsdlToOpenApiTransformationException, IOException { + ClassPathResource wsdlResource = new ClassPathResource( + "/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookService.wsdl"); + + WsdlToOpenApiTransformer simpleWsdlToOpenApiTransformer = new WsdlToOpenApiTransformer(wsdlResource.getURI()); + String generatedYaml = simpleWsdlToOpenApiTransformer.transformToOpenApi(); + + Resource expectedYamlResource = new ClasspathResource( + "/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookService-generated.yaml"); + + String expectedYaml = readToString(expectedYamlResource); + assertThat(generatedYaml).isEqualToIgnoringWhitespace(expectedYaml); + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/util/MultipartConverter.java b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/util/MultipartConverter.java new file mode 100644 index 0000000000..fda129439b --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/java/org/citrusframework/openapi/generator/util/MultipartConverter.java @@ -0,0 +1,72 @@ +package org.citrusframework.openapi.generator.util; + +import org.apache.commons.fileupload.MultipartStream; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.http.message.HttpMessage; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE; + +/** + * Provides utility method to convert a multipart http message to a map for simplified assertion. + */ +public class MultipartConverter { + + private static final Pattern NAME_PATTERN = Pattern.compile("name=\"([^\"]+)\""); + + private static final Pattern CONTENT_PATTERN = Pattern.compile("Content-Type:\\s*([^\\s;]+)"); + + public static Map multipartMessageToMap(HttpMessage message) { + String contentType = message.getContentType(); + String boundary = contentType.substring(contentType.indexOf("=") + 1); + + Map partMap = new HashMap<>(); + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(message.getPayload(String.class).getBytes())) { + MultipartStream multipartStream = new MultipartStream(inputStream, boundary.getBytes(), 4096, null); + + boolean nextPart = multipartStream.skipPreamble(); + while (nextPart) { + String headers = multipartStream.readHeaders(); + String partName = getHeaderGroup(headers, NAME_PATTERN); + String partContentType = getHeaderGroup(headers, CONTENT_PATTERN); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + multipartStream.readBodyData(outputStream); + String rawBodyContent = outputStream.toString(); + + partMap.put(partName, convertContent(rawBodyContent, partContentType)); + nextPart = multipartStream.readBoundary(); + } + + return partMap; + } catch (IOException e) { + throw new CitrusRuntimeException("Unable to parse multipart data"); + } + } + + private static String getHeaderGroup(String headers, Pattern groupPattern) { + Matcher matcher = groupPattern.matcher(headers); + if (matcher.find()) { + return matcher.group(1); + } else { + throw new CitrusRuntimeException( + "unable to determine header group name: " + groupPattern); + } + } + + private static Object convertContent(String rawContent, String contentType) { + if (contentType != null&& contentType.contains(APPLICATION_OCTET_STREAM_VALUE)) { + return rawContent.getBytes(StandardCharsets.ISO_8859_1); + } + + return rawContent; + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/META-INF/spring.handlers b/test-api-generator/citrus-test-api-generator-core/src/test/resources/META-INF/spring.handlers new file mode 100644 index 0000000000..f1955af26b --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/META-INF/spring.handlers @@ -0,0 +1,3 @@ +http\://www.citrusframework.org/citrus-test-schema/bookservice-api=org.citrusframework.openapi.generator.soap.bookservice.spring.BookServiceNamespaceHandler +http\://www.citrusframework.org/citrus-test-schema/petstore-api=org.citrusframework.openapi.generator.rest.petstore.spring.PetStoreNamespaceHandler +http\://www.citrusframework.org/citrus-test-schema/extpetstore-api=org.citrusframework.openapi.generator.rest.extpetstore.spring.ExtPetStoreNamespaceHandler \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/META-INF/spring.schemas b/test-api-generator/citrus-test-api-generator-core/src/test/resources/META-INF/spring.schemas new file mode 100644 index 0000000000..5b3c2539b9 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/META-INF/spring.schemas @@ -0,0 +1,4 @@ +http\://www.citrusframework.org/citrus-test-schema/petstore-api/petstore-api.xsd=schema/xsd/petstore-api.xsd +http\://www.citrusframework.org/citrus-test-schema/extpetstore-api/extpetstore-api.xsd=schema/xsd/extpetstore-api.xsd +http\://www.citrusframework.org/citrus-test-schema/bookservice-api/bookservice-api.xsd=schema/xsd/bookservice-api.xsd +http\://www.citrusframework.org/bookstore/datatypes/BookDatatypes.xsd=schema/xsd/BookDatatypes.xsd diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/apis/BookService-generated.yaml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/apis/BookService-generated.yaml new file mode 100644 index 0000000000..f70656312d --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/apis/BookService-generated.yaml @@ -0,0 +1,43 @@ +--- +info: + contact: + name: "org.citrusframework.openapi.generator.SimpleWsdlToOpenApiTransformer" + description: "This api has been generated from the following wsdl 'BookService.wsdl'.\ + \ It's purpose is solely to serve as input for SOAP API generation. Note that\ + \ only operations are extracted from the WSDL. No schema information whatsoever\ + \ is generated!" + title: "Generated api from wsdl" + version: "1.0.0" +openapi: "3.0.1" +paths: + /GetBook: + post: + description: "This operation retrieves details for a specific book identified\ + \ by its ID." + operationId: "GetBook" + responses: + default: + description: "Generic Response" + summary: "http://www.citrusframework.com/BookService/GetBook" + tags: + - "BookServiceSOAP" + /AddBook: + post: + description: "" + operationId: "AddBook" + responses: + default: + description: "Generic Response" + summary: "http://www.citrusframework.com/BookService/AddBook" + tags: + - "BookServiceSOAP" + /GetAllBooks: + post: + description: "" + operationId: "GetAllBooks" + responses: + default: + description: "Generic Response" + summary: "http://www.citrusframework.com/BookService/GetAllBooks" + tags: + - "BookServiceSOAP" \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/apis/petstore-extended-v3.yaml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/apis/petstore-extended-v3.yaml new file mode 100644 index 0000000000..255b98d238 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/apis/petstore-extended-v3.yaml @@ -0,0 +1,1317 @@ +openapi: 3.0.2 +info: + title: Extended Petstore API + description: | + This API extends the standard Petstore API. Although the operations may not be meaningful in + a real-world context, they are designed to showcase various advanced OpenAPI features that + are not present in the standard Petstore API. + version: 1.0.0 +servers: + - url: http://localhost:9000/api/v3/ext + +paths: + /pet/without-operation-id/{petId}: + get: + tags: + - extPet + summary: Get a pet by id. This operation has no operationId. + description: "Returns pet by ID using. This operation has no operationId." + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/simple/object/uuid/{petUuid}: + get: + tags: + - extPet + summary: Get a pet by UUID + description: Returns pet by UUID. + operationId: getPetByUuid + parameters: + - name: petUuid + in: path + description: UUID of pet to return + required: true + style: simple + explode: false + schema: + type: string + format: uuid + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/simple/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithSimpleStyleArray + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: simple + explode: false + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/simple/object/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithSimpleStyleObject + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: simple + explode: false + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/simple/exploded/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithSimpleStyleArrayExploded + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: simple + explode: false + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/simple/exploded/object/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithSimpleStyleObjectExploded + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: simple + explode: true + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/label/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithLabelStyleArray + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: label + explode: false + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/label/object/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithLabelStyleObject + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: label + explode: false + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/label/exploded/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithLabelStyleArrayExploded + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: label + explode: true + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/label/exploded/object/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithLabelStyleObjectExploded + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: label + explode: true + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/matrix/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithMatrixStyleArray + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: matrix + explode: false + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/matrix/object/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithMatrixStyleObject + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: matrix + explode: false + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/matrix/exploded/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithMatrixStyleArrayExploded + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: matrix + explode: true + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/matrix/exploded/object/{petId}: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in path parameter + operationId: getPetWithMatrixStyleObjectExploded + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: path + description: IDs of pet to return + required: true + style: matrix + explode: true + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/parameter-with-url-encoding-required/{Pet ID}: + get: + tags: + - extPet + summary: Get a pet by id with parameters that potentially require encoding + operationId: getPetWithParametersRequiringEncoding + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: Pet ID + in: path + description: IDs of pet to return + required: true + schema: + type: integer + - name: Query ID + in: query + description: A query parameter containing a space + schema: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/header/simple: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in header parameter + operationId: getPetWithSimpleStyleHeader + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: header + description: IDs of pet to return + required: true + style: simple + explode: false + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/header/simple/exploded: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in header parameter + operationId: getPetWithSimpleStyleExplodedHeader + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: header + description: IDs of pet to return + required: true + style: simple + explode: true + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/header/simple/object: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in header parameter + operationId: getPetWithSimpleStyleObjectHeader + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: header + description: IDs of pet to return + required: true + style: simple + explode: false + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/header/simple/exploded/object: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in header parameter + operationId: getPetWithSimpleStyleExplodedObjectHeader + description: "Returns multiple pets by ID using multiple ids in header parameter." + parameters: + - name: petId + in: header + description: IDs of pet to return + required: true + style: simple + explode: true + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/query/form: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in header parameter + operationId: getPetWithFormStyleQuery + description: "Returns multiple pets by ID using multiple ids in query parameter." + parameters: + - name: petId + in: query + description: IDs of pet to return + required: true + style: form + explode: false + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/query/form/object: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in header parameter + operationId: getPetWithFormStyleObjectQuery + description: "Returns multiple pets by ID using multiple ids in query parameter." + parameters: + - name: petId + in: query + description: IDs of pet to return + required: true + style: form + explode: false + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/query/form/exploded: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in header parameter + operationId: getPetWithFormStyleExplodedQuery + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: query + description: IDs of pet to return + required: true + style: form + explode: true + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/query/form/exploded/object: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in header parameter + operationId: getPetWithFormStyleExplodedObjectQuery + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: query + description: IDs of pet to return + required: true + style: form + explode: true + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/query/deep/object: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in header parameter + operationId: getPetWithDeepObjectTypeQuery + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: query + description: IDs of pet to return + required: true + style: deepObject + explode: true + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/cookie/form: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in cookie parameter + operationId: getPetWithFormStyleCookie + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: cookie + description: IDs of pet to return + required: true + style: form + explode: false + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/cookie/form/object: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in cookie parameter + operationId: getPetWithFormObjectStyleCookie + description: "Returns multiple pets by ID using multiple ids in path parameter." + parameters: + - name: petId + in: cookie + description: IDs of pet to return + required: true + style: form + explode: false + schema: + $ref: '#/components/schemas/PetIdentifier' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/cookie/form/exploded: + get: + tags: + - extPet + summary: Get a pet by id supporting multiple ids in cookie parameter + operationId: getPetWithFormExplodedStyleCookie + description: "Returns multiple pets by ID using multiple ids in cookie parameter." + parameters: + - name: petId + in: cookie + description: IDs of pet to return + required: true + style: form + explode: true + schema: + type: array + items: + type: integer + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/{petId}: + get: + tags: + - extPet + summary: Get a pet using a session cookie + operationId: getPetWithCookie + description: "Returns a single pet by ID, using a session ID stored in a cookie." + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + - name: session_id + in: cookie + required: true + schema: + type: string + description: The session ID cookie + - name: opt_trx_id + in: cookie + schema: + type: string + description: The optional transaction ID cookie + responses: + '200': + description: Successful operation + content: + application/xml: + schema: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + application/json: + schema: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + put: + tags: + - extPet + summary: Updates a pet in the store with query data. + description: "Update the name, status, tags and nicknames of an existing pet using query data." + operationId: updatePetWithArrayQueryData + parameters: + - name: petId + in: path + required: true + description: ID of the pet that needs to be updated + schema: + type: integer + format: int64 + - name: name + in: query + required: true + description: Updated name of the pet + schema: + type: string + - name: status + in: query + required: true + description: Updated status of the pet + schema: + type: string + - name: tags + in: query + required: true + description: Updated tags of the pet + schema: + type: array + items: + type: string + - name: nicknames + in: query + required: true + description: Updated nicknames of the pet + schema: + type: array + items: + type: string + - name: sampleStringHeader + in: header + required: true + description: A sample string header + schema: + type: string + - name: sampleIntHeader + in: header + description: A sample int header + schema: + type: integer + responses: + '200': + description: Pet successfully updated + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + /pet/form/{petId}: + put: + tags: + - extPet + summary: Updates a pet in the store with form data + description: "Update the name or status of an existing pet using url encoded form data." + operationId: updatePetWithFormUrlEncoded + parameters: + - name: petId + in: path + required: true + description: ID of the pet that needs to be updated + schema: + type: integer + format: int64 + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: [ "name", "status", "tags", "age" ] + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + age: + description: The age of the pet in years + type: integer + owners: + description: The number of pre owners + type: integer + tags: + type: array + items: + type: string + nicknames: + type: array + items: + type: string + responses: + '200': + description: Pet successfully updated + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + /secure-basic/pet/{petId}: + get: + tags: + - extPet + summary: Get a pet via basic authentication + description: "Returns a single pet by ID, requiring basic authentication." + operationId: getPetByIdWithBasicAuthentication + security: + - basicAuth: [ ] + + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + - name: allDetails + in: query + description: Requests all details. + required: true + schema: + type: boolean + - name: details + in: query + description: A list with detail specifiers. + schema: + type: array + items: + type: string + - name: requesterInformation + in: query + description: A list of specific requester information. + schema: + type: array + items: + type: string + responses: + '200': + description: Successful operation + content: + application/xml: + schema: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + application/json: + schema: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /secure-bearer/pet/{petId}: + get: + tags: + - extPet + summary: Get a pet via basic authentication + description: "Returns a single pet by ID, requiring basic authentication." + operationId: getPetByIdWithBearerAuthentication + security: + - bearerAuth: [ ] + + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + - name: allDetails + in: query + description: Requests all details. + required: true + schema: + type: boolean + - name: details + in: query + description: A list with detail specifiers. + schema: + type: array + items: + type: string + - name: requesterInformation + in: query + description: A list of specific requester information. + schema: + type: array + items: + type: string + responses: + '200': + description: Successful operation + content: + application/xml: + schema: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + application/json: + schema: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /secure-api-key/pet/{petId}: + get: + tags: + - extPet + summary: Get a pet via basic authentication + description: "Returns a single pet by ID, requiring basic authentication." + operationId: getPetByIdWithApiKeyAuthentication + security: + - api_key_header: [ ] + - api_key_query: [ ] + - api_key_cookie: [ ] + + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + - name: allDetails + in: query + description: Requests all details. + required: true + schema: + type: boolean + - name: details + in: query + description: A list with detail specifiers. + required: false + schema: + type: array + items: + type: string + - name: requesterInformation + in: query + description: A list of specific requester information. + schema: + type: array + items: + type: string + responses: + '200': + description: Successful operation + content: + application/xml: + schema: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + application/json: + schema: + $ref: './petstore-v3.yaml#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + /pet/vaccination/{bucket}/{filename}: + post: + tags: + - extPet + operationId: postVaccinationDocument + summary: Upload a pet vaccination document + description: "Uploads a vaccination document for a pet to a specified S3 bucket." + parameters: + - name: bucket + description: The name of an existing S3 bucket. + in: path + required: true + schema: + type: string + - name: filename + description: The name under which to store the vaccination document. + in: path + required: true + schema: + type: string + responses: + '201': + description: Document successfully uploaded + content: + application/json: + schema: + $ref: '#/components/schemas/VaccinationDocumentResult' + '400': + description: Bad Request - Invalid parameters supplied + '500': + description: Internal Server Error + /pet/vaccination/status-report: + post: + tags: + - extPet + operationId: generateVaccinationReport + summary: Generate a vaccination status report + description: "Generates a vaccination status report in PDF format based on the provided template and data." + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [ 'template', "reqIntVal" ] + properties: + template: + description: | + Vaccination report template file. + type: string + format: binary + reqIntVal: + description: | + A required integer value. + type: integer + optIntVal: + description: | + An optional integer value. + type: integer + optBoolVal: + description: | + An optional boolean value. + type: boolean + optNumberVal: + description: | + An optional number value. + type: number + optStringVal: + description: | + An optional string value. + type: string + optDateVal: + description: | + An optional date value. + type: string + format: date + additionalData: + $ref: '#/components/schemas/HistoricalData' + schema: + description: | + An optional JSON schema file to validate the created report against. + type: string + format: binary + responses: + '200': + description: A vaccination report in PDF + content: + application/pdf: + schema: + type: string + format: binary + '422': + description: Unprocessable Entity - Validation error in the provided data + '500': + description: Internal Server Error + /pet/vaccination/form: + post: + tags: + - extPet + operationId: postVaccinationFormData + summary: Submit vaccination form data + description: "Submits vaccination details for a pet." + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + vaccine: + type: string + description: The name of the vaccine administered. + isFirstVaccination: + type: boolean + description: Indicates if this is the first vaccination or a repetition. + doseNumber: + type: integer + description: The dose number of the vaccine (e.g., 1 for first dose, 2 for second dose). + vaccinationDate: + type: string + format: date + description: The date when the vaccination was administered. + responses: + '201': + description: Form data successfully submitted + content: + application/json: + schema: + $ref: '#/components/schemas/VaccinationDocumentResult' + '400': + description: Bad Request - Invalid form data + '500': + description: Internal Server Error + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + bearerAuth: + type: http + scheme: bearer # Specifies Bearer token authentication + bearerFormat: JWT # Optional, specifies the format of the bearer token (e.g., JWT) + api_key_header: + type: apiKey + description: Header api key description + name: api_key_header + in: header + api_key_cookie: + type: apiKey + description: Cookie api key description + name: api_key_cookie + in: cookie + api_key_query: + type: apiKey + description: Query api key description + name: api_key_query + in: query + + schemas: + HistoricalData: + description: | + Additional historical data for a vaccination report, not contained in internal storage. + type: object + properties: + lastVaccinationDate: + type: string + format: date + description: The date of the last vaccination. + vaccinationCount: + type: integer + description: The number of vaccinations the pet has received. + VaccinationDocumentResult: + type: object + properties: + documentId: + type: string + description: The unique ID of the uploaded vaccination document. + example: "abc123" + PetIdentifier: + type: object + properties: + name: + type: string + alias: + type: string diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/apis/petstore-v3.yaml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/apis/petstore-v3.yaml new file mode 100644 index 0000000000..8312d0e5a5 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/apis/petstore-v3.yaml @@ -0,0 +1,803 @@ +openapi: 3.0.2 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + termsOfService: http://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.19 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +servers: + - url: http://localhost:9000/api/v3 +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: http://swagger.io + - name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: http://swagger.io + - name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "405": + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store + description: Add a new pet to the store + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + "405": + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: false + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + "200": + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid status value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: "Multiple tags can be provided with comma separated strings. Use\ + \ tag1, tag2, tag3 for testing." + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: false + explode: true + schema: + type: array + items: + type: string + responses: + "200": + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid tag value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + security: + - api_key: [] + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: "" + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + "405": + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet + description: "" + operationId: deletePet + parameters: + - name: api_key + in: header + description: "" + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "400": + description: Invalid pet value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: uploads an image + description: "" + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: Place a new order in the store + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + "405": + description: Invalid input + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID + description: For valid response try integer IDs with value <= 5 or > 10. Other + values will generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid ID supplied + "404": + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: For valid response try integer IDs with value < 1000. Anything + above 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + responses: + "400": + description: Invalid ID supplied + "404": + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + default: + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: Creates list of users with given input array + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + default: + description: successful operation + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: "" + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + "400": + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: "" + operationId: logoutUser + parameters: [] + responses: + default: + description: successful operation + /user/{username}: + get: + tags: + - user + summary: Get user by user name + description: "" + operationId: getUserByName + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + "400": + description: Invalid username supplied + "404": + description: User not found + put: + tags: + - user + summary: Update user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that needs to be updated + required: true + schema: + type: string + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + default: + description: successful operation + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "400": + description: Invalid username supplied + "404": + description: User not found +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Customer: + type: object + properties: + id: + type: integer + format: int64 + example: 100000 + username: + type: string + example: fehguy + address: + type: array + xml: + name: addresses + wrapped: true + items: + $ref: '#/components/schemas/Address' + xml: + name: customer + Address: + type: object + properties: + street: + type: string + example: 437 Lytton + city: + type: string + example: Palo Alto + state: + type: string + example: CA + zip: + type: string + example: "94301" + xml: + name: address + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: "12345" + phone: + type: string + example: "12345" + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/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 + xml: + name: '##default' + requestBodies: + Pet: + 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' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/citrus-context.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/citrus-context.xml new file mode 100644 index 0000000000..3f2a783fca --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/citrus-context.xml @@ -0,0 +1,7 @@ + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/ExtPetStoreOpenApi.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/ExtPetStoreOpenApi.java new file mode 100644 index 0000000000..a170a880f7 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/ExtPetStoreOpenApi.java @@ -0,0 +1,10 @@ +package org.citrusframework.openapi.generator.rest.extpetstore; + +import org.citrusframework.openapi.OpenApiSpecification; + +public class ExtPetStoreOpenApi { + + public static final OpenApiSpecification extPetStoreSpecification = OpenApiSpecification + .from(ExtPetStoreOpenApi.class.getResource("ExtPetStore_openApi.yaml")); + +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/Category.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/Category.java new file mode 100644 index 0000000000..25c79ddbb6 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/Category.java @@ -0,0 +1,118 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.extpetstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Category + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.524898+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class Category { + private Long id; + + private String _name; + + public Category() { + } + + public Category id(Long id) { + + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + @jakarta.annotation.Nullable + + public Long getId() { + return id; + } + + + public void setId(Long id) { + this.id = id; + } + + public Category _name(String _name) { + + this._name = _name; + return this; + } + + /** + * Get _name + * @return _name + */ + @jakarta.annotation.Nullable + + public String getName() { + return _name; + } + + + public void setName(String _name) { + this._name = _name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Category category = (Category) o; + return Objects.equals(this.id, category.id) && + Objects.equals(this._name, category._name); + } + + @Override + public int hashCode() { + return Objects.hash(id, _name); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Category {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" _name: ").append(toIndentedString(_name)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/HistoricalData.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/HistoricalData.java new file mode 100644 index 0000000000..a4966efcb1 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/HistoricalData.java @@ -0,0 +1,119 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.extpetstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.time.LocalDate; + +/** + * Additional historical data for a vaccination report, not contained in internal storage. + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.524898+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class HistoricalData { + private LocalDate lastVaccinationDate; + + private Integer vaccinationCount; + + public HistoricalData() { + } + + public HistoricalData lastVaccinationDate(LocalDate lastVaccinationDate) { + + this.lastVaccinationDate = lastVaccinationDate; + return this; + } + + /** + * The date of the last vaccination. + * @return lastVaccinationDate + */ + @jakarta.annotation.Nullable + + public LocalDate getLastVaccinationDate() { + return lastVaccinationDate; + } + + + public void setLastVaccinationDate(LocalDate lastVaccinationDate) { + this.lastVaccinationDate = lastVaccinationDate; + } + + public HistoricalData vaccinationCount(Integer vaccinationCount) { + + this.vaccinationCount = vaccinationCount; + return this; + } + + /** + * The number of vaccinations the pet has received. + * @return vaccinationCount + */ + @jakarta.annotation.Nullable + + public Integer getVaccinationCount() { + return vaccinationCount; + } + + + public void setVaccinationCount(Integer vaccinationCount) { + this.vaccinationCount = vaccinationCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + HistoricalData historicalData = (HistoricalData) o; + return Objects.equals(this.lastVaccinationDate, historicalData.lastVaccinationDate) && + Objects.equals(this.vaccinationCount, historicalData.vaccinationCount); + } + + @Override + public int hashCode() { + return Objects.hash(lastVaccinationDate, vaccinationCount); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class HistoricalData {\n"); + sb.append(" lastVaccinationDate: ").append(toIndentedString(lastVaccinationDate)).append("\n"); + sb.append(" vaccinationCount: ").append(toIndentedString(vaccinationCount)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/Pet.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/Pet.java new file mode 100644 index 0000000000..19a5293f83 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/Pet.java @@ -0,0 +1,274 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.extpetstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.citrusframework.openapi.generator.rest.extpetstore.model.Category; +import org.citrusframework.openapi.generator.rest.extpetstore.model.Tag; + +/** + * Pet + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.524898+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class Pet { + private Long id; + + private String _name; + + private Category category; + + private List photoUrls = new ArrayList<>(); + + private List tags = new ArrayList<>(); + + /** + * pet status in the store + */ + public enum StatusEnum { + AVAILABLE("available"), + + PENDING("pending"), + + SOLD("sold"); + + private String value; + + StatusEnum(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + public static StatusEnum fromValue(String value) { + for (StatusEnum b : StatusEnum.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + } + + private StatusEnum status; + + public Pet() { + } + + public Pet id(Long id) { + + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + @jakarta.annotation.Nullable + + public Long getId() { + return id; + } + + + public void setId(Long id) { + this.id = id; + } + + public Pet _name(String _name) { + + this._name = _name; + return this; + } + + /** + * Get _name + * @return _name + */ + @jakarta.annotation.Nonnull + + public String getName() { + return _name; + } + + + public void setName(String _name) { + this._name = _name; + } + + public Pet category(Category category) { + + this.category = category; + return this; + } + + /** + * Get category + * @return category + */ + @jakarta.annotation.Nullable + + public Category getCategory() { + return category; + } + + + public void setCategory(Category category) { + this.category = category; + } + + public Pet photoUrls(List photoUrls) { + + this.photoUrls = photoUrls; + return this; + } + + public Pet addPhotoUrlsItem(String photoUrlsItem) { + if (this.photoUrls == null) { + this.photoUrls = new ArrayList<>(); + } + this.photoUrls.add(photoUrlsItem); + return this; + } + + /** + * Get photoUrls + * @return photoUrls + */ + @jakarta.annotation.Nonnull + + public List getPhotoUrls() { + return photoUrls; + } + + + public void setPhotoUrls(List photoUrls) { + this.photoUrls = photoUrls; + } + + public Pet tags(List tags) { + + this.tags = tags; + return this; + } + + public Pet addTagsItem(Tag tagsItem) { + if (this.tags == null) { + this.tags = new ArrayList<>(); + } + this.tags.add(tagsItem); + return this; + } + + /** + * Get tags + * @return tags + */ + @jakarta.annotation.Nullable + + public List getTags() { + return tags; + } + + + public void setTags(List tags) { + this.tags = tags; + } + + public Pet status(StatusEnum status) { + + this.status = status; + return this; + } + + /** + * pet status in the store + * @return status + */ + @jakarta.annotation.Nullable + + public StatusEnum getStatus() { + return status; + } + + + public void setStatus(StatusEnum status) { + this.status = status; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Pet pet = (Pet) o; + return Objects.equals(this.id, pet.id) && + Objects.equals(this._name, pet._name) && + Objects.equals(this.category, pet.category) && + Objects.equals(this.photoUrls, pet.photoUrls) && + Objects.equals(this.tags, pet.tags) && + Objects.equals(this.status, pet.status); + } + + @Override + public int hashCode() { + return Objects.hash(id, _name, category, photoUrls, tags, status); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Pet {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" _name: ").append(toIndentedString(_name)).append("\n"); + sb.append(" category: ").append(toIndentedString(category)).append("\n"); + sb.append(" photoUrls: ").append(toIndentedString(photoUrls)).append("\n"); + sb.append(" tags: ").append(toIndentedString(tags)).append("\n"); + sb.append(" status: ").append(toIndentedString(status)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/PetIdentifier.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/PetIdentifier.java new file mode 100644 index 0000000000..49117bed19 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/PetIdentifier.java @@ -0,0 +1,118 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.extpetstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * PetIdentifier + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.524898+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class PetIdentifier { + private String _name; + + private String alias; + + public PetIdentifier() { + } + + public PetIdentifier _name(String _name) { + + this._name = _name; + return this; + } + + /** + * Get _name + * @return _name + */ + @jakarta.annotation.Nullable + + public String getName() { + return _name; + } + + + public void setName(String _name) { + this._name = _name; + } + + public PetIdentifier alias(String alias) { + + this.alias = alias; + return this; + } + + /** + * Get alias + * @return alias + */ + @jakarta.annotation.Nullable + + public String getAlias() { + return alias; + } + + + public void setAlias(String alias) { + this.alias = alias; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PetIdentifier petIdentifier = (PetIdentifier) o; + return Objects.equals(this._name, petIdentifier._name) && + Objects.equals(this.alias, petIdentifier.alias); + } + + @Override + public int hashCode() { + return Objects.hash(_name, alias); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PetIdentifier {\n"); + sb.append(" _name: ").append(toIndentedString(_name)).append("\n"); + sb.append(" alias: ").append(toIndentedString(alias)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/Tag.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/Tag.java new file mode 100644 index 0000000000..c95a7058f5 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/Tag.java @@ -0,0 +1,118 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.extpetstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Tag + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.524898+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class Tag { + private Long id; + + private String _name; + + public Tag() { + } + + public Tag id(Long id) { + + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + @jakarta.annotation.Nullable + + public Long getId() { + return id; + } + + + public void setId(Long id) { + this.id = id; + } + + public Tag _name(String _name) { + + this._name = _name; + return this; + } + + /** + * Get _name + * @return _name + */ + @jakarta.annotation.Nullable + + public String getName() { + return _name; + } + + + public void setName(String _name) { + this._name = _name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Tag tag = (Tag) o; + return Objects.equals(this.id, tag.id) && + Objects.equals(this._name, tag._name); + } + + @Override + public int hashCode() { + return Objects.hash(id, _name); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Tag {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" _name: ").append(toIndentedString(_name)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/VaccinationDocumentResult.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/VaccinationDocumentResult.java new file mode 100644 index 0000000000..4600b1ece3 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/model/VaccinationDocumentResult.java @@ -0,0 +1,93 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.extpetstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * VaccinationDocumentResult + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.524898+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class VaccinationDocumentResult { + private String documentId; + + public VaccinationDocumentResult() { + } + + public VaccinationDocumentResult documentId(String documentId) { + + this.documentId = documentId; + return this; + } + + /** + * The unique ID of the uploaded vaccination document. + * @return documentId + */ + @jakarta.annotation.Nullable + + public String getDocumentId() { + return documentId; + } + + + public void setDocumentId(String documentId) { + this.documentId = documentId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VaccinationDocumentResult vaccinationDocumentResult = (VaccinationDocumentResult) o; + return Objects.equals(this.documentId, vaccinationDocumentResult.documentId); + } + + @Override + public int hashCode() { + return Objects.hash(documentId); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class VaccinationDocumentResult {\n"); + sb.append(" documentId: ").append(toIndentedString(documentId)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/request/ExtPetApi.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/request/ExtPetApi.java new file mode 100644 index 0000000000..79f431441a --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/request/ExtPetApi.java @@ -0,0 +1,5496 @@ +package org.citrusframework.openapi.generator.rest.extpetstore.request; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.citrusframework.util.StringUtils.isEmpty; +import static org.citrusframework.util.StringUtils.isNotEmpty; + +import static org.citrusframework.openapi.generator.rest.extpetstore.ExtPetStoreOpenApi.extPetStoreSpecification; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.net.URL; +import java.time.LocalDate; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.UUID; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +import org.citrusframework.openapi.testapi.GeneratedApiOperationInfo; +import org.citrusframework.openapi.testapi.ParameterStyle; +import org.citrusframework.openapi.testapi.RestApiSendMessageActionBuilder; +import org.citrusframework.openapi.testapi.RestApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.testapi.TestApiUtils; +import org.citrusframework.spi.Resource; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; + +import org.citrusframework.openapi.generator.rest.extpetstore.ExtPetStoreOpenApi; +import java.math.BigDecimal; +import org.citrusframework.openapi.generator.rest.extpetstore.model.HistoricalData; +import java.time.LocalDate; +import org.citrusframework.openapi.generator.rest.extpetstore.model.Pet; +import org.citrusframework.openapi.generator.rest.extpetstore.model.PetIdentifier; +import java.util.UUID; +import org.citrusframework.openapi.generator.rest.extpetstore.model.VaccinationDocumentResult; + +@SuppressWarnings("unused") +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.524898+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class ExtPetApi implements GeneratedApi +{ + + @Value("${" + "extpetstore.base64-encode-api-key:#{false}}") + private boolean base64EncodeApiKey; + + @Value("${" + "extpetstore.basic.username:#{null}}") + private String basicUsername; + + @Value("${" + "extpetstore.basic.password:#{null}}") + private String basicPassword; + + @Value("${" + "extpetstore.bearer.token:#{null}}") + private String basicAuthBearer; + + @Value("${" + "extpetstore.api-key-header:#{null}}") + private String defaultApiKeyHeader; + + @Value("${" + "extpetstore.api-key-cookie:#{null}}") + private String defaultApiKeyCookie; + + @Value("${" + "extpetstore.api-key-query:#{null}}") + private String defaultApiKeyQuery; + + private final List customizers; + + /** + * An optional default endpoint which will be passed into the requests. + */ + private final Endpoint defaultEndpoint; + + public ExtPetApi(@Nullable Endpoint defaultEndpoint) { + this(defaultEndpoint, emptyList()); + } + + public ExtPetApi(@Nullable Endpoint defaultEndpoint, @Nullable List customizers) { + this.defaultEndpoint = defaultEndpoint; + this.customizers = customizers; + } + + public static ExtPetApi extPetApi(Endpoint defaultEndpoint) { + return new ExtPetApi(defaultEndpoint); + } + + @Override + public String getApiTitle() { + return "Extended Petstore API"; + } + + @Override + public String getApiVersion() { + return "1.0.0"; + } + + @Override + public String getApiPrefix() { + return "ExtPetStore"; + } + + @Override + public Map getApiInfoExtensions() { + return emptyMap(); + } + + @Override + @Nullable + public Endpoint getEndpoint() { + return defaultEndpoint; + } + + @Override + public List getCustomizers() { + return customizers; + } + + /** + * Builder with type safe required parameters. + */ + public GenerateVaccinationReportSendActionBuilder sendGenerateVaccinationReport(org.citrusframework.spi.Resource template, Integer reqIntVal) { + return new GenerateVaccinationReportSendActionBuilder(this, template, reqIntVal); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GenerateVaccinationReportSendActionBuilder sendGenerateVaccinationReport$(String templateExpression, String reqIntValExpression ) { + return new GenerateVaccinationReportSendActionBuilder(templateExpression, reqIntValExpression, this); + } + + public GenerateVaccinationReportReceiveActionBuilder receiveGenerateVaccinationReport(@NotNull HttpStatus statusCode) { + return new GenerateVaccinationReportReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GenerateVaccinationReportReceiveActionBuilder receiveGenerateVaccinationReport(@NotNull String statusCode) { + return new GenerateVaccinationReportReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder sendGetPetByIdWithApiKeyAuthentication(Long petId, Boolean allDetails) { + GetPetByIdWithApiKeyAuthenticationSendActionBuilder builder = new GetPetByIdWithApiKeyAuthenticationSendActionBuilder(this, petId, allDetails); + builder.setBase64EncodeApiKey(base64EncodeApiKey); + return builder; + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder sendGetPetByIdWithApiKeyAuthentication$(String petIdExpression, String allDetailsExpression ) { + GetPetByIdWithApiKeyAuthenticationSendActionBuilder builder = new GetPetByIdWithApiKeyAuthenticationSendActionBuilder(petIdExpression, allDetailsExpression, this); + builder.setBase64EncodeApiKey(base64EncodeApiKey); + builder.setApiKeyQuery(defaultApiKeyQuery); + builder.setApiKeyHeader(defaultApiKeyHeader); + builder.setApiKeyCookie(defaultApiKeyCookie); + return builder; + } + + public GetPetByIdWithApiKeyAuthenticationReceiveActionBuilder receiveGetPetByIdWithApiKeyAuthentication(@NotNull HttpStatus statusCode) { + return new GetPetByIdWithApiKeyAuthenticationReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetByIdWithApiKeyAuthenticationReceiveActionBuilder receiveGetPetByIdWithApiKeyAuthentication(@NotNull String statusCode) { + return new GetPetByIdWithApiKeyAuthenticationReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetByIdWithBasicAuthenticationSendActionBuilder sendGetPetByIdWithBasicAuthentication(Long petId, Boolean allDetails) { + GetPetByIdWithBasicAuthenticationSendActionBuilder builder = new GetPetByIdWithBasicAuthenticationSendActionBuilder(this, petId, allDetails); + return builder; + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetByIdWithBasicAuthenticationSendActionBuilder sendGetPetByIdWithBasicAuthentication$(String petIdExpression, String allDetailsExpression ) { + GetPetByIdWithBasicAuthenticationSendActionBuilder builder = new GetPetByIdWithBasicAuthenticationSendActionBuilder(petIdExpression, allDetailsExpression, this); + builder.setBasicAuthUsername(basicUsername); + builder.setBasicAuthPassword(basicPassword); + return builder; + } + + public GetPetByIdWithBasicAuthenticationReceiveActionBuilder receiveGetPetByIdWithBasicAuthentication(@NotNull HttpStatus statusCode) { + return new GetPetByIdWithBasicAuthenticationReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetByIdWithBasicAuthenticationReceiveActionBuilder receiveGetPetByIdWithBasicAuthentication(@NotNull String statusCode) { + return new GetPetByIdWithBasicAuthenticationReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetByIdWithBearerAuthenticationSendActionBuilder sendGetPetByIdWithBearerAuthentication(Long petId, Boolean allDetails) { + GetPetByIdWithBearerAuthenticationSendActionBuilder builder = new GetPetByIdWithBearerAuthenticationSendActionBuilder(this, petId, allDetails); + return builder; + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetByIdWithBearerAuthenticationSendActionBuilder sendGetPetByIdWithBearerAuthentication$(String petIdExpression, String allDetailsExpression ) { + GetPetByIdWithBearerAuthenticationSendActionBuilder builder = new GetPetByIdWithBearerAuthenticationSendActionBuilder(petIdExpression, allDetailsExpression, this); + builder.setBasicAuthBearer(basicAuthBearer); + return builder; + } + + public GetPetByIdWithBearerAuthenticationReceiveActionBuilder receiveGetPetByIdWithBearerAuthentication(@NotNull HttpStatus statusCode) { + return new GetPetByIdWithBearerAuthenticationReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetByIdWithBearerAuthenticationReceiveActionBuilder receiveGetPetByIdWithBearerAuthentication(@NotNull String statusCode) { + return new GetPetByIdWithBearerAuthenticationReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetByUuidSendActionBuilder sendGetPetByUuid(UUID petUuid) { + return new GetPetByUuidSendActionBuilder(this, petUuid); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetByUuidSendActionBuilder sendGetPetByUuid$(String petUuidExpression ) { + return new GetPetByUuidSendActionBuilder(petUuidExpression, this); + } + + public GetPetByUuidReceiveActionBuilder receiveGetPetByUuid(@NotNull HttpStatus statusCode) { + return new GetPetByUuidReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetByUuidReceiveActionBuilder receiveGetPetByUuid(@NotNull String statusCode) { + return new GetPetByUuidReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithCookieSendActionBuilder sendGetPetWithCookie(Long petId, String sessionId) { + return new GetPetWithCookieSendActionBuilder(this, petId, sessionId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithCookieSendActionBuilder sendGetPetWithCookie$(String petIdExpression, String sessionIdExpression ) { + return new GetPetWithCookieSendActionBuilder(petIdExpression, sessionIdExpression, this); + } + + public GetPetWithCookieReceiveActionBuilder receiveGetPetWithCookie(@NotNull HttpStatus statusCode) { + return new GetPetWithCookieReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithCookieReceiveActionBuilder receiveGetPetWithCookie(@NotNull String statusCode) { + return new GetPetWithCookieReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithDeepObjectTypeQuerySendActionBuilder sendGetPetWithDeepObjectTypeQuery(PetIdentifier petId) { + return new GetPetWithDeepObjectTypeQuerySendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithDeepObjectTypeQuerySendActionBuilder sendGetPetWithDeepObjectTypeQuery$(String petIdExpression ) { + return new GetPetWithDeepObjectTypeQuerySendActionBuilder(petIdExpression, this); + } + + public GetPetWithDeepObjectTypeQueryReceiveActionBuilder receiveGetPetWithDeepObjectTypeQuery(@NotNull HttpStatus statusCode) { + return new GetPetWithDeepObjectTypeQueryReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithDeepObjectTypeQueryReceiveActionBuilder receiveGetPetWithDeepObjectTypeQuery(@NotNull String statusCode) { + return new GetPetWithDeepObjectTypeQueryReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithFormExplodedStyleCookieSendActionBuilder sendGetPetWithFormExplodedStyleCookie(List petId) { + return new GetPetWithFormExplodedStyleCookieSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithFormExplodedStyleCookieSendActionBuilder sendGetPetWithFormExplodedStyleCookie$(List petIdExpression ) { + return new GetPetWithFormExplodedStyleCookieSendActionBuilder(petIdExpression, this); + } + + public GetPetWithFormExplodedStyleCookieReceiveActionBuilder receiveGetPetWithFormExplodedStyleCookie(@NotNull HttpStatus statusCode) { + return new GetPetWithFormExplodedStyleCookieReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithFormExplodedStyleCookieReceiveActionBuilder receiveGetPetWithFormExplodedStyleCookie(@NotNull String statusCode) { + return new GetPetWithFormExplodedStyleCookieReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithFormObjectStyleCookieSendActionBuilder sendGetPetWithFormObjectStyleCookie(PetIdentifier petId) { + return new GetPetWithFormObjectStyleCookieSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithFormObjectStyleCookieSendActionBuilder sendGetPetWithFormObjectStyleCookie$(String petIdExpression ) { + return new GetPetWithFormObjectStyleCookieSendActionBuilder(petIdExpression, this); + } + + public GetPetWithFormObjectStyleCookieReceiveActionBuilder receiveGetPetWithFormObjectStyleCookie(@NotNull HttpStatus statusCode) { + return new GetPetWithFormObjectStyleCookieReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithFormObjectStyleCookieReceiveActionBuilder receiveGetPetWithFormObjectStyleCookie(@NotNull String statusCode) { + return new GetPetWithFormObjectStyleCookieReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithFormStyleCookieSendActionBuilder sendGetPetWithFormStyleCookie(List petId) { + return new GetPetWithFormStyleCookieSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithFormStyleCookieSendActionBuilder sendGetPetWithFormStyleCookie$(List petIdExpression ) { + return new GetPetWithFormStyleCookieSendActionBuilder(petIdExpression, this); + } + + public GetPetWithFormStyleCookieReceiveActionBuilder receiveGetPetWithFormStyleCookie(@NotNull HttpStatus statusCode) { + return new GetPetWithFormStyleCookieReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithFormStyleCookieReceiveActionBuilder receiveGetPetWithFormStyleCookie(@NotNull String statusCode) { + return new GetPetWithFormStyleCookieReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithFormStyleExplodedObjectQuerySendActionBuilder sendGetPetWithFormStyleExplodedObjectQuery(PetIdentifier petId) { + return new GetPetWithFormStyleExplodedObjectQuerySendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithFormStyleExplodedObjectQuerySendActionBuilder sendGetPetWithFormStyleExplodedObjectQuery$(String petIdExpression ) { + return new GetPetWithFormStyleExplodedObjectQuerySendActionBuilder(petIdExpression, this); + } + + public GetPetWithFormStyleExplodedObjectQueryReceiveActionBuilder receiveGetPetWithFormStyleExplodedObjectQuery(@NotNull HttpStatus statusCode) { + return new GetPetWithFormStyleExplodedObjectQueryReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithFormStyleExplodedObjectQueryReceiveActionBuilder receiveGetPetWithFormStyleExplodedObjectQuery(@NotNull String statusCode) { + return new GetPetWithFormStyleExplodedObjectQueryReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithFormStyleExplodedQuerySendActionBuilder sendGetPetWithFormStyleExplodedQuery(List petId) { + return new GetPetWithFormStyleExplodedQuerySendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithFormStyleExplodedQuerySendActionBuilder sendGetPetWithFormStyleExplodedQuery$(List petIdExpression ) { + return new GetPetWithFormStyleExplodedQuerySendActionBuilder(petIdExpression, this); + } + + public GetPetWithFormStyleExplodedQueryReceiveActionBuilder receiveGetPetWithFormStyleExplodedQuery(@NotNull HttpStatus statusCode) { + return new GetPetWithFormStyleExplodedQueryReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithFormStyleExplodedQueryReceiveActionBuilder receiveGetPetWithFormStyleExplodedQuery(@NotNull String statusCode) { + return new GetPetWithFormStyleExplodedQueryReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithFormStyleObjectQuerySendActionBuilder sendGetPetWithFormStyleObjectQuery(PetIdentifier petId) { + return new GetPetWithFormStyleObjectQuerySendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithFormStyleObjectQuerySendActionBuilder sendGetPetWithFormStyleObjectQuery$(String petIdExpression ) { + return new GetPetWithFormStyleObjectQuerySendActionBuilder(petIdExpression, this); + } + + public GetPetWithFormStyleObjectQueryReceiveActionBuilder receiveGetPetWithFormStyleObjectQuery(@NotNull HttpStatus statusCode) { + return new GetPetWithFormStyleObjectQueryReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithFormStyleObjectQueryReceiveActionBuilder receiveGetPetWithFormStyleObjectQuery(@NotNull String statusCode) { + return new GetPetWithFormStyleObjectQueryReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithFormStyleQuerySendActionBuilder sendGetPetWithFormStyleQuery(List petId) { + return new GetPetWithFormStyleQuerySendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithFormStyleQuerySendActionBuilder sendGetPetWithFormStyleQuery$(List petIdExpression ) { + return new GetPetWithFormStyleQuerySendActionBuilder(petIdExpression, this); + } + + public GetPetWithFormStyleQueryReceiveActionBuilder receiveGetPetWithFormStyleQuery(@NotNull HttpStatus statusCode) { + return new GetPetWithFormStyleQueryReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithFormStyleQueryReceiveActionBuilder receiveGetPetWithFormStyleQuery(@NotNull String statusCode) { + return new GetPetWithFormStyleQueryReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithLabelStyleArraySendActionBuilder sendGetPetWithLabelStyleArray(List petId) { + return new GetPetWithLabelStyleArraySendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithLabelStyleArraySendActionBuilder sendGetPetWithLabelStyleArray$(List petIdExpression ) { + return new GetPetWithLabelStyleArraySendActionBuilder(petIdExpression, this); + } + + public GetPetWithLabelStyleArrayReceiveActionBuilder receiveGetPetWithLabelStyleArray(@NotNull HttpStatus statusCode) { + return new GetPetWithLabelStyleArrayReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithLabelStyleArrayReceiveActionBuilder receiveGetPetWithLabelStyleArray(@NotNull String statusCode) { + return new GetPetWithLabelStyleArrayReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithLabelStyleArrayExplodedSendActionBuilder sendGetPetWithLabelStyleArrayExploded(List petId) { + return new GetPetWithLabelStyleArrayExplodedSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithLabelStyleArrayExplodedSendActionBuilder sendGetPetWithLabelStyleArrayExploded$(List petIdExpression ) { + return new GetPetWithLabelStyleArrayExplodedSendActionBuilder(petIdExpression, this); + } + + public GetPetWithLabelStyleArrayExplodedReceiveActionBuilder receiveGetPetWithLabelStyleArrayExploded(@NotNull HttpStatus statusCode) { + return new GetPetWithLabelStyleArrayExplodedReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithLabelStyleArrayExplodedReceiveActionBuilder receiveGetPetWithLabelStyleArrayExploded(@NotNull String statusCode) { + return new GetPetWithLabelStyleArrayExplodedReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithLabelStyleObjectSendActionBuilder sendGetPetWithLabelStyleObject(PetIdentifier petId) { + return new GetPetWithLabelStyleObjectSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithLabelStyleObjectSendActionBuilder sendGetPetWithLabelStyleObject$(String petIdExpression ) { + return new GetPetWithLabelStyleObjectSendActionBuilder(petIdExpression, this); + } + + public GetPetWithLabelStyleObjectReceiveActionBuilder receiveGetPetWithLabelStyleObject(@NotNull HttpStatus statusCode) { + return new GetPetWithLabelStyleObjectReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithLabelStyleObjectReceiveActionBuilder receiveGetPetWithLabelStyleObject(@NotNull String statusCode) { + return new GetPetWithLabelStyleObjectReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithLabelStyleObjectExplodedSendActionBuilder sendGetPetWithLabelStyleObjectExploded(PetIdentifier petId) { + return new GetPetWithLabelStyleObjectExplodedSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithLabelStyleObjectExplodedSendActionBuilder sendGetPetWithLabelStyleObjectExploded$(String petIdExpression ) { + return new GetPetWithLabelStyleObjectExplodedSendActionBuilder(petIdExpression, this); + } + + public GetPetWithLabelStyleObjectExplodedReceiveActionBuilder receiveGetPetWithLabelStyleObjectExploded(@NotNull HttpStatus statusCode) { + return new GetPetWithLabelStyleObjectExplodedReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithLabelStyleObjectExplodedReceiveActionBuilder receiveGetPetWithLabelStyleObjectExploded(@NotNull String statusCode) { + return new GetPetWithLabelStyleObjectExplodedReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithMatrixStyleArraySendActionBuilder sendGetPetWithMatrixStyleArray(List petId) { + return new GetPetWithMatrixStyleArraySendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithMatrixStyleArraySendActionBuilder sendGetPetWithMatrixStyleArray$(List petIdExpression ) { + return new GetPetWithMatrixStyleArraySendActionBuilder(petIdExpression, this); + } + + public GetPetWithMatrixStyleArrayReceiveActionBuilder receiveGetPetWithMatrixStyleArray(@NotNull HttpStatus statusCode) { + return new GetPetWithMatrixStyleArrayReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithMatrixStyleArrayReceiveActionBuilder receiveGetPetWithMatrixStyleArray(@NotNull String statusCode) { + return new GetPetWithMatrixStyleArrayReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithMatrixStyleArrayExplodedSendActionBuilder sendGetPetWithMatrixStyleArrayExploded(List petId) { + return new GetPetWithMatrixStyleArrayExplodedSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithMatrixStyleArrayExplodedSendActionBuilder sendGetPetWithMatrixStyleArrayExploded$(List petIdExpression ) { + return new GetPetWithMatrixStyleArrayExplodedSendActionBuilder(petIdExpression, this); + } + + public GetPetWithMatrixStyleArrayExplodedReceiveActionBuilder receiveGetPetWithMatrixStyleArrayExploded(@NotNull HttpStatus statusCode) { + return new GetPetWithMatrixStyleArrayExplodedReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithMatrixStyleArrayExplodedReceiveActionBuilder receiveGetPetWithMatrixStyleArrayExploded(@NotNull String statusCode) { + return new GetPetWithMatrixStyleArrayExplodedReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithMatrixStyleObjectSendActionBuilder sendGetPetWithMatrixStyleObject(PetIdentifier petId) { + return new GetPetWithMatrixStyleObjectSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithMatrixStyleObjectSendActionBuilder sendGetPetWithMatrixStyleObject$(String petIdExpression ) { + return new GetPetWithMatrixStyleObjectSendActionBuilder(petIdExpression, this); + } + + public GetPetWithMatrixStyleObjectReceiveActionBuilder receiveGetPetWithMatrixStyleObject(@NotNull HttpStatus statusCode) { + return new GetPetWithMatrixStyleObjectReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithMatrixStyleObjectReceiveActionBuilder receiveGetPetWithMatrixStyleObject(@NotNull String statusCode) { + return new GetPetWithMatrixStyleObjectReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithMatrixStyleObjectExplodedSendActionBuilder sendGetPetWithMatrixStyleObjectExploded(PetIdentifier petId) { + return new GetPetWithMatrixStyleObjectExplodedSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithMatrixStyleObjectExplodedSendActionBuilder sendGetPetWithMatrixStyleObjectExploded$(String petIdExpression ) { + return new GetPetWithMatrixStyleObjectExplodedSendActionBuilder(petIdExpression, this); + } + + public GetPetWithMatrixStyleObjectExplodedReceiveActionBuilder receiveGetPetWithMatrixStyleObjectExploded(@NotNull HttpStatus statusCode) { + return new GetPetWithMatrixStyleObjectExplodedReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithMatrixStyleObjectExplodedReceiveActionBuilder receiveGetPetWithMatrixStyleObjectExploded(@NotNull String statusCode) { + return new GetPetWithMatrixStyleObjectExplodedReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithParametersRequiringEncodingSendActionBuilder sendGetPetWithParametersRequiringEncoding(Integer petID) { + return new GetPetWithParametersRequiringEncodingSendActionBuilder(this, petID); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithParametersRequiringEncodingSendActionBuilder sendGetPetWithParametersRequiringEncoding$(String petIDExpression ) { + return new GetPetWithParametersRequiringEncodingSendActionBuilder(petIDExpression, this); + } + + public GetPetWithParametersRequiringEncodingReceiveActionBuilder receiveGetPetWithParametersRequiringEncoding(@NotNull HttpStatus statusCode) { + return new GetPetWithParametersRequiringEncodingReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithParametersRequiringEncodingReceiveActionBuilder receiveGetPetWithParametersRequiringEncoding(@NotNull String statusCode) { + return new GetPetWithParametersRequiringEncodingReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithSimpleStyleArraySendActionBuilder sendGetPetWithSimpleStyleArray(List petId) { + return new GetPetWithSimpleStyleArraySendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithSimpleStyleArraySendActionBuilder sendGetPetWithSimpleStyleArray$(List petIdExpression ) { + return new GetPetWithSimpleStyleArraySendActionBuilder(petIdExpression, this); + } + + public GetPetWithSimpleStyleArrayReceiveActionBuilder receiveGetPetWithSimpleStyleArray(@NotNull HttpStatus statusCode) { + return new GetPetWithSimpleStyleArrayReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithSimpleStyleArrayReceiveActionBuilder receiveGetPetWithSimpleStyleArray(@NotNull String statusCode) { + return new GetPetWithSimpleStyleArrayReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithSimpleStyleArrayExplodedSendActionBuilder sendGetPetWithSimpleStyleArrayExploded(List petId) { + return new GetPetWithSimpleStyleArrayExplodedSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithSimpleStyleArrayExplodedSendActionBuilder sendGetPetWithSimpleStyleArrayExploded$(List petIdExpression ) { + return new GetPetWithSimpleStyleArrayExplodedSendActionBuilder(petIdExpression, this); + } + + public GetPetWithSimpleStyleArrayExplodedReceiveActionBuilder receiveGetPetWithSimpleStyleArrayExploded(@NotNull HttpStatus statusCode) { + return new GetPetWithSimpleStyleArrayExplodedReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithSimpleStyleArrayExplodedReceiveActionBuilder receiveGetPetWithSimpleStyleArrayExploded(@NotNull String statusCode) { + return new GetPetWithSimpleStyleArrayExplodedReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithSimpleStyleExplodedHeaderSendActionBuilder sendGetPetWithSimpleStyleExplodedHeader(List petId) { + return new GetPetWithSimpleStyleExplodedHeaderSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithSimpleStyleExplodedHeaderSendActionBuilder sendGetPetWithSimpleStyleExplodedHeader$(List petIdExpression ) { + return new GetPetWithSimpleStyleExplodedHeaderSendActionBuilder(petIdExpression, this); + } + + public GetPetWithSimpleStyleExplodedHeaderReceiveActionBuilder receiveGetPetWithSimpleStyleExplodedHeader(@NotNull HttpStatus statusCode) { + return new GetPetWithSimpleStyleExplodedHeaderReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithSimpleStyleExplodedHeaderReceiveActionBuilder receiveGetPetWithSimpleStyleExplodedHeader(@NotNull String statusCode) { + return new GetPetWithSimpleStyleExplodedHeaderReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithSimpleStyleExplodedObjectHeaderSendActionBuilder sendGetPetWithSimpleStyleExplodedObjectHeader(PetIdentifier petId) { + return new GetPetWithSimpleStyleExplodedObjectHeaderSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithSimpleStyleExplodedObjectHeaderSendActionBuilder sendGetPetWithSimpleStyleExplodedObjectHeader$(String petIdExpression ) { + return new GetPetWithSimpleStyleExplodedObjectHeaderSendActionBuilder(petIdExpression, this); + } + + public GetPetWithSimpleStyleExplodedObjectHeaderReceiveActionBuilder receiveGetPetWithSimpleStyleExplodedObjectHeader(@NotNull HttpStatus statusCode) { + return new GetPetWithSimpleStyleExplodedObjectHeaderReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithSimpleStyleExplodedObjectHeaderReceiveActionBuilder receiveGetPetWithSimpleStyleExplodedObjectHeader(@NotNull String statusCode) { + return new GetPetWithSimpleStyleExplodedObjectHeaderReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithSimpleStyleHeaderSendActionBuilder sendGetPetWithSimpleStyleHeader(List petId) { + return new GetPetWithSimpleStyleHeaderSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithSimpleStyleHeaderSendActionBuilder sendGetPetWithSimpleStyleHeader$(List petIdExpression ) { + return new GetPetWithSimpleStyleHeaderSendActionBuilder(petIdExpression, this); + } + + public GetPetWithSimpleStyleHeaderReceiveActionBuilder receiveGetPetWithSimpleStyleHeader(@NotNull HttpStatus statusCode) { + return new GetPetWithSimpleStyleHeaderReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithSimpleStyleHeaderReceiveActionBuilder receiveGetPetWithSimpleStyleHeader(@NotNull String statusCode) { + return new GetPetWithSimpleStyleHeaderReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithSimpleStyleObjectSendActionBuilder sendGetPetWithSimpleStyleObject(PetIdentifier petId) { + return new GetPetWithSimpleStyleObjectSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithSimpleStyleObjectSendActionBuilder sendGetPetWithSimpleStyleObject$(String petIdExpression ) { + return new GetPetWithSimpleStyleObjectSendActionBuilder(petIdExpression, this); + } + + public GetPetWithSimpleStyleObjectReceiveActionBuilder receiveGetPetWithSimpleStyleObject(@NotNull HttpStatus statusCode) { + return new GetPetWithSimpleStyleObjectReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithSimpleStyleObjectReceiveActionBuilder receiveGetPetWithSimpleStyleObject(@NotNull String statusCode) { + return new GetPetWithSimpleStyleObjectReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithSimpleStyleObjectExplodedSendActionBuilder sendGetPetWithSimpleStyleObjectExploded(PetIdentifier petId) { + return new GetPetWithSimpleStyleObjectExplodedSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithSimpleStyleObjectExplodedSendActionBuilder sendGetPetWithSimpleStyleObjectExploded$(String petIdExpression ) { + return new GetPetWithSimpleStyleObjectExplodedSendActionBuilder(petIdExpression, this); + } + + public GetPetWithSimpleStyleObjectExplodedReceiveActionBuilder receiveGetPetWithSimpleStyleObjectExploded(@NotNull HttpStatus statusCode) { + return new GetPetWithSimpleStyleObjectExplodedReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithSimpleStyleObjectExplodedReceiveActionBuilder receiveGetPetWithSimpleStyleObjectExploded(@NotNull String statusCode) { + return new GetPetWithSimpleStyleObjectExplodedReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetWithSimpleStyleObjectHeaderSendActionBuilder sendGetPetWithSimpleStyleObjectHeader(PetIdentifier petId) { + return new GetPetWithSimpleStyleObjectHeaderSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetWithSimpleStyleObjectHeaderSendActionBuilder sendGetPetWithSimpleStyleObjectHeader$(String petIdExpression ) { + return new GetPetWithSimpleStyleObjectHeaderSendActionBuilder(petIdExpression, this); + } + + public GetPetWithSimpleStyleObjectHeaderReceiveActionBuilder receiveGetPetWithSimpleStyleObjectHeader(@NotNull HttpStatus statusCode) { + return new GetPetWithSimpleStyleObjectHeaderReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetWithSimpleStyleObjectHeaderReceiveActionBuilder receiveGetPetWithSimpleStyleObjectHeader(@NotNull String statusCode) { + return new GetPetWithSimpleStyleObjectHeaderReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public PetWithoutOperationIdPetIdGetSendActionBuilder sendPetWithoutOperationIdPetIdGet(Integer petId) { + return new PetWithoutOperationIdPetIdGetSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public PetWithoutOperationIdPetIdGetSendActionBuilder sendPetWithoutOperationIdPetIdGet$(String petIdExpression ) { + return new PetWithoutOperationIdPetIdGetSendActionBuilder(petIdExpression, this); + } + + public PetWithoutOperationIdPetIdGetReceiveActionBuilder receivePetWithoutOperationIdPetIdGet(@NotNull HttpStatus statusCode) { + return new PetWithoutOperationIdPetIdGetReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public PetWithoutOperationIdPetIdGetReceiveActionBuilder receivePetWithoutOperationIdPetIdGet(@NotNull String statusCode) { + return new PetWithoutOperationIdPetIdGetReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public PostVaccinationDocumentSendActionBuilder sendPostVaccinationDocument(String bucket, String filename) { + return new PostVaccinationDocumentSendActionBuilder(this, bucket, filename); + } + + public PostVaccinationDocumentReceiveActionBuilder receivePostVaccinationDocument(@NotNull HttpStatus statusCode) { + return new PostVaccinationDocumentReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public PostVaccinationDocumentReceiveActionBuilder receivePostVaccinationDocument(@NotNull String statusCode) { + return new PostVaccinationDocumentReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public PostVaccinationFormDataSendActionBuilder sendPostVaccinationFormData() { + return new PostVaccinationFormDataSendActionBuilder(this); + } + + public PostVaccinationFormDataReceiveActionBuilder receivePostVaccinationFormData(@NotNull HttpStatus statusCode) { + return new PostVaccinationFormDataReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public PostVaccinationFormDataReceiveActionBuilder receivePostVaccinationFormData(@NotNull String statusCode) { + return new PostVaccinationFormDataReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public UpdatePetWithArrayQueryDataSendActionBuilder sendUpdatePetWithArrayQueryData(Long petId, String _name, String status, List tags, List nicknames, String sampleStringHeader) { + return new UpdatePetWithArrayQueryDataSendActionBuilder(this, petId, _name, status, tags, nicknames, sampleStringHeader); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public UpdatePetWithArrayQueryDataSendActionBuilder sendUpdatePetWithArrayQueryData$(String petIdExpression, String _nameExpression, String statusExpression, List tagsExpression, List nicknamesExpression, String sampleStringHeaderExpression ) { + return new UpdatePetWithArrayQueryDataSendActionBuilder(petIdExpression, _nameExpression, statusExpression, tagsExpression, nicknamesExpression, sampleStringHeaderExpression, this); + } + + public UpdatePetWithArrayQueryDataReceiveActionBuilder receiveUpdatePetWithArrayQueryData(@NotNull HttpStatus statusCode) { + return new UpdatePetWithArrayQueryDataReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public UpdatePetWithArrayQueryDataReceiveActionBuilder receiveUpdatePetWithArrayQueryData(@NotNull String statusCode) { + return new UpdatePetWithArrayQueryDataReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public UpdatePetWithFormUrlEncodedSendActionBuilder sendUpdatePetWithFormUrlEncoded(Long petId, String _name, String status, Integer age, List tags) { + return new UpdatePetWithFormUrlEncodedSendActionBuilder(this, petId, _name, status, age, tags); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public UpdatePetWithFormUrlEncodedSendActionBuilder sendUpdatePetWithFormUrlEncoded$(String petIdExpression, String _nameExpression, String statusExpression, String ageExpression, List tagsExpression ) { + return new UpdatePetWithFormUrlEncodedSendActionBuilder(petIdExpression, _nameExpression, statusExpression, ageExpression, tagsExpression, this); + } + + public UpdatePetWithFormUrlEncodedReceiveActionBuilder receiveUpdatePetWithFormUrlEncoded(@NotNull HttpStatus statusCode) { + return new UpdatePetWithFormUrlEncodedReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public UpdatePetWithFormUrlEncodedReceiveActionBuilder receiveUpdatePetWithFormUrlEncoded(@NotNull String statusCode) { + return new UpdatePetWithFormUrlEncodedReceiveActionBuilder(this, statusCode); + } + + public static class GenerateVaccinationReportSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/ext/pet/vaccination/status-report"; + + private static final String OPERATION_NAME = "generateVaccinationReport"; + + /** + * Constructor with type safe required parameters. + */ + public GenerateVaccinationReportSendActionBuilder(ExtPetApi extPetApi, org.citrusframework.spi.Resource template, Integer reqIntVal) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + formParameter("template", toBinary(template) ); + formParameter("reqIntVal", reqIntVal); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GenerateVaccinationReportSendActionBuilder(String templateExpression, String reqIntValExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + formParameter("template", toBinary(templateExpression) ); + formParameter("reqIntVal", reqIntValExpression); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GenerateVaccinationReportSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String templateExpression, String reqIntValExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + formParameter("template", toBinary(templateExpression) ); + formParameter("reqIntVal", reqIntValExpression); + } + + public GenerateVaccinationReportSendActionBuilder template(org.citrusframework.spi.Resource template) { + formParameter("template", toBinary(template) ); + return this; + } + + public GenerateVaccinationReportSendActionBuilder template(String templateExpression) { + formParameter("template", toBinary(templateExpression) ); + return this; + } + + public GenerateVaccinationReportSendActionBuilder reqIntVal(Integer reqIntVal) { + formParameter("reqIntVal", reqIntVal); + return this; + } + + public GenerateVaccinationReportSendActionBuilder reqIntVal(String reqIntValExpression) { + formParameter("reqIntVal", reqIntValExpression); + return this; + } + + public GenerateVaccinationReportSendActionBuilder optIntVal(Integer optIntVal) { + formParameter("optIntVal", optIntVal); + return this; + } + + public void setOptIntVal(Integer optIntVal) { + formParameter("optIntVal", optIntVal); + } + + public GenerateVaccinationReportSendActionBuilder optIntVal(String optIntValExpression) { + formParameter("optIntVal", optIntValExpression); + return this; + } + + public void setOptIntVal(String optIntValExpression) { + formParameter("optIntVal", optIntValExpression); + } + + public GenerateVaccinationReportSendActionBuilder optBoolVal(Boolean optBoolVal) { + formParameter("optBoolVal", optBoolVal); + return this; + } + + public void setOptBoolVal(Boolean optBoolVal) { + formParameter("optBoolVal", optBoolVal); + } + + public GenerateVaccinationReportSendActionBuilder optBoolVal(String optBoolValExpression) { + formParameter("optBoolVal", optBoolValExpression); + return this; + } + + public void setOptBoolVal(String optBoolValExpression) { + formParameter("optBoolVal", optBoolValExpression); + } + + public GenerateVaccinationReportSendActionBuilder optNumberVal(BigDecimal optNumberVal) { + formParameter("optNumberVal", optNumberVal); + return this; + } + + public void setOptNumberVal(BigDecimal optNumberVal) { + formParameter("optNumberVal", optNumberVal); + } + + public GenerateVaccinationReportSendActionBuilder optNumberVal(String optNumberValExpression) { + formParameter("optNumberVal", optNumberValExpression); + return this; + } + + public void setOptNumberVal(String optNumberValExpression) { + formParameter("optNumberVal", optNumberValExpression); + } + + public GenerateVaccinationReportSendActionBuilder optStringVal(String optStringVal) { + formParameter("optStringVal", optStringVal); + return this; + } + + public void setOptStringVal(String optStringVal) { + formParameter("optStringVal", optStringVal); + } + + public GenerateVaccinationReportSendActionBuilder optDateVal(LocalDate optDateVal) { + formParameter("optDateVal", optDateVal); + return this; + } + + public void setOptDateVal(LocalDate optDateVal) { + formParameter("optDateVal", optDateVal); + } + + public GenerateVaccinationReportSendActionBuilder optDateVal(String optDateValExpression) { + formParameter("optDateVal", optDateValExpression); + return this; + } + + public void setOptDateVal(String optDateValExpression) { + formParameter("optDateVal", optDateValExpression); + } + + public GenerateVaccinationReportSendActionBuilder additionalData(HistoricalData additionalData) { + formParameter("additionalData", additionalData); + return this; + } + + public void setAdditionalData(HistoricalData additionalData) { + formParameter("additionalData", additionalData); + } + + public GenerateVaccinationReportSendActionBuilder additionalData(String additionalDataExpression) { + formParameter("additionalData", additionalDataExpression); + return this; + } + + public void setAdditionalData(String additionalDataExpression) { + formParameter("additionalData", additionalDataExpression); + } + + public GenerateVaccinationReportSendActionBuilder schema(org.citrusframework.spi.Resource schema) { + formParameter("schema", toBinary(schema) ); + return this; + } + + public void setSchema(org.citrusframework.spi.Resource schema) { + formParameter("schema", toBinary(schema) ); + } + + public GenerateVaccinationReportSendActionBuilder schema(String schemaExpression) { + formParameter("schema", toBinary(schemaExpression) ); + return this; + } + + public void setSchema(String schemaExpression) { + formParameter("schema", toBinary(schemaExpression) ); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GenerateVaccinationReportReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/ext/pet/vaccination/status-report"; + + private static final String OPERATION_NAME = "generateVaccinationReport"; + + public GenerateVaccinationReportReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GenerateVaccinationReportReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetByIdWithApiKeyAuthenticationSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/secure-api-key/pet/{petId}"; + + private static final String OPERATION_NAME = "getPetByIdWithApiKeyAuthentication"; + + @Value("${" + "extpetstore.base64-encode-api-key:#{false}}") + private boolean base64EncodeApiKey; + + @Value("${" + "extpetstore.api-key-query:#{null}}") + private String defaultApiKeyQuery; + + private String apiKeyQuery; + + @Value("${" + "extpetstore.api-key-header:#{null}}") + private String defaultApiKeyHeader; + + private String apiKeyHeader; + + @Value("${" + "extpetstore.api-key-cookie:#{null}}") + private String defaultApiKeyCookie; + + private String apiKeyCookie; + + /** + * Constructor with type safe required parameters. + */ + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder(ExtPetApi extPetApi, Long petId, Boolean allDetails) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + queryParameter("allDetails", allDetails, ParameterStyle.FORM, true, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder(String petIdExpression, String allDetailsExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + queryParameter("allDetails", allDetailsExpression, ParameterStyle.FORM, true, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression, String allDetailsExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + queryParameter("allDetails", allDetailsExpression, ParameterStyle.FORM, true, false); + } + + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder petId(Long petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder allDetails(Boolean allDetails) { + queryParameter("allDetails", allDetails, ParameterStyle.FORM, true, false); + return this; + } + + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder allDetails(String allDetailsExpression) { + queryParameter("allDetails", allDetailsExpression, ParameterStyle.FORM, true, false); + return this; + } + + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder details(String...details) { + queryParameter("details", details, ParameterStyle.FORM, true, false); + return this; + } + + public void setDetails(String...details) { + queryParameter("details", details, ParameterStyle.FORM, true, false); + } + + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder requesterInformation(String...requesterInformation) { + queryParameter("requesterInformation", requesterInformation, ParameterStyle.FORM, true, false); + return this; + } + + public void setRequesterInformation(String...requesterInformation) { + queryParameter("requesterInformation", requesterInformation, ParameterStyle.FORM, true, false); + } + + public void setBase64EncodeApiKey(boolean encode) { + this.base64EncodeApiKey = encode; + } + + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder apiKeyQuery(String apiKeyQuery) { + this.apiKeyQuery = apiKeyQuery; + return this; + } + + public void setApiKeyQuery(String apiKeyQuery) { + this.apiKeyQuery = apiKeyQuery; + } + + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder apiKeyHeader(String apiKeyHeader) { + this.apiKeyHeader = apiKeyHeader; + return this; + } + + public void setApiKeyHeader(String apiKeyHeader) { + this.apiKeyHeader = apiKeyHeader; + } + + public GetPetByIdWithApiKeyAuthenticationSendActionBuilder apiKeyCookie(String apiKeyCookie) { + this.apiKeyCookie = apiKeyCookie; + return this; + } + + public void setApiKeyCookie(String apiKeyCookie) { + this.apiKeyCookie = apiKeyCookie; + } + + @Override + public SendMessageAction doBuild() { + queryParameter("api_key_query", getOrDefault(apiKeyQuery, defaultApiKeyQuery, base64EncodeApiKey)); + headerParameter("api_key_header", getOrDefault(apiKeyHeader, defaultApiKeyHeader, base64EncodeApiKey)); + cookieParameter("api_key_cookie", getOrDefault(apiKeyCookie, defaultApiKeyCookie, base64EncodeApiKey)); + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetByIdWithApiKeyAuthenticationReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/secure-api-key/pet/{petId}"; + + private static final String OPERATION_NAME = "getPetByIdWithApiKeyAuthentication"; + + public GetPetByIdWithApiKeyAuthenticationReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetByIdWithApiKeyAuthenticationReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetByIdWithBasicAuthenticationSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/secure-basic/pet/{petId}"; + + private static final String OPERATION_NAME = "getPetByIdWithBasicAuthentication"; + + @Value("${" + "extpetstore.basic.username:#{null}}") + private String defaultBasicUsername; + + private String basicUsername; + + @Value("${" + "extpetstore.basic.password:#{null}}") + private String defaultBasicPassword; + + private String basicPassword; + + /** + * Constructor with type safe required parameters. + */ + public GetPetByIdWithBasicAuthenticationSendActionBuilder(ExtPetApi extPetApi, Long petId, Boolean allDetails) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + queryParameter("allDetails", allDetails, ParameterStyle.FORM, true, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetByIdWithBasicAuthenticationSendActionBuilder(String petIdExpression, String allDetailsExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + queryParameter("allDetails", allDetailsExpression, ParameterStyle.FORM, true, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetByIdWithBasicAuthenticationSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression, String allDetailsExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + queryParameter("allDetails", allDetailsExpression, ParameterStyle.FORM, true, false); + } + + public GetPetByIdWithBasicAuthenticationSendActionBuilder petId(Long petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetByIdWithBasicAuthenticationSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetByIdWithBasicAuthenticationSendActionBuilder allDetails(Boolean allDetails) { + queryParameter("allDetails", allDetails, ParameterStyle.FORM, true, false); + return this; + } + + public GetPetByIdWithBasicAuthenticationSendActionBuilder allDetails(String allDetailsExpression) { + queryParameter("allDetails", allDetailsExpression, ParameterStyle.FORM, true, false); + return this; + } + + public GetPetByIdWithBasicAuthenticationSendActionBuilder details(String...details) { + queryParameter("details", details, ParameterStyle.FORM, true, false); + return this; + } + + public void setDetails(String...details) { + queryParameter("details", details, ParameterStyle.FORM, true, false); + } + + public GetPetByIdWithBasicAuthenticationSendActionBuilder requesterInformation(String...requesterInformation) { + queryParameter("requesterInformation", requesterInformation, ParameterStyle.FORM, true, false); + return this; + } + + public void setRequesterInformation(String...requesterInformation) { + queryParameter("requesterInformation", requesterInformation, ParameterStyle.FORM, true, false); + } + + public GetPetByIdWithBasicAuthenticationSendActionBuilder basicAuthUsername(String basicUsername) { + this.basicUsername = basicUsername; + return this; + } + + public void setBasicAuthUsername(String basicUsername) { + this.basicUsername = basicUsername; + } + + public GetPetByIdWithBasicAuthenticationSendActionBuilder basicAuthPassword(String password) { + this.basicPassword = password; + return this; + } + + public void setBasicAuthPassword(String password) { + this.basicPassword = password; + } + + protected void addBasicAuthHeader(String basicUsername, String basicPassword, + HttpMessageBuilderSupport messageBuilderSupport) { + TestApiUtils.addBasicAuthHeader( + isNotEmpty(basicUsername) ? basicUsername : defaultBasicUsername, + isNotEmpty(basicPassword) ? basicPassword : defaultBasicPassword, + messageBuilderSupport); + } + + @Override + public SendMessageAction doBuild() { + addBasicAuthHeader(basicUsername, basicPassword, getMessageBuilderSupport()); + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetByIdWithBasicAuthenticationReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/secure-basic/pet/{petId}"; + + private static final String OPERATION_NAME = "getPetByIdWithBasicAuthentication"; + + public GetPetByIdWithBasicAuthenticationReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetByIdWithBasicAuthenticationReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetByIdWithBearerAuthenticationSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/secure-bearer/pet/{petId}"; + + private static final String OPERATION_NAME = "getPetByIdWithBearerAuthentication"; + + @Value("${" + "extpetstore.bearer.token:#{null}}") + private String defaultBasicAuthBearer; + + private String basicAuthBearer; + + /** + * Constructor with type safe required parameters. + */ + public GetPetByIdWithBearerAuthenticationSendActionBuilder(ExtPetApi extPetApi, Long petId, Boolean allDetails) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + queryParameter("allDetails", allDetails, ParameterStyle.FORM, true, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetByIdWithBearerAuthenticationSendActionBuilder(String petIdExpression, String allDetailsExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + queryParameter("allDetails", allDetailsExpression, ParameterStyle.FORM, true, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetByIdWithBearerAuthenticationSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression, String allDetailsExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + queryParameter("allDetails", allDetailsExpression, ParameterStyle.FORM, true, false); + } + + public GetPetByIdWithBearerAuthenticationSendActionBuilder petId(Long petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetByIdWithBearerAuthenticationSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetByIdWithBearerAuthenticationSendActionBuilder allDetails(Boolean allDetails) { + queryParameter("allDetails", allDetails, ParameterStyle.FORM, true, false); + return this; + } + + public GetPetByIdWithBearerAuthenticationSendActionBuilder allDetails(String allDetailsExpression) { + queryParameter("allDetails", allDetailsExpression, ParameterStyle.FORM, true, false); + return this; + } + + public GetPetByIdWithBearerAuthenticationSendActionBuilder details(String...details) { + queryParameter("details", details, ParameterStyle.FORM, true, false); + return this; + } + + public void setDetails(String...details) { + queryParameter("details", details, ParameterStyle.FORM, true, false); + } + + public GetPetByIdWithBearerAuthenticationSendActionBuilder requesterInformation(String...requesterInformation) { + queryParameter("requesterInformation", requesterInformation, ParameterStyle.FORM, true, false); + return this; + } + + public void setRequesterInformation(String...requesterInformation) { + queryParameter("requesterInformation", requesterInformation, ParameterStyle.FORM, true, false); + } + + public GetPetByIdWithBearerAuthenticationSendActionBuilder basicAuthBearer(String basicAuthBearer) { + this.basicAuthBearer = basicAuthBearer; + return this; + } + + public void setBasicAuthBearer(String basicAuthBearer) { + this.basicAuthBearer = basicAuthBearer; + } + + @Override + public SendMessageAction doBuild() { + if (!isEmpty(basicAuthBearer) || !isEmpty(defaultBasicAuthBearer)) { + headerParameter("Authorization", "Bearer " +getOrDefault(basicAuthBearer, defaultBasicAuthBearer, true)); + } + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetByIdWithBearerAuthenticationReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/secure-bearer/pet/{petId}"; + + private static final String OPERATION_NAME = "getPetByIdWithBearerAuthentication"; + + public GetPetByIdWithBearerAuthenticationReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetByIdWithBearerAuthenticationReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetByUuidSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/simple/object/uuid/{petUuid}"; + + private static final String OPERATION_NAME = "getPetByUuid"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetByUuidSendActionBuilder(ExtPetApi extPetApi, UUID petUuid) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petUuid", petUuid, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetByUuidSendActionBuilder(String petUuidExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petUuid", petUuidExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetByUuidSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petUuidExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petUuid", petUuidExpression, ParameterStyle.SIMPLE, false, false); + } + + public GetPetByUuidSendActionBuilder petUuid(UUID petUuid) { + pathParameter("petUuid", petUuid, ParameterStyle.SIMPLE, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetByUuidReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/simple/object/uuid/{petUuid}"; + + private static final String OPERATION_NAME = "getPetByUuid"; + + public GetPetByUuidReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetByUuidReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithCookieSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/{petId}"; + + private static final String OPERATION_NAME = "getPetWithCookie"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithCookieSendActionBuilder(ExtPetApi extPetApi, Long petId, String sessionId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + cookieParameter("session_id", sessionId, ParameterStyle.FORM, true, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithCookieSendActionBuilder(String petIdExpression, String sessionIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + cookieParameter("session_id", sessionIdExpression, ParameterStyle.FORM, true, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithCookieSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression, String sessionIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + cookieParameter("session_id", sessionIdExpression, ParameterStyle.FORM, true, false); + } + + public GetPetWithCookieSendActionBuilder petId(Long petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetWithCookieSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetWithCookieSendActionBuilder sessionId(String sessionId) { + cookieParameter("session_id", sessionId, ParameterStyle.FORM, true, false); + return this; + } + + public GetPetWithCookieSendActionBuilder optTrxId(String optTrxId) { + cookieParameter("opt_trx_id", optTrxId, ParameterStyle.FORM, true, false); + return this; + } + + public void setOptTrxId(String optTrxId) { + cookieParameter("opt_trx_id", optTrxId, ParameterStyle.FORM, true, false); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithCookieReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/{petId}"; + + private static final String OPERATION_NAME = "getPetWithCookie"; + + public GetPetWithCookieReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithCookieReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithDeepObjectTypeQuerySendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/query/deep/object"; + + private static final String OPERATION_NAME = "getPetWithDeepObjectTypeQuery"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithDeepObjectTypeQuerySendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petId, ParameterStyle.DEEPOBJECT, true, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithDeepObjectTypeQuerySendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petIdExpression, ParameterStyle.DEEPOBJECT, true, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithDeepObjectTypeQuerySendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petIdExpression, ParameterStyle.DEEPOBJECT, true, true); + } + + public GetPetWithDeepObjectTypeQuerySendActionBuilder petId(PetIdentifier petId) { + queryParameter("petId", petId, ParameterStyle.DEEPOBJECT, true, true); + return this; + } + + public GetPetWithDeepObjectTypeQuerySendActionBuilder petId(String petIdExpression) { + queryParameter("petId", petIdExpression, ParameterStyle.DEEPOBJECT, true, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithDeepObjectTypeQueryReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/query/deep/object"; + + private static final String OPERATION_NAME = "getPetWithDeepObjectTypeQuery"; + + public GetPetWithDeepObjectTypeQueryReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithDeepObjectTypeQueryReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithFormExplodedStyleCookieSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/cookie/form/exploded"; + + private static final String OPERATION_NAME = "getPetWithFormExplodedStyleCookie"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithFormExplodedStyleCookieSendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + cookieParameter("petId", petId, ParameterStyle.FORM, true, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormExplodedStyleCookieSendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + cookieParameter("petId", petIdExpression, ParameterStyle.FORM, true, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormExplodedStyleCookieSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + cookieParameter("petId", petId, ParameterStyle.FORM, true, false); + } + + public GetPetWithFormExplodedStyleCookieSendActionBuilder petId(Integer...petId) { + cookieParameter("petId", petId, ParameterStyle.FORM, true, false); + return this; + } + + public GetPetWithFormExplodedStyleCookieSendActionBuilder petId(String...petIdExpression) { + cookieParameter("petId", petIdExpression, ParameterStyle.FORM, true, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithFormExplodedStyleCookieReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/cookie/form/exploded"; + + private static final String OPERATION_NAME = "getPetWithFormExplodedStyleCookie"; + + public GetPetWithFormExplodedStyleCookieReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithFormExplodedStyleCookieReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithFormObjectStyleCookieSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/cookie/form/object"; + + private static final String OPERATION_NAME = "getPetWithFormObjectStyleCookie"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithFormObjectStyleCookieSendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + cookieParameter("petId", petId, ParameterStyle.FORM, false, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormObjectStyleCookieSendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + cookieParameter("petId", petIdExpression, ParameterStyle.FORM, false, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormObjectStyleCookieSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + cookieParameter("petId", petIdExpression, ParameterStyle.FORM, false, true); + } + + public GetPetWithFormObjectStyleCookieSendActionBuilder petId(PetIdentifier petId) { + cookieParameter("petId", petId, ParameterStyle.FORM, false, true); + return this; + } + + public GetPetWithFormObjectStyleCookieSendActionBuilder petId(String petIdExpression) { + cookieParameter("petId", petIdExpression, ParameterStyle.FORM, false, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithFormObjectStyleCookieReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/cookie/form/object"; + + private static final String OPERATION_NAME = "getPetWithFormObjectStyleCookie"; + + public GetPetWithFormObjectStyleCookieReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithFormObjectStyleCookieReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithFormStyleCookieSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/cookie/form"; + + private static final String OPERATION_NAME = "getPetWithFormStyleCookie"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithFormStyleCookieSendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + cookieParameter("petId", petId, ParameterStyle.FORM, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormStyleCookieSendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + cookieParameter("petId", petIdExpression, ParameterStyle.FORM, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormStyleCookieSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + cookieParameter("petId", petId, ParameterStyle.FORM, false, false); + } + + public GetPetWithFormStyleCookieSendActionBuilder petId(Integer...petId) { + cookieParameter("petId", petId, ParameterStyle.FORM, false, false); + return this; + } + + public GetPetWithFormStyleCookieSendActionBuilder petId(String...petIdExpression) { + cookieParameter("petId", petIdExpression, ParameterStyle.FORM, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithFormStyleCookieReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/cookie/form"; + + private static final String OPERATION_NAME = "getPetWithFormStyleCookie"; + + public GetPetWithFormStyleCookieReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithFormStyleCookieReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithFormStyleExplodedObjectQuerySendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/query/form/exploded/object"; + + private static final String OPERATION_NAME = "getPetWithFormStyleExplodedObjectQuery"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithFormStyleExplodedObjectQuerySendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petId, ParameterStyle.FORM, true, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormStyleExplodedObjectQuerySendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petIdExpression, ParameterStyle.FORM, true, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormStyleExplodedObjectQuerySendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petIdExpression, ParameterStyle.FORM, true, true); + } + + public GetPetWithFormStyleExplodedObjectQuerySendActionBuilder petId(PetIdentifier petId) { + queryParameter("petId", petId, ParameterStyle.FORM, true, true); + return this; + } + + public GetPetWithFormStyleExplodedObjectQuerySendActionBuilder petId(String petIdExpression) { + queryParameter("petId", petIdExpression, ParameterStyle.FORM, true, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithFormStyleExplodedObjectQueryReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/query/form/exploded/object"; + + private static final String OPERATION_NAME = "getPetWithFormStyleExplodedObjectQuery"; + + public GetPetWithFormStyleExplodedObjectQueryReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithFormStyleExplodedObjectQueryReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithFormStyleExplodedQuerySendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/query/form/exploded"; + + private static final String OPERATION_NAME = "getPetWithFormStyleExplodedQuery"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithFormStyleExplodedQuerySendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petId, ParameterStyle.FORM, true, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormStyleExplodedQuerySendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petIdExpression, ParameterStyle.FORM, true, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormStyleExplodedQuerySendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petId, ParameterStyle.FORM, true, false); + } + + public GetPetWithFormStyleExplodedQuerySendActionBuilder petId(Integer...petId) { + queryParameter("petId", petId, ParameterStyle.FORM, true, false); + return this; + } + + public GetPetWithFormStyleExplodedQuerySendActionBuilder petId(String...petIdExpression) { + queryParameter("petId", petIdExpression, ParameterStyle.FORM, true, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithFormStyleExplodedQueryReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/query/form/exploded"; + + private static final String OPERATION_NAME = "getPetWithFormStyleExplodedQuery"; + + public GetPetWithFormStyleExplodedQueryReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithFormStyleExplodedQueryReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithFormStyleObjectQuerySendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/query/form/object"; + + private static final String OPERATION_NAME = "getPetWithFormStyleObjectQuery"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithFormStyleObjectQuerySendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petId, ParameterStyle.FORM, false, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormStyleObjectQuerySendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petIdExpression, ParameterStyle.FORM, false, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormStyleObjectQuerySendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petIdExpression, ParameterStyle.FORM, false, true); + } + + public GetPetWithFormStyleObjectQuerySendActionBuilder petId(PetIdentifier petId) { + queryParameter("petId", petId, ParameterStyle.FORM, false, true); + return this; + } + + public GetPetWithFormStyleObjectQuerySendActionBuilder petId(String petIdExpression) { + queryParameter("petId", petIdExpression, ParameterStyle.FORM, false, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithFormStyleObjectQueryReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/query/form/object"; + + private static final String OPERATION_NAME = "getPetWithFormStyleObjectQuery"; + + public GetPetWithFormStyleObjectQueryReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithFormStyleObjectQueryReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithFormStyleQuerySendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/query/form"; + + private static final String OPERATION_NAME = "getPetWithFormStyleQuery"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithFormStyleQuerySendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petId, ParameterStyle.FORM, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormStyleQuerySendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petIdExpression, ParameterStyle.FORM, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithFormStyleQuerySendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + queryParameter("petId", petId, ParameterStyle.FORM, false, false); + } + + public GetPetWithFormStyleQuerySendActionBuilder petId(Integer...petId) { + queryParameter("petId", petId, ParameterStyle.FORM, false, false); + return this; + } + + public GetPetWithFormStyleQuerySendActionBuilder petId(String...petIdExpression) { + queryParameter("petId", petIdExpression, ParameterStyle.FORM, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithFormStyleQueryReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/query/form"; + + private static final String OPERATION_NAME = "getPetWithFormStyleQuery"; + + public GetPetWithFormStyleQueryReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithFormStyleQueryReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithLabelStyleArraySendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/label/{petId}"; + + private static final String OPERATION_NAME = "getPetWithLabelStyleArray"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithLabelStyleArraySendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.LABEL, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithLabelStyleArraySendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.LABEL, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithLabelStyleArraySendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.LABEL, false, false); + } + + public GetPetWithLabelStyleArraySendActionBuilder petId(Integer...petId) { + pathParameter("petId", petId, ParameterStyle.LABEL, false, false); + return this; + } + + public GetPetWithLabelStyleArraySendActionBuilder petId(String...petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.LABEL, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithLabelStyleArrayReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/label/{petId}"; + + private static final String OPERATION_NAME = "getPetWithLabelStyleArray"; + + public GetPetWithLabelStyleArrayReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithLabelStyleArrayReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithLabelStyleArrayExplodedSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/label/exploded/{petId}"; + + private static final String OPERATION_NAME = "getPetWithLabelStyleArrayExploded"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithLabelStyleArrayExplodedSendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.LABEL, true, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithLabelStyleArrayExplodedSendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.LABEL, true, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithLabelStyleArrayExplodedSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.LABEL, true, false); + } + + public GetPetWithLabelStyleArrayExplodedSendActionBuilder petId(Integer...petId) { + pathParameter("petId", petId, ParameterStyle.LABEL, true, false); + return this; + } + + public GetPetWithLabelStyleArrayExplodedSendActionBuilder petId(String...petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.LABEL, true, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithLabelStyleArrayExplodedReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/label/exploded/{petId}"; + + private static final String OPERATION_NAME = "getPetWithLabelStyleArrayExploded"; + + public GetPetWithLabelStyleArrayExplodedReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithLabelStyleArrayExplodedReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithLabelStyleObjectSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/label/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithLabelStyleObject"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithLabelStyleObjectSendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.LABEL, false, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithLabelStyleObjectSendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.LABEL, false, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithLabelStyleObjectSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.LABEL, false, true); + } + + public GetPetWithLabelStyleObjectSendActionBuilder petId(PetIdentifier petId) { + pathParameter("petId", petId, ParameterStyle.LABEL, false, true); + return this; + } + + public GetPetWithLabelStyleObjectSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.LABEL, false, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithLabelStyleObjectReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/label/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithLabelStyleObject"; + + public GetPetWithLabelStyleObjectReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithLabelStyleObjectReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithLabelStyleObjectExplodedSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/label/exploded/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithLabelStyleObjectExploded"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithLabelStyleObjectExplodedSendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.LABEL, true, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithLabelStyleObjectExplodedSendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.LABEL, true, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithLabelStyleObjectExplodedSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.LABEL, true, true); + } + + public GetPetWithLabelStyleObjectExplodedSendActionBuilder petId(PetIdentifier petId) { + pathParameter("petId", petId, ParameterStyle.LABEL, true, true); + return this; + } + + public GetPetWithLabelStyleObjectExplodedSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.LABEL, true, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithLabelStyleObjectExplodedReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/label/exploded/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithLabelStyleObjectExploded"; + + public GetPetWithLabelStyleObjectExplodedReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithLabelStyleObjectExplodedReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithMatrixStyleArraySendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/matrix/{petId}"; + + private static final String OPERATION_NAME = "getPetWithMatrixStyleArray"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithMatrixStyleArraySendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.MATRIX, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithMatrixStyleArraySendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.MATRIX, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithMatrixStyleArraySendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.MATRIX, false, false); + } + + public GetPetWithMatrixStyleArraySendActionBuilder petId(Integer...petId) { + pathParameter("petId", petId, ParameterStyle.MATRIX, false, false); + return this; + } + + public GetPetWithMatrixStyleArraySendActionBuilder petId(String...petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.MATRIX, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithMatrixStyleArrayReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/matrix/{petId}"; + + private static final String OPERATION_NAME = "getPetWithMatrixStyleArray"; + + public GetPetWithMatrixStyleArrayReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithMatrixStyleArrayReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithMatrixStyleArrayExplodedSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/matrix/exploded/{petId}"; + + private static final String OPERATION_NAME = "getPetWithMatrixStyleArrayExploded"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithMatrixStyleArrayExplodedSendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.MATRIX, true, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithMatrixStyleArrayExplodedSendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.MATRIX, true, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithMatrixStyleArrayExplodedSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.MATRIX, true, false); + } + + public GetPetWithMatrixStyleArrayExplodedSendActionBuilder petId(Integer...petId) { + pathParameter("petId", petId, ParameterStyle.MATRIX, true, false); + return this; + } + + public GetPetWithMatrixStyleArrayExplodedSendActionBuilder petId(String...petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.MATRIX, true, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithMatrixStyleArrayExplodedReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/matrix/exploded/{petId}"; + + private static final String OPERATION_NAME = "getPetWithMatrixStyleArrayExploded"; + + public GetPetWithMatrixStyleArrayExplodedReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithMatrixStyleArrayExplodedReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithMatrixStyleObjectSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/matrix/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithMatrixStyleObject"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithMatrixStyleObjectSendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.MATRIX, false, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithMatrixStyleObjectSendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.MATRIX, false, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithMatrixStyleObjectSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.MATRIX, false, true); + } + + public GetPetWithMatrixStyleObjectSendActionBuilder petId(PetIdentifier petId) { + pathParameter("petId", petId, ParameterStyle.MATRIX, false, true); + return this; + } + + public GetPetWithMatrixStyleObjectSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.MATRIX, false, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithMatrixStyleObjectReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/matrix/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithMatrixStyleObject"; + + public GetPetWithMatrixStyleObjectReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithMatrixStyleObjectReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithMatrixStyleObjectExplodedSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/matrix/exploded/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithMatrixStyleObjectExploded"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithMatrixStyleObjectExplodedSendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.MATRIX, true, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithMatrixStyleObjectExplodedSendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.MATRIX, true, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithMatrixStyleObjectExplodedSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.MATRIX, true, true); + } + + public GetPetWithMatrixStyleObjectExplodedSendActionBuilder petId(PetIdentifier petId) { + pathParameter("petId", petId, ParameterStyle.MATRIX, true, true); + return this; + } + + public GetPetWithMatrixStyleObjectExplodedSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.MATRIX, true, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithMatrixStyleObjectExplodedReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/matrix/exploded/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithMatrixStyleObjectExploded"; + + public GetPetWithMatrixStyleObjectExplodedReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithMatrixStyleObjectExplodedReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithParametersRequiringEncodingSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/parameter-with-url-encoding-required/{Pet ID}"; + + private static final String OPERATION_NAME = "getPetWithParametersRequiringEncoding"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithParametersRequiringEncodingSendActionBuilder(ExtPetApi extPetApi, Integer petID) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("Pet ID", petID, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithParametersRequiringEncodingSendActionBuilder(String petIDExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("Pet ID", petIDExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithParametersRequiringEncodingSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIDExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("Pet ID", petIDExpression, ParameterStyle.SIMPLE, false, false); + } + + public GetPetWithParametersRequiringEncodingSendActionBuilder petID(Integer petID) { + pathParameter("Pet ID", petID, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetWithParametersRequiringEncodingSendActionBuilder petID(String petIDExpression) { + pathParameter("Pet ID", petIDExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetWithParametersRequiringEncodingSendActionBuilder queryID(Integer queryID) { + queryParameter("Query ID", queryID, ParameterStyle.FORM, true, false); + return this; + } + + public void setQueryID(Integer queryID) { + queryParameter("Query ID", queryID, ParameterStyle.FORM, true, false); + } + + public GetPetWithParametersRequiringEncodingSendActionBuilder queryID(String queryIDExpression) { + queryParameter("Query ID", queryIDExpression, ParameterStyle.FORM, true, false); + return this; + } + + public void setQueryID(String queryIDExpression) { + queryParameter("Query ID", queryIDExpression, ParameterStyle.FORM, true, false); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithParametersRequiringEncodingReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/parameter-with-url-encoding-required/{Pet ID}"; + + private static final String OPERATION_NAME = "getPetWithParametersRequiringEncoding"; + + public GetPetWithParametersRequiringEncodingReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithParametersRequiringEncodingReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithSimpleStyleArraySendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/simple/{petId}"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleArray"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithSimpleStyleArraySendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleArraySendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleArraySendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + } + + public GetPetWithSimpleStyleArraySendActionBuilder petId(Integer...petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetWithSimpleStyleArraySendActionBuilder petId(String...petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithSimpleStyleArrayReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/simple/{petId}"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleArray"; + + public GetPetWithSimpleStyleArrayReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithSimpleStyleArrayReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithSimpleStyleArrayExplodedSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/simple/exploded/{petId}"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleArrayExploded"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithSimpleStyleArrayExplodedSendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleArrayExplodedSendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleArrayExplodedSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + } + + public GetPetWithSimpleStyleArrayExplodedSendActionBuilder petId(Integer...petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetWithSimpleStyleArrayExplodedSendActionBuilder petId(String...petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithSimpleStyleArrayExplodedReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/simple/exploded/{petId}"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleArrayExploded"; + + public GetPetWithSimpleStyleArrayExplodedReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithSimpleStyleArrayExplodedReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithSimpleStyleExplodedHeaderSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/header/simple/exploded"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleExplodedHeader"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithSimpleStyleExplodedHeaderSendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petId, ParameterStyle.SIMPLE, true, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleExplodedHeaderSendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petIdExpression, ParameterStyle.SIMPLE, true, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleExplodedHeaderSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petId, ParameterStyle.SIMPLE, true, false); + } + + public GetPetWithSimpleStyleExplodedHeaderSendActionBuilder petId(Integer...petId) { + headerParameter("petId", petId, ParameterStyle.SIMPLE, true, false); + return this; + } + + public GetPetWithSimpleStyleExplodedHeaderSendActionBuilder petId(String...petIdExpression) { + headerParameter("petId", petIdExpression, ParameterStyle.SIMPLE, true, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithSimpleStyleExplodedHeaderReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/header/simple/exploded"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleExplodedHeader"; + + public GetPetWithSimpleStyleExplodedHeaderReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithSimpleStyleExplodedHeaderReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithSimpleStyleExplodedObjectHeaderSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/header/simple/exploded/object"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleExplodedObjectHeader"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithSimpleStyleExplodedObjectHeaderSendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petId, ParameterStyle.SIMPLE, true, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleExplodedObjectHeaderSendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petIdExpression, ParameterStyle.SIMPLE, true, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleExplodedObjectHeaderSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petIdExpression, ParameterStyle.SIMPLE, true, true); + } + + public GetPetWithSimpleStyleExplodedObjectHeaderSendActionBuilder petId(PetIdentifier petId) { + headerParameter("petId", petId, ParameterStyle.SIMPLE, true, true); + return this; + } + + public GetPetWithSimpleStyleExplodedObjectHeaderSendActionBuilder petId(String petIdExpression) { + headerParameter("petId", petIdExpression, ParameterStyle.SIMPLE, true, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithSimpleStyleExplodedObjectHeaderReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/header/simple/exploded/object"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleExplodedObjectHeader"; + + public GetPetWithSimpleStyleExplodedObjectHeaderReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithSimpleStyleExplodedObjectHeaderReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithSimpleStyleHeaderSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/header/simple"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleHeader"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithSimpleStyleHeaderSendActionBuilder(ExtPetApi extPetApi, List petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleHeaderSendActionBuilder(List petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleHeaderSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, List petId) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + } + + public GetPetWithSimpleStyleHeaderSendActionBuilder petId(Integer...petId) { + headerParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetWithSimpleStyleHeaderSendActionBuilder petId(String...petIdExpression) { + headerParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithSimpleStyleHeaderReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/header/simple"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleHeader"; + + public GetPetWithSimpleStyleHeaderReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithSimpleStyleHeaderReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithSimpleStyleObjectSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/simple/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleObject"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithSimpleStyleObjectSendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleObjectSendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleObjectSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, true); + } + + public GetPetWithSimpleStyleObjectSendActionBuilder petId(PetIdentifier petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, true); + return this; + } + + public GetPetWithSimpleStyleObjectSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithSimpleStyleObjectReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/simple/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleObject"; + + public GetPetWithSimpleStyleObjectReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithSimpleStyleObjectReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithSimpleStyleObjectExplodedSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/simple/exploded/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleObjectExploded"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithSimpleStyleObjectExplodedSendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, true, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleObjectExplodedSendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, true, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleObjectExplodedSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, true, true); + } + + public GetPetWithSimpleStyleObjectExplodedSendActionBuilder petId(PetIdentifier petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, true, true); + return this; + } + + public GetPetWithSimpleStyleObjectExplodedSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, true, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithSimpleStyleObjectExplodedReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/simple/exploded/object/{petId}"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleObjectExploded"; + + public GetPetWithSimpleStyleObjectExplodedReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithSimpleStyleObjectExplodedReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetWithSimpleStyleObjectHeaderSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/header/simple/object"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleObjectHeader"; + + /** + * Constructor with type safe required parameters. + */ + public GetPetWithSimpleStyleObjectHeaderSendActionBuilder(ExtPetApi extPetApi, PetIdentifier petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petId, ParameterStyle.SIMPLE, false, true); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleObjectHeaderSendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, true); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetWithSimpleStyleObjectHeaderSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + headerParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, true); + } + + public GetPetWithSimpleStyleObjectHeaderSendActionBuilder petId(PetIdentifier petId) { + headerParameter("petId", petId, ParameterStyle.SIMPLE, false, true); + return this; + } + + public GetPetWithSimpleStyleObjectHeaderSendActionBuilder petId(String petIdExpression) { + headerParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, true); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetWithSimpleStyleObjectHeaderReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/header/simple/object"; + + private static final String OPERATION_NAME = "getPetWithSimpleStyleObjectHeader"; + + public GetPetWithSimpleStyleObjectHeaderReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetWithSimpleStyleObjectHeaderReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class PetWithoutOperationIdPetIdGetSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/without-operation-id/{petId}"; + + private static final String OPERATION_NAME = "GET_/api/v3/ext/pet/without-operation-id/{petId}"; + + /** + * Constructor with type safe required parameters. + */ + public PetWithoutOperationIdPetIdGetSendActionBuilder(ExtPetApi extPetApi, Integer petId) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public PetWithoutOperationIdPetIdGetSendActionBuilder(String petIdExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public PetWithoutOperationIdPetIdGetSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + public PetWithoutOperationIdPetIdGetSendActionBuilder petId(Integer petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public PetWithoutOperationIdPetIdGetSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class PetWithoutOperationIdPetIdGetReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/ext/pet/without-operation-id/{petId}"; + + private static final String OPERATION_NAME = "GET_/api/v3/ext/pet/without-operation-id/{petId}"; + + public PetWithoutOperationIdPetIdGetReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public PetWithoutOperationIdPetIdGetReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class PostVaccinationDocumentSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/ext/pet/vaccination/{bucket}/{filename}"; + + private static final String OPERATION_NAME = "postVaccinationDocument"; + + /** + * Constructor with type safe required parameters. + */ + public PostVaccinationDocumentSendActionBuilder(ExtPetApi extPetApi, String bucket, String filename) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("bucket", bucket, ParameterStyle.SIMPLE, false, false); + pathParameter("filename", filename, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public PostVaccinationDocumentSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String bucketExpression, String filenameExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("bucket", bucketExpression, ParameterStyle.SIMPLE, false, false); + pathParameter("filename", filenameExpression, ParameterStyle.SIMPLE, false, false); + } + + public PostVaccinationDocumentSendActionBuilder bucket(String bucket) { + pathParameter("bucket", bucket, ParameterStyle.SIMPLE, false, false); + return this; + } + + public PostVaccinationDocumentSendActionBuilder filename(String filename) { + pathParameter("filename", filename, ParameterStyle.SIMPLE, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class PostVaccinationDocumentReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/ext/pet/vaccination/{bucket}/{filename}"; + + private static final String OPERATION_NAME = "postVaccinationDocument"; + + public PostVaccinationDocumentReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public PostVaccinationDocumentReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class PostVaccinationFormDataSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/ext/pet/vaccination/form"; + + private static final String OPERATION_NAME = "postVaccinationFormData"; + + /** + * Constructor with type safe required parameters. + */ + public PostVaccinationFormDataSendActionBuilder(ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + public PostVaccinationFormDataSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + public PostVaccinationFormDataSendActionBuilder vaccine(String vaccine) { + formParameter("vaccine", vaccine); + return this; + } + + public void setVaccine(String vaccine) { + formParameter("vaccine", vaccine); + } + + public PostVaccinationFormDataSendActionBuilder isFirstVaccination(Boolean isFirstVaccination) { + formParameter("isFirstVaccination", isFirstVaccination); + return this; + } + + public void setIsFirstVaccination(Boolean isFirstVaccination) { + formParameter("isFirstVaccination", isFirstVaccination); + } + + public PostVaccinationFormDataSendActionBuilder isFirstVaccination(String isFirstVaccinationExpression) { + formParameter("isFirstVaccination", isFirstVaccinationExpression); + return this; + } + + public void setIsFirstVaccination(String isFirstVaccinationExpression) { + formParameter("isFirstVaccination", isFirstVaccinationExpression); + } + + public PostVaccinationFormDataSendActionBuilder doseNumber(Integer doseNumber) { + formParameter("doseNumber", doseNumber); + return this; + } + + public void setDoseNumber(Integer doseNumber) { + formParameter("doseNumber", doseNumber); + } + + public PostVaccinationFormDataSendActionBuilder doseNumber(String doseNumberExpression) { + formParameter("doseNumber", doseNumberExpression); + return this; + } + + public void setDoseNumber(String doseNumberExpression) { + formParameter("doseNumber", doseNumberExpression); + } + + public PostVaccinationFormDataSendActionBuilder vaccinationDate(LocalDate vaccinationDate) { + formParameter("vaccinationDate", vaccinationDate); + return this; + } + + public void setVaccinationDate(LocalDate vaccinationDate) { + formParameter("vaccinationDate", vaccinationDate); + } + + public PostVaccinationFormDataSendActionBuilder vaccinationDate(String vaccinationDateExpression) { + formParameter("vaccinationDate", vaccinationDateExpression); + return this; + } + + public void setVaccinationDate(String vaccinationDateExpression) { + formParameter("vaccinationDate", vaccinationDateExpression); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class PostVaccinationFormDataReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/ext/pet/vaccination/form"; + + private static final String OPERATION_NAME = "postVaccinationFormData"; + + public PostVaccinationFormDataReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public PostVaccinationFormDataReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class UpdatePetWithArrayQueryDataSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "PUT"; + + private static final String ENDPOINT = "/api/v3/ext/pet/{petId}"; + + private static final String OPERATION_NAME = "updatePetWithArrayQueryData"; + + /** + * Constructor with type safe required parameters. + */ + public UpdatePetWithArrayQueryDataSendActionBuilder(ExtPetApi extPetApi, Long petId, String _name, String status, List tags, List nicknames, String sampleStringHeader) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + queryParameter("name", _name, ParameterStyle.FORM, true, false); + queryParameter("status", status, ParameterStyle.FORM, true, false); + queryParameter("tags", tags, ParameterStyle.FORM, true, false); + queryParameter("nicknames", nicknames, ParameterStyle.FORM, true, false); + headerParameter("sampleStringHeader", sampleStringHeader, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public UpdatePetWithArrayQueryDataSendActionBuilder(String petIdExpression, String _nameExpression, String statusExpression, List tagsExpression, List nicknamesExpression, String sampleStringHeaderExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + queryParameter("name", _nameExpression, ParameterStyle.FORM, true, false); + queryParameter("status", statusExpression, ParameterStyle.FORM, true, false); + queryParameter("tags", tagsExpression, ParameterStyle.FORM, true, false); + queryParameter("nicknames", nicknamesExpression, ParameterStyle.FORM, true, false); + headerParameter("sampleStringHeader", sampleStringHeaderExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public UpdatePetWithArrayQueryDataSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression, String _nameExpression, String statusExpression, List tags, List nicknames, String sampleStringHeaderExpression) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + queryParameter("name", _nameExpression, ParameterStyle.FORM, true, false); + queryParameter("status", statusExpression, ParameterStyle.FORM, true, false); + queryParameter("tags", tags, ParameterStyle.FORM, true, false); + queryParameter("nicknames", nicknames, ParameterStyle.FORM, true, false); + headerParameter("sampleStringHeader", sampleStringHeaderExpression, ParameterStyle.SIMPLE, false, false); + } + + public UpdatePetWithArrayQueryDataSendActionBuilder petId(Long petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public UpdatePetWithArrayQueryDataSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public UpdatePetWithArrayQueryDataSendActionBuilder _name(String _name) { + queryParameter("name", _name, ParameterStyle.FORM, true, false); + return this; + } + + public UpdatePetWithArrayQueryDataSendActionBuilder status(String status) { + queryParameter("status", status, ParameterStyle.FORM, true, false); + return this; + } + + public UpdatePetWithArrayQueryDataSendActionBuilder tags(String...tags) { + queryParameter("tags", tags, ParameterStyle.FORM, true, false); + return this; + } + + public UpdatePetWithArrayQueryDataSendActionBuilder nicknames(String...nicknames) { + queryParameter("nicknames", nicknames, ParameterStyle.FORM, true, false); + return this; + } + + public UpdatePetWithArrayQueryDataSendActionBuilder sampleStringHeader(String sampleStringHeader) { + headerParameter("sampleStringHeader", sampleStringHeader, ParameterStyle.SIMPLE, false, false); + return this; + } + + public UpdatePetWithArrayQueryDataSendActionBuilder sampleIntHeader(Integer sampleIntHeader) { + headerParameter("sampleIntHeader", sampleIntHeader, ParameterStyle.SIMPLE, false, false); + return this; + } + + public void setSampleIntHeader(Integer sampleIntHeader) { + headerParameter("sampleIntHeader", sampleIntHeader, ParameterStyle.SIMPLE, false, false); + } + + public UpdatePetWithArrayQueryDataSendActionBuilder sampleIntHeader(String sampleIntHeaderExpression) { + headerParameter("sampleIntHeader", sampleIntHeaderExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public void setSampleIntHeader(String sampleIntHeaderExpression) { + headerParameter("sampleIntHeader", sampleIntHeaderExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class UpdatePetWithArrayQueryDataReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "PUT"; + + private static final String ENDPOINT = "/api/v3/ext/pet/{petId}"; + + private static final String OPERATION_NAME = "updatePetWithArrayQueryData"; + + public UpdatePetWithArrayQueryDataReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public UpdatePetWithArrayQueryDataReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class UpdatePetWithFormUrlEncodedSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "PUT"; + + private static final String ENDPOINT = "/api/v3/ext/pet/form/{petId}"; + + private static final String OPERATION_NAME = "updatePetWithFormUrlEncoded"; + + /** + * Constructor with type safe required parameters. + */ + public UpdatePetWithFormUrlEncodedSendActionBuilder(ExtPetApi extPetApi, Long petId, String _name, String status, Integer age, List tags) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + formParameter("name", _name); + formParameter("status", status); + formParameter("age", age); + formParameter("tags", tags); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public UpdatePetWithFormUrlEncodedSendActionBuilder(String petIdExpression, String _nameExpression, String statusExpression, String ageExpression, List tagsExpression, ExtPetApi extPetApi) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + formParameter("name", _nameExpression); + formParameter("status", statusExpression); + formParameter("age", ageExpression); + formParameter("tags", tagsExpression); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public UpdatePetWithFormUrlEncodedSendActionBuilder(ExtPetApi extPetApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression, String _nameExpression, String statusExpression, String ageExpression, List tags) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + formParameter("name", _nameExpression); + formParameter("status", statusExpression); + formParameter("age", ageExpression); + formParameter("tags", tags); + } + + public UpdatePetWithFormUrlEncodedSendActionBuilder petId(Long petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public UpdatePetWithFormUrlEncodedSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public UpdatePetWithFormUrlEncodedSendActionBuilder _name(String _name) { + formParameter("name", _name); + return this; + } + + public UpdatePetWithFormUrlEncodedSendActionBuilder status(String status) { + formParameter("status", status); + return this; + } + + public UpdatePetWithFormUrlEncodedSendActionBuilder age(Integer age) { + formParameter("age", age); + return this; + } + + public UpdatePetWithFormUrlEncodedSendActionBuilder age(String ageExpression) { + formParameter("age", ageExpression); + return this; + } + + public UpdatePetWithFormUrlEncodedSendActionBuilder tags(String...tags) { + formParameter("tags", tags); + return this; + } + + public UpdatePetWithFormUrlEncodedSendActionBuilder owners(Integer owners) { + formParameter("owners", owners); + return this; + } + + public void setOwners(Integer owners) { + formParameter("owners", owners); + } + + public UpdatePetWithFormUrlEncodedSendActionBuilder owners(String ownersExpression) { + formParameter("owners", ownersExpression); + return this; + } + + public void setOwners(String ownersExpression) { + formParameter("owners", ownersExpression); + } + + public UpdatePetWithFormUrlEncodedSendActionBuilder nicknames(String...nicknames) { + formParameter("nicknames", nicknames); + return this; + } + + public void setNicknames(String...nicknames) { + formParameter("nicknames", nicknames); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class UpdatePetWithFormUrlEncodedReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "PUT"; + + private static final String ENDPOINT = "/api/v3/ext/pet/form/{petId}"; + + private static final String OPERATION_NAME = "updatePetWithFormUrlEncoded"; + + public UpdatePetWithFormUrlEncodedReceiveActionBuilder(ExtPetApi extPetApi, String statusCode) { + super(extPetApi, extPetStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public UpdatePetWithFormUrlEncodedReceiveActionBuilder(ExtPetApi extPetApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(extPetApi, extPetStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/spring/ExtPetStoreBeanConfiguration.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/spring/ExtPetStoreBeanConfiguration.java new file mode 100644 index 0000000000..0ca6dbfd28 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/spring/ExtPetStoreBeanConfiguration.java @@ -0,0 +1,33 @@ +package org.citrusframework.openapi.generator.rest.extpetstore.spring; + +import static org.citrusframework.openapi.generator.rest.extpetstore.ExtPetStoreOpenApi.extPetStoreSpecification; + +import java.util.List; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.OpenApiRepository; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +import org.citrusframework.openapi.generator.rest.extpetstore.request.ExtPetApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.citrusframework.openapi.generator.rest.extpetstore.ExtPetStoreOpenApi; + +@Configuration +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.524898+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class ExtPetStoreBeanConfiguration { + + @Bean + public OpenApiRepository extPetStoreOpenApiRepository() { + var openApiRepository = new OpenApiRepository(); + openApiRepository.getOpenApiSpecifications().add(extPetStoreSpecification); + return openApiRepository; + } + + @Bean(name="ExtPetApi") + public ExtPetApi extPetApi(@Autowired(required = false) @Qualifier("extpetstore.endpoint") Endpoint defaultEndpoint, @Autowired(required = false) List customizers) { + return new ExtPetApi(defaultEndpoint, customizers); + } + +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/spring/ExtPetStoreNamespaceHandler.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/spring/ExtPetStoreNamespaceHandler.java new file mode 100644 index 0000000000..d1147d9d86 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/extpetstore/spring/ExtPetStoreNamespaceHandler.java @@ -0,0 +1,258 @@ +package org.citrusframework.openapi.generator.rest.extpetstore.spring; + +import static org.citrusframework.openapi.generator.rest.extpetstore.ExtPetStoreOpenApi.extPetStoreSpecification; + +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.RestApiSendMessageActionBuilder; +import org.citrusframework.openapi.testapi.RestApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.generator.rest.extpetstore.request.ExtPetApi; +import org.citrusframework.openapi.testapi.spring.RestApiReceiveMessageActionParser; +import org.citrusframework.openapi.testapi.spring.RestApiSendMessageActionParser; +import org.citrusframework.openapi.generator.rest.extpetstore.ExtPetStoreOpenApi; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.524898+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class ExtPetStoreNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + + registerOperationParsers(ExtPetApi.class,"generate-vaccination-report", "generateVaccinationReport", "/pet/vaccination/status-report", + ExtPetApi.GenerateVaccinationReportSendActionBuilder.class, + ExtPetApi.GenerateVaccinationReportReceiveActionBuilder.class, + new String[]{ "template", "reqIntVal" }, + new String[]{ "optIntVal", "optBoolVal", "optNumberVal", "optStringVal", "optDateVal", "additionalData", "schema" }); + + registerOperationParsers(ExtPetApi.class,"get-pet-by-id-with-api-key-authentication", "getPetByIdWithApiKeyAuthentication", "/secure-api-key/pet/{petId}", + ExtPetApi.GetPetByIdWithApiKeyAuthenticationSendActionBuilder.class, + ExtPetApi.GetPetByIdWithApiKeyAuthenticationReceiveActionBuilder.class, + new String[]{ "petId", "allDetails" }, + new String[]{ "details", "requesterInformation", "apiKeyQuery", "apiKeyHeader", "apiKeyCookie" }); + + registerOperationParsers(ExtPetApi.class,"get-pet-by-id-with-basic-authentication", "getPetByIdWithBasicAuthentication", "/secure-basic/pet/{petId}", + ExtPetApi.GetPetByIdWithBasicAuthenticationSendActionBuilder.class, + ExtPetApi.GetPetByIdWithBasicAuthenticationReceiveActionBuilder.class, + new String[]{ "petId", "allDetails" }, + new String[]{ "details", "requesterInformation", "basicAuthUsername", "basicAuthPassword" }); + + registerOperationParsers(ExtPetApi.class,"get-pet-by-id-with-bearer-authentication", "getPetByIdWithBearerAuthentication", "/secure-bearer/pet/{petId}", + ExtPetApi.GetPetByIdWithBearerAuthenticationSendActionBuilder.class, + ExtPetApi.GetPetByIdWithBearerAuthenticationReceiveActionBuilder.class, + new String[]{ "petId", "allDetails" }, + new String[]{ "details", "requesterInformation", "basicAuthBearer" }); + + registerOperationParsers(ExtPetApi.class,"get-pet-by-uuid", "getPetByUuid", "/pet/simple/object/uuid/{petUuid}", + ExtPetApi.GetPetByUuidSendActionBuilder.class, + ExtPetApi.GetPetByUuidReceiveActionBuilder.class, + new String[]{ "petUuid" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-cookie", "getPetWithCookie", "/pet/{petId}", + ExtPetApi.GetPetWithCookieSendActionBuilder.class, + ExtPetApi.GetPetWithCookieReceiveActionBuilder.class, + new String[]{ "petId", "sessionId" }, + new String[]{ "optTrxId" }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-deep-object-type-query", "getPetWithDeepObjectTypeQuery", "/pet/query/deep/object", + ExtPetApi.GetPetWithDeepObjectTypeQuerySendActionBuilder.class, + ExtPetApi.GetPetWithDeepObjectTypeQueryReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-form-exploded-style-cookie", "getPetWithFormExplodedStyleCookie", "/pet/cookie/form/exploded", + ExtPetApi.GetPetWithFormExplodedStyleCookieSendActionBuilder.class, + ExtPetApi.GetPetWithFormExplodedStyleCookieReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-form-object-style-cookie", "getPetWithFormObjectStyleCookie", "/pet/cookie/form/object", + ExtPetApi.GetPetWithFormObjectStyleCookieSendActionBuilder.class, + ExtPetApi.GetPetWithFormObjectStyleCookieReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-form-style-cookie", "getPetWithFormStyleCookie", "/pet/cookie/form", + ExtPetApi.GetPetWithFormStyleCookieSendActionBuilder.class, + ExtPetApi.GetPetWithFormStyleCookieReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-form-style-exploded-object-query", "getPetWithFormStyleExplodedObjectQuery", "/pet/query/form/exploded/object", + ExtPetApi.GetPetWithFormStyleExplodedObjectQuerySendActionBuilder.class, + ExtPetApi.GetPetWithFormStyleExplodedObjectQueryReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-form-style-exploded-query", "getPetWithFormStyleExplodedQuery", "/pet/query/form/exploded", + ExtPetApi.GetPetWithFormStyleExplodedQuerySendActionBuilder.class, + ExtPetApi.GetPetWithFormStyleExplodedQueryReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-form-style-object-query", "getPetWithFormStyleObjectQuery", "/pet/query/form/object", + ExtPetApi.GetPetWithFormStyleObjectQuerySendActionBuilder.class, + ExtPetApi.GetPetWithFormStyleObjectQueryReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-form-style-query", "getPetWithFormStyleQuery", "/pet/query/form", + ExtPetApi.GetPetWithFormStyleQuerySendActionBuilder.class, + ExtPetApi.GetPetWithFormStyleQueryReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-label-style-array", "getPetWithLabelStyleArray", "/pet/label/{petId}", + ExtPetApi.GetPetWithLabelStyleArraySendActionBuilder.class, + ExtPetApi.GetPetWithLabelStyleArrayReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-label-style-array-exploded", "getPetWithLabelStyleArrayExploded", "/pet/label/exploded/{petId}", + ExtPetApi.GetPetWithLabelStyleArrayExplodedSendActionBuilder.class, + ExtPetApi.GetPetWithLabelStyleArrayExplodedReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-label-style-object", "getPetWithLabelStyleObject", "/pet/label/object/{petId}", + ExtPetApi.GetPetWithLabelStyleObjectSendActionBuilder.class, + ExtPetApi.GetPetWithLabelStyleObjectReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-label-style-object-exploded", "getPetWithLabelStyleObjectExploded", "/pet/label/exploded/object/{petId}", + ExtPetApi.GetPetWithLabelStyleObjectExplodedSendActionBuilder.class, + ExtPetApi.GetPetWithLabelStyleObjectExplodedReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-matrix-style-array", "getPetWithMatrixStyleArray", "/pet/matrix/{petId}", + ExtPetApi.GetPetWithMatrixStyleArraySendActionBuilder.class, + ExtPetApi.GetPetWithMatrixStyleArrayReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-matrix-style-array-exploded", "getPetWithMatrixStyleArrayExploded", "/pet/matrix/exploded/{petId}", + ExtPetApi.GetPetWithMatrixStyleArrayExplodedSendActionBuilder.class, + ExtPetApi.GetPetWithMatrixStyleArrayExplodedReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-matrix-style-object", "getPetWithMatrixStyleObject", "/pet/matrix/object/{petId}", + ExtPetApi.GetPetWithMatrixStyleObjectSendActionBuilder.class, + ExtPetApi.GetPetWithMatrixStyleObjectReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-matrix-style-object-exploded", "getPetWithMatrixStyleObjectExploded", "/pet/matrix/exploded/object/{petId}", + ExtPetApi.GetPetWithMatrixStyleObjectExplodedSendActionBuilder.class, + ExtPetApi.GetPetWithMatrixStyleObjectExplodedReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-parameters-requiring-encoding", "getPetWithParametersRequiringEncoding", "/pet/parameter-with-url-encoding-required/{Pet ID}", + ExtPetApi.GetPetWithParametersRequiringEncodingSendActionBuilder.class, + ExtPetApi.GetPetWithParametersRequiringEncodingReceiveActionBuilder.class, + new String[]{ "petID" }, + new String[]{ "queryID" }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-simple-style-array", "getPetWithSimpleStyleArray", "/pet/simple/{petId}", + ExtPetApi.GetPetWithSimpleStyleArraySendActionBuilder.class, + ExtPetApi.GetPetWithSimpleStyleArrayReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-simple-style-array-exploded", "getPetWithSimpleStyleArrayExploded", "/pet/simple/exploded/{petId}", + ExtPetApi.GetPetWithSimpleStyleArrayExplodedSendActionBuilder.class, + ExtPetApi.GetPetWithSimpleStyleArrayExplodedReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-simple-style-exploded-header", "getPetWithSimpleStyleExplodedHeader", "/pet/header/simple/exploded", + ExtPetApi.GetPetWithSimpleStyleExplodedHeaderSendActionBuilder.class, + ExtPetApi.GetPetWithSimpleStyleExplodedHeaderReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-simple-style-exploded-object-header", "getPetWithSimpleStyleExplodedObjectHeader", "/pet/header/simple/exploded/object", + ExtPetApi.GetPetWithSimpleStyleExplodedObjectHeaderSendActionBuilder.class, + ExtPetApi.GetPetWithSimpleStyleExplodedObjectHeaderReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-simple-style-header", "getPetWithSimpleStyleHeader", "/pet/header/simple", + ExtPetApi.GetPetWithSimpleStyleHeaderSendActionBuilder.class, + ExtPetApi.GetPetWithSimpleStyleHeaderReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-simple-style-object", "getPetWithSimpleStyleObject", "/pet/simple/object/{petId}", + ExtPetApi.GetPetWithSimpleStyleObjectSendActionBuilder.class, + ExtPetApi.GetPetWithSimpleStyleObjectReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-simple-style-object-exploded", "getPetWithSimpleStyleObjectExploded", "/pet/simple/exploded/object/{petId}", + ExtPetApi.GetPetWithSimpleStyleObjectExplodedSendActionBuilder.class, + ExtPetApi.GetPetWithSimpleStyleObjectExplodedReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"get-pet-with-simple-style-object-header", "getPetWithSimpleStyleObjectHeader", "/pet/header/simple/object", + ExtPetApi.GetPetWithSimpleStyleObjectHeaderSendActionBuilder.class, + ExtPetApi.GetPetWithSimpleStyleObjectHeaderReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"pet-without-operation-id-pet-id-get", "GET_/api/v3/ext/pet/without-operation-id/{petId}", "/pet/without-operation-id/{petId}", + ExtPetApi.PetWithoutOperationIdPetIdGetSendActionBuilder.class, + ExtPetApi.PetWithoutOperationIdPetIdGetReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"post-vaccination-document", "postVaccinationDocument", "/pet/vaccination/{bucket}/{filename}", + ExtPetApi.PostVaccinationDocumentSendActionBuilder.class, + ExtPetApi.PostVaccinationDocumentReceiveActionBuilder.class, + new String[]{ "bucket", "filename" }, + new String[]{ }); + + registerOperationParsers(ExtPetApi.class,"post-vaccination-form-data", "postVaccinationFormData", "/pet/vaccination/form", + ExtPetApi.PostVaccinationFormDataSendActionBuilder.class, + ExtPetApi.PostVaccinationFormDataReceiveActionBuilder.class, + new String[]{ }, + new String[]{ "vaccine", "isFirstVaccination", "doseNumber", "vaccinationDate" }); + + registerOperationParsers(ExtPetApi.class,"update-pet-with-array-query-data", "updatePetWithArrayQueryData", "/pet/{petId}", + ExtPetApi.UpdatePetWithArrayQueryDataSendActionBuilder.class, + ExtPetApi.UpdatePetWithArrayQueryDataReceiveActionBuilder.class, + new String[]{ "petId", "_name", "status", "tags", "nicknames", "sampleStringHeader" }, + new String[]{ "sampleIntHeader" }); + + registerOperationParsers(ExtPetApi.class,"update-pet-with-form-url-encoded", "updatePetWithFormUrlEncoded", "/pet/form/{petId}", + ExtPetApi.UpdatePetWithFormUrlEncodedSendActionBuilder.class, + ExtPetApi.UpdatePetWithFormUrlEncodedReceiveActionBuilder.class, + new String[]{ "petId", "_name", "status", "age", "tags" }, + new String[]{ "owners", "nicknames" }); + } + + private void registerOperationParsers(Class apiClass, String elementName, String operationName, String path, + Class sendBeanClass, + Class receiveBeanClass, + String[] constructorParameters, + String[] nonConstructorParameters) { + + RestApiSendMessageActionParser sendParser = new RestApiSendMessageActionParser(extPetStoreSpecification, operationName, + path, + apiClass, + sendBeanClass, + receiveBeanClass, + "extpetstore.endpoint"); + sendParser.setConstructorParameters(constructorParameters); + sendParser.setNonConstructorParameters(nonConstructorParameters); + registerBeanDefinitionParser("send-"+elementName, sendParser); + + RestApiReceiveMessageActionParser receiveParser = new RestApiReceiveMessageActionParser(extPetStoreSpecification, + operationName, apiClass, receiveBeanClass, "extpetstore.endpoint"); + registerBeanDefinitionParser("receive-"+elementName, receiveParser); + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/PetStoreOpenApi.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/PetStoreOpenApi.java new file mode 100644 index 0000000000..5565d7b5e7 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/PetStoreOpenApi.java @@ -0,0 +1,10 @@ +package org.citrusframework.openapi.generator.rest.petstore; + +import org.citrusframework.openapi.OpenApiSpecification; + +public class PetStoreOpenApi { + + public static final OpenApiSpecification petStoreSpecification = OpenApiSpecification + .from(PetStoreOpenApi.class.getResource("petStore_openApi.yaml")); + +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Address.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Address.java new file mode 100644 index 0000000000..c3c9eeac45 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Address.java @@ -0,0 +1,168 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.petstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Address + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class Address { + private String street; + + private String city; + + private String state; + + private String zip; + + public Address() { + } + + public Address street(String street) { + + this.street = street; + return this; + } + + /** + * Get street + * @return street + */ + @jakarta.annotation.Nullable + + public String getStreet() { + return street; + } + + + public void setStreet(String street) { + this.street = street; + } + + public Address city(String city) { + + this.city = city; + return this; + } + + /** + * Get city + * @return city + */ + @jakarta.annotation.Nullable + + public String getCity() { + return city; + } + + + public void setCity(String city) { + this.city = city; + } + + public Address state(String state) { + + this.state = state; + return this; + } + + /** + * Get state + * @return state + */ + @jakarta.annotation.Nullable + + public String getState() { + return state; + } + + + public void setState(String state) { + this.state = state; + } + + public Address zip(String zip) { + + this.zip = zip; + return this; + } + + /** + * Get zip + * @return zip + */ + @jakarta.annotation.Nullable + + public String getZip() { + return zip; + } + + + public void setZip(String zip) { + this.zip = zip; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Address address = (Address) o; + return Objects.equals(this.street, address.street) && + Objects.equals(this.city, address.city) && + Objects.equals(this.state, address.state) && + Objects.equals(this.zip, address.zip); + } + + @Override + public int hashCode() { + return Objects.hash(street, city, state, zip); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Address {\n"); + sb.append(" street: ").append(toIndentedString(street)).append("\n"); + sb.append(" city: ").append(toIndentedString(city)).append("\n"); + sb.append(" state: ").append(toIndentedString(state)).append("\n"); + sb.append(" zip: ").append(toIndentedString(zip)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Category.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Category.java new file mode 100644 index 0000000000..54bfb4c0ed --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Category.java @@ -0,0 +1,118 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.petstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Category + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class Category { + private Long id; + + private String _name; + + public Category() { + } + + public Category id(Long id) { + + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + @jakarta.annotation.Nullable + + public Long getId() { + return id; + } + + + public void setId(Long id) { + this.id = id; + } + + public Category _name(String _name) { + + this._name = _name; + return this; + } + + /** + * Get _name + * @return _name + */ + @jakarta.annotation.Nullable + + public String getName() { + return _name; + } + + + public void setName(String _name) { + this._name = _name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Category category = (Category) o; + return Objects.equals(this.id, category.id) && + Objects.equals(this._name, category._name); + } + + @Override + public int hashCode() { + return Objects.hash(id, _name); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Category {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" _name: ").append(toIndentedString(_name)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Customer.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Customer.java new file mode 100644 index 0000000000..aaa350d108 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Customer.java @@ -0,0 +1,155 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.petstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.citrusframework.openapi.generator.rest.petstore.model.Address; + +/** + * Customer + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class Customer { + private Long id; + + private String username; + + private List

      address = new ArrayList<>(); + + public Customer() { + } + + public Customer id(Long id) { + + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + @jakarta.annotation.Nullable + + public Long getId() { + return id; + } + + + public void setId(Long id) { + this.id = id; + } + + public Customer username(String username) { + + this.username = username; + return this; + } + + /** + * Get username + * @return username + */ + @jakarta.annotation.Nullable + + public String getUsername() { + return username; + } + + + public void setUsername(String username) { + this.username = username; + } + + public Customer address(List
      address) { + + this.address = address; + return this; + } + + public Customer addAddressItem(Address addressItem) { + if (this.address == null) { + this.address = new ArrayList<>(); + } + this.address.add(addressItem); + return this; + } + + /** + * Get address + * @return address + */ + @jakarta.annotation.Nullable + + public List
      getAddress() { + return address; + } + + + public void setAddress(List
      address) { + this.address = address; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Customer customer = (Customer) o; + return Objects.equals(this.id, customer.id) && + Objects.equals(this.username, customer.username) && + Objects.equals(this.address, customer.address); + } + + @Override + public int hashCode() { + return Objects.hash(id, username, address); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Customer {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" username: ").append(toIndentedString(username)).append("\n"); + sb.append(" address: ").append(toIndentedString(address)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/ModelApiResponse.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/ModelApiResponse.java new file mode 100644 index 0000000000..829c0d4384 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/ModelApiResponse.java @@ -0,0 +1,143 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.petstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * ModelApiResponse + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class ModelApiResponse { + private Integer code; + + private String type; + + private String _message; + + public ModelApiResponse() { + } + + public ModelApiResponse code(Integer code) { + + this.code = code; + return this; + } + + /** + * Get code + * @return code + */ + @jakarta.annotation.Nullable + + public Integer getCode() { + return code; + } + + + public void setCode(Integer code) { + this.code = code; + } + + public ModelApiResponse type(String type) { + + this.type = type; + return this; + } + + /** + * Get type + * @return type + */ + @jakarta.annotation.Nullable + + public String getType() { + return type; + } + + + public void setType(String type) { + this.type = type; + } + + public ModelApiResponse _message(String _message) { + + this._message = _message; + return this; + } + + /** + * Get _message + * @return _message + */ + @jakarta.annotation.Nullable + + public String getMessage() { + return _message; + } + + + public void setMessage(String _message) { + this._message = _message; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ModelApiResponse _apiResponse = (ModelApiResponse) o; + return Objects.equals(this.code, _apiResponse.code) && + Objects.equals(this.type, _apiResponse.type) && + Objects.equals(this._message, _apiResponse._message); + } + + @Override + public int hashCode() { + return Objects.hash(code, type, _message); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ModelApiResponse {\n"); + sb.append(" code: ").append(toIndentedString(code)).append("\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" _message: ").append(toIndentedString(_message)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Order.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Order.java new file mode 100644 index 0000000000..ca29b9d1ca --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Order.java @@ -0,0 +1,254 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.petstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.time.OffsetDateTime; + +/** + * Order + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class Order { + private Long id; + + private Long petId; + + private Integer quantity; + + private OffsetDateTime shipDate; + + /** + * Order Status + */ + public enum StatusEnum { + PLACED("placed"), + + APPROVED("approved"), + + DELIVERED("delivered"); + + private String value; + + StatusEnum(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + public static StatusEnum fromValue(String value) { + for (StatusEnum b : StatusEnum.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + } + + private StatusEnum status; + + private Boolean complete; + + public Order() { + } + + public Order id(Long id) { + + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + @jakarta.annotation.Nullable + + public Long getId() { + return id; + } + + + public void setId(Long id) { + this.id = id; + } + + public Order petId(Long petId) { + + this.petId = petId; + return this; + } + + /** + * Get petId + * @return petId + */ + @jakarta.annotation.Nullable + + public Long getPetId() { + return petId; + } + + + public void setPetId(Long petId) { + this.petId = petId; + } + + public Order quantity(Integer quantity) { + + this.quantity = quantity; + return this; + } + + /** + * Get quantity + * @return quantity + */ + @jakarta.annotation.Nullable + + public Integer getQuantity() { + return quantity; + } + + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public Order shipDate(OffsetDateTime shipDate) { + + this.shipDate = shipDate; + return this; + } + + /** + * Get shipDate + * @return shipDate + */ + @jakarta.annotation.Nullable + + public OffsetDateTime getShipDate() { + return shipDate; + } + + + public void setShipDate(OffsetDateTime shipDate) { + this.shipDate = shipDate; + } + + public Order status(StatusEnum status) { + + this.status = status; + return this; + } + + /** + * Order Status + * @return status + */ + @jakarta.annotation.Nullable + + public StatusEnum getStatus() { + return status; + } + + + public void setStatus(StatusEnum status) { + this.status = status; + } + + public Order complete(Boolean complete) { + + this.complete = complete; + return this; + } + + /** + * Get complete + * @return complete + */ + @jakarta.annotation.Nullable + + public Boolean getComplete() { + return complete; + } + + + public void setComplete(Boolean complete) { + this.complete = complete; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Order order = (Order) o; + return Objects.equals(this.id, order.id) && + Objects.equals(this.petId, order.petId) && + Objects.equals(this.quantity, order.quantity) && + Objects.equals(this.shipDate, order.shipDate) && + Objects.equals(this.status, order.status) && + Objects.equals(this.complete, order.complete); + } + + @Override + public int hashCode() { + return Objects.hash(id, petId, quantity, shipDate, status, complete); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Order {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" petId: ").append(toIndentedString(petId)).append("\n"); + sb.append(" quantity: ").append(toIndentedString(quantity)).append("\n"); + sb.append(" shipDate: ").append(toIndentedString(shipDate)).append("\n"); + sb.append(" status: ").append(toIndentedString(status)).append("\n"); + sb.append(" complete: ").append(toIndentedString(complete)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Pet.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Pet.java new file mode 100644 index 0000000000..6241feff86 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Pet.java @@ -0,0 +1,274 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.petstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.citrusframework.openapi.generator.rest.petstore.model.Category; +import org.citrusframework.openapi.generator.rest.petstore.model.Tag; + +/** + * Pet + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class Pet { + private Long id; + + private String _name; + + private Category category; + + private List photoUrls = new ArrayList<>(); + + private List tags = new ArrayList<>(); + + /** + * pet status in the store + */ + public enum StatusEnum { + AVAILABLE("available"), + + PENDING("pending"), + + SOLD("sold"); + + private String value; + + StatusEnum(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + public static StatusEnum fromValue(String value) { + for (StatusEnum b : StatusEnum.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + } + + private StatusEnum status; + + public Pet() { + } + + public Pet id(Long id) { + + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + @jakarta.annotation.Nullable + + public Long getId() { + return id; + } + + + public void setId(Long id) { + this.id = id; + } + + public Pet _name(String _name) { + + this._name = _name; + return this; + } + + /** + * Get _name + * @return _name + */ + @jakarta.annotation.Nonnull + + public String getName() { + return _name; + } + + + public void setName(String _name) { + this._name = _name; + } + + public Pet category(Category category) { + + this.category = category; + return this; + } + + /** + * Get category + * @return category + */ + @jakarta.annotation.Nullable + + public Category getCategory() { + return category; + } + + + public void setCategory(Category category) { + this.category = category; + } + + public Pet photoUrls(List photoUrls) { + + this.photoUrls = photoUrls; + return this; + } + + public Pet addPhotoUrlsItem(String photoUrlsItem) { + if (this.photoUrls == null) { + this.photoUrls = new ArrayList<>(); + } + this.photoUrls.add(photoUrlsItem); + return this; + } + + /** + * Get photoUrls + * @return photoUrls + */ + @jakarta.annotation.Nonnull + + public List getPhotoUrls() { + return photoUrls; + } + + + public void setPhotoUrls(List photoUrls) { + this.photoUrls = photoUrls; + } + + public Pet tags(List tags) { + + this.tags = tags; + return this; + } + + public Pet addTagsItem(Tag tagsItem) { + if (this.tags == null) { + this.tags = new ArrayList<>(); + } + this.tags.add(tagsItem); + return this; + } + + /** + * Get tags + * @return tags + */ + @jakarta.annotation.Nullable + + public List getTags() { + return tags; + } + + + public void setTags(List tags) { + this.tags = tags; + } + + public Pet status(StatusEnum status) { + + this.status = status; + return this; + } + + /** + * pet status in the store + * @return status + */ + @jakarta.annotation.Nullable + + public StatusEnum getStatus() { + return status; + } + + + public void setStatus(StatusEnum status) { + this.status = status; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Pet pet = (Pet) o; + return Objects.equals(this.id, pet.id) && + Objects.equals(this._name, pet._name) && + Objects.equals(this.category, pet.category) && + Objects.equals(this.photoUrls, pet.photoUrls) && + Objects.equals(this.tags, pet.tags) && + Objects.equals(this.status, pet.status); + } + + @Override + public int hashCode() { + return Objects.hash(id, _name, category, photoUrls, tags, status); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Pet {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" _name: ").append(toIndentedString(_name)).append("\n"); + sb.append(" category: ").append(toIndentedString(category)).append("\n"); + sb.append(" photoUrls: ").append(toIndentedString(photoUrls)).append("\n"); + sb.append(" tags: ").append(toIndentedString(tags)).append("\n"); + sb.append(" status: ").append(toIndentedString(status)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Tag.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Tag.java new file mode 100644 index 0000000000..2139697852 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/Tag.java @@ -0,0 +1,118 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.petstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * Tag + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class Tag { + private Long id; + + private String _name; + + public Tag() { + } + + public Tag id(Long id) { + + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + @jakarta.annotation.Nullable + + public Long getId() { + return id; + } + + + public void setId(Long id) { + this.id = id; + } + + public Tag _name(String _name) { + + this._name = _name; + return this; + } + + /** + * Get _name + * @return _name + */ + @jakarta.annotation.Nullable + + public String getName() { + return _name; + } + + + public void setName(String _name) { + this._name = _name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Tag tag = (Tag) o; + return Objects.equals(this.id, tag.id) && + Objects.equals(this._name, tag._name); + } + + @Override + public int hashCode() { + return Objects.hash(id, _name); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Tag {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" _name: ").append(toIndentedString(_name)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/User.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/User.java new file mode 100644 index 0000000000..34509d8ac4 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/model/User.java @@ -0,0 +1,268 @@ +/* +* Copyright the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package org.citrusframework.openapi.generator.rest.petstore.model; + +import java.util.Objects; +import java.util.Arrays; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +/** + * User + */ +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class User { + private Long id; + + private String username; + + private String firstName; + + private String lastName; + + private String email; + + private String password; + + private String phone; + + private Integer userStatus; + + public User() { + } + + public User id(Long id) { + + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + @jakarta.annotation.Nullable + + public Long getId() { + return id; + } + + + public void setId(Long id) { + this.id = id; + } + + public User username(String username) { + + this.username = username; + return this; + } + + /** + * Get username + * @return username + */ + @jakarta.annotation.Nullable + + public String getUsername() { + return username; + } + + + public void setUsername(String username) { + this.username = username; + } + + public User firstName(String firstName) { + + this.firstName = firstName; + return this; + } + + /** + * Get firstName + * @return firstName + */ + @jakarta.annotation.Nullable + + public String getFirstName() { + return firstName; + } + + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public User lastName(String lastName) { + + this.lastName = lastName; + return this; + } + + /** + * Get lastName + * @return lastName + */ + @jakarta.annotation.Nullable + + public String getLastName() { + return lastName; + } + + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public User email(String email) { + + this.email = email; + return this; + } + + /** + * Get email + * @return email + */ + @jakarta.annotation.Nullable + + public String getEmail() { + return email; + } + + + public void setEmail(String email) { + this.email = email; + } + + public User password(String password) { + + this.password = password; + return this; + } + + /** + * Get password + * @return password + */ + @jakarta.annotation.Nullable + + public String getPassword() { + return password; + } + + + public void setPassword(String password) { + this.password = password; + } + + public User phone(String phone) { + + this.phone = phone; + return this; + } + + /** + * Get phone + * @return phone + */ + @jakarta.annotation.Nullable + + public String getPhone() { + return phone; + } + + + public void setPhone(String phone) { + this.phone = phone; + } + + public User userStatus(Integer userStatus) { + + this.userStatus = userStatus; + return this; + } + + /** + * User Status + * @return userStatus + */ + @jakarta.annotation.Nullable + + public Integer getUserStatus() { + return userStatus; + } + + + public void setUserStatus(Integer userStatus) { + this.userStatus = userStatus; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + User user = (User) o; + return Objects.equals(this.id, user.id) && + Objects.equals(this.username, user.username) && + Objects.equals(this.firstName, user.firstName) && + Objects.equals(this.lastName, user.lastName) && + Objects.equals(this.email, user.email) && + Objects.equals(this.password, user.password) && + Objects.equals(this.phone, user.phone) && + Objects.equals(this.userStatus, user.userStatus); + } + + @Override + public int hashCode() { + return Objects.hash(id, username, firstName, lastName, email, password, phone, userStatus); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class User {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" username: ").append(toIndentedString(username)).append("\n"); + sb.append(" firstName: ").append(toIndentedString(firstName)).append("\n"); + sb.append(" lastName: ").append(toIndentedString(lastName)).append("\n"); + sb.append(" email: ").append(toIndentedString(email)).append("\n"); + sb.append(" password: ").append(toIndentedString(password)).append("\n"); + sb.append(" phone: ").append(toIndentedString(phone)).append("\n"); + sb.append(" userStatus: ").append(toIndentedString(userStatus)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/request/PetApi.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/request/PetApi.java new file mode 100644 index 0000000000..17f78a1a90 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/request/PetApi.java @@ -0,0 +1,1156 @@ +package org.citrusframework.openapi.generator.rest.petstore.request; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.citrusframework.util.StringUtils.isEmpty; +import static org.citrusframework.util.StringUtils.isNotEmpty; + +import static org.citrusframework.openapi.generator.rest.petstore.PetStoreOpenApi.petStoreSpecification; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.net.URL; +import java.time.LocalDate; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.UUID; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +import org.citrusframework.openapi.testapi.GeneratedApiOperationInfo; +import org.citrusframework.openapi.testapi.ParameterStyle; +import org.citrusframework.openapi.testapi.RestApiSendMessageActionBuilder; +import org.citrusframework.openapi.testapi.RestApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.testapi.TestApiUtils; +import org.citrusframework.spi.Resource; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; + +import org.citrusframework.openapi.generator.rest.petstore.PetStoreOpenApi; +import org.citrusframework.openapi.generator.rest.petstore.model.ModelApiResponse; +import org.citrusframework.openapi.generator.rest.petstore.model.Pet; + +@SuppressWarnings("unused") +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class PetApi implements GeneratedApi +{ + + @Value("${" + "petstore.base64-encode-api-key:#{false}}") + private boolean base64EncodeApiKey; + + @Value("${" + "petstore.api-key:#{null}}") + private String defaultApiKey; + + private final List customizers; + + /** + * An optional default endpoint which will be passed into the requests. + */ + private final Endpoint defaultEndpoint; + + public PetApi(@Nullable Endpoint defaultEndpoint) { + this(defaultEndpoint, emptyList()); + } + + public PetApi(@Nullable Endpoint defaultEndpoint, @Nullable List customizers) { + this.defaultEndpoint = defaultEndpoint; + this.customizers = customizers; + } + + public static PetApi petApi(Endpoint defaultEndpoint) { + return new PetApi(defaultEndpoint); + } + + @Override + public String getApiTitle() { + return "Swagger Petstore - OpenAPI 3.0"; + } + + @Override + public String getApiVersion() { + return "1.0.19"; + } + + @Override + public String getApiPrefix() { + return "petStore"; + } + + @Override + public Map getApiInfoExtensions() { + return emptyMap(); + } + + @Override + @Nullable + public Endpoint getEndpoint() { + return defaultEndpoint; + } + + @Override + public List getCustomizers() { + return customizers; + } + + /** + * Builder with type safe required parameters. + */ + public AddPetSendActionBuilder sendAddPet() { + return new AddPetSendActionBuilder(this); + } + + public AddPetReceiveActionBuilder receiveAddPet(@NotNull HttpStatus statusCode) { + return new AddPetReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public AddPetReceiveActionBuilder receiveAddPet(@NotNull String statusCode) { + return new AddPetReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public DeletePetSendActionBuilder sendDeletePet(Long petId) { + return new DeletePetSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public DeletePetSendActionBuilder sendDeletePet$(String petIdExpression ) { + return new DeletePetSendActionBuilder(petIdExpression, this); + } + + public DeletePetReceiveActionBuilder receiveDeletePet(@NotNull HttpStatus statusCode) { + return new DeletePetReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public DeletePetReceiveActionBuilder receiveDeletePet(@NotNull String statusCode) { + return new DeletePetReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public FindPetsByStatusSendActionBuilder sendFindPetsByStatus() { + return new FindPetsByStatusSendActionBuilder(this); + } + + public FindPetsByStatusReceiveActionBuilder receiveFindPetsByStatus(@NotNull HttpStatus statusCode) { + return new FindPetsByStatusReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public FindPetsByStatusReceiveActionBuilder receiveFindPetsByStatus(@NotNull String statusCode) { + return new FindPetsByStatusReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public FindPetsByTagsSendActionBuilder sendFindPetsByTags() { + return new FindPetsByTagsSendActionBuilder(this); + } + + public FindPetsByTagsReceiveActionBuilder receiveFindPetsByTags(@NotNull HttpStatus statusCode) { + return new FindPetsByTagsReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public FindPetsByTagsReceiveActionBuilder receiveFindPetsByTags(@NotNull String statusCode) { + return new FindPetsByTagsReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetPetByIdSendActionBuilder sendGetPetById(Long petId) { + GetPetByIdSendActionBuilder builder = new GetPetByIdSendActionBuilder(this, petId); + builder.setBase64EncodeApiKey(base64EncodeApiKey); + return builder; + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetPetByIdSendActionBuilder sendGetPetById$(String petIdExpression ) { + GetPetByIdSendActionBuilder builder = new GetPetByIdSendActionBuilder(petIdExpression, this); + builder.setBase64EncodeApiKey(base64EncodeApiKey); + builder.setApiKey(defaultApiKey); + return builder; + } + + public GetPetByIdReceiveActionBuilder receiveGetPetById(@NotNull HttpStatus statusCode) { + return new GetPetByIdReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetPetByIdReceiveActionBuilder receiveGetPetById(@NotNull String statusCode) { + return new GetPetByIdReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public UpdatePetSendActionBuilder sendUpdatePet() { + return new UpdatePetSendActionBuilder(this); + } + + public UpdatePetReceiveActionBuilder receiveUpdatePet(@NotNull HttpStatus statusCode) { + return new UpdatePetReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public UpdatePetReceiveActionBuilder receiveUpdatePet(@NotNull String statusCode) { + return new UpdatePetReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public UpdatePetWithFormSendActionBuilder sendUpdatePetWithForm(Long petId) { + return new UpdatePetWithFormSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public UpdatePetWithFormSendActionBuilder sendUpdatePetWithForm$(String petIdExpression ) { + return new UpdatePetWithFormSendActionBuilder(petIdExpression, this); + } + + public UpdatePetWithFormReceiveActionBuilder receiveUpdatePetWithForm(@NotNull HttpStatus statusCode) { + return new UpdatePetWithFormReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public UpdatePetWithFormReceiveActionBuilder receiveUpdatePetWithForm(@NotNull String statusCode) { + return new UpdatePetWithFormReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public UploadFileSendActionBuilder sendUploadFile(Long petId) { + return new UploadFileSendActionBuilder(this, petId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public UploadFileSendActionBuilder sendUploadFile$(String petIdExpression ) { + return new UploadFileSendActionBuilder(petIdExpression, this); + } + + public UploadFileReceiveActionBuilder receiveUploadFile(@NotNull HttpStatus statusCode) { + return new UploadFileReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public UploadFileReceiveActionBuilder receiveUploadFile(@NotNull String statusCode) { + return new UploadFileReceiveActionBuilder(this, statusCode); + } + + public static class AddPetSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/pet"; + + private static final String OPERATION_NAME = "addPet"; + + /** + * Constructor with type safe required parameters. + */ + public AddPetSendActionBuilder(PetApi petApi) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + public AddPetSendActionBuilder(PetApi petApi, TestApiClientRequestMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class AddPetReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/pet"; + + private static final String OPERATION_NAME = "addPet"; + + public AddPetReceiveActionBuilder(PetApi petApi, String statusCode) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public AddPetReceiveActionBuilder(PetApi petApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class DeletePetSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "DELETE"; + + private static final String ENDPOINT = "/api/v3/pet/{petId}"; + + private static final String OPERATION_NAME = "deletePet"; + + /** + * Constructor with type safe required parameters. + */ + public DeletePetSendActionBuilder(PetApi petApi, Long petId) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public DeletePetSendActionBuilder(String petIdExpression, PetApi petApi) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public DeletePetSendActionBuilder(PetApi petApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + public DeletePetSendActionBuilder petId(Long petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public DeletePetSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public DeletePetSendActionBuilder apiKey(String apiKey) { + headerParameter("api_key", apiKey, ParameterStyle.SIMPLE, false, false); + return this; + } + + public void setApiKey(String apiKey) { + headerParameter("api_key", apiKey, ParameterStyle.SIMPLE, false, false); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class DeletePetReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "DELETE"; + + private static final String ENDPOINT = "/api/v3/pet/{petId}"; + + private static final String OPERATION_NAME = "deletePet"; + + public DeletePetReceiveActionBuilder(PetApi petApi, String statusCode) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public DeletePetReceiveActionBuilder(PetApi petApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class FindPetsByStatusSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/pet/findByStatus"; + + private static final String OPERATION_NAME = "findPetsByStatus"; + + /** + * Constructor with type safe required parameters. + */ + public FindPetsByStatusSendActionBuilder(PetApi petApi) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + public FindPetsByStatusSendActionBuilder(PetApi petApi, TestApiClientRequestMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + public FindPetsByStatusSendActionBuilder status(String status) { + queryParameter("status", status, ParameterStyle.FORM, true, false); + return this; + } + + public void setStatus(String status) { + queryParameter("status", status, ParameterStyle.FORM, true, false); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class FindPetsByStatusReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/pet/findByStatus"; + + private static final String OPERATION_NAME = "findPetsByStatus"; + + public FindPetsByStatusReceiveActionBuilder(PetApi petApi, String statusCode) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public FindPetsByStatusReceiveActionBuilder(PetApi petApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class FindPetsByTagsSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/pet/findByTags"; + + private static final String OPERATION_NAME = "findPetsByTags"; + + /** + * Constructor with type safe required parameters. + */ + public FindPetsByTagsSendActionBuilder(PetApi petApi) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + public FindPetsByTagsSendActionBuilder(PetApi petApi, TestApiClientRequestMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + public FindPetsByTagsSendActionBuilder tags(String...tags) { + queryParameter("tags", tags, ParameterStyle.FORM, true, false); + return this; + } + + public void setTags(String...tags) { + queryParameter("tags", tags, ParameterStyle.FORM, true, false); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class FindPetsByTagsReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/pet/findByTags"; + + private static final String OPERATION_NAME = "findPetsByTags"; + + public FindPetsByTagsReceiveActionBuilder(PetApi petApi, String statusCode) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public FindPetsByTagsReceiveActionBuilder(PetApi petApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetPetByIdSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/pet/{petId}"; + + private static final String OPERATION_NAME = "getPetById"; + + @Value("${" + "petstore.base64-encode-api-key:#{false}}") + private boolean base64EncodeApiKey; + + @Value("${" + "petstore.api-key:#{null}}") + private String defaultApiKey; + + private String apiKey; + + /** + * Constructor with type safe required parameters. + */ + public GetPetByIdSendActionBuilder(PetApi petApi, Long petId) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetByIdSendActionBuilder(String petIdExpression, PetApi petApi) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetPetByIdSendActionBuilder(PetApi petApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + public GetPetByIdSendActionBuilder petId(Long petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetPetByIdSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public void setBase64EncodeApiKey(boolean encode) { + this.base64EncodeApiKey = encode; + } + + public GetPetByIdSendActionBuilder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public SendMessageAction doBuild() { + headerParameter("api_key", getOrDefault(apiKey, defaultApiKey, base64EncodeApiKey)); + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetPetByIdReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/pet/{petId}"; + + private static final String OPERATION_NAME = "getPetById"; + + public GetPetByIdReceiveActionBuilder(PetApi petApi, String statusCode) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetPetByIdReceiveActionBuilder(PetApi petApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class UpdatePetSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "PUT"; + + private static final String ENDPOINT = "/api/v3/pet"; + + private static final String OPERATION_NAME = "updatePet"; + + /** + * Constructor with type safe required parameters. + */ + public UpdatePetSendActionBuilder(PetApi petApi) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + public UpdatePetSendActionBuilder(PetApi petApi, TestApiClientRequestMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class UpdatePetReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "PUT"; + + private static final String ENDPOINT = "/api/v3/pet"; + + private static final String OPERATION_NAME = "updatePet"; + + public UpdatePetReceiveActionBuilder(PetApi petApi, String statusCode) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public UpdatePetReceiveActionBuilder(PetApi petApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class UpdatePetWithFormSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/pet/{petId}"; + + private static final String OPERATION_NAME = "updatePetWithForm"; + + /** + * Constructor with type safe required parameters. + */ + public UpdatePetWithFormSendActionBuilder(PetApi petApi, Long petId) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public UpdatePetWithFormSendActionBuilder(String petIdExpression, PetApi petApi) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public UpdatePetWithFormSendActionBuilder(PetApi petApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + public UpdatePetWithFormSendActionBuilder petId(Long petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public UpdatePetWithFormSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public UpdatePetWithFormSendActionBuilder _name(String _name) { + queryParameter("name", _name, ParameterStyle.FORM, true, false); + return this; + } + + public void set_name(String _name) { + queryParameter("name", _name, ParameterStyle.FORM, true, false); + } + + public UpdatePetWithFormSendActionBuilder status(String status) { + queryParameter("status", status, ParameterStyle.FORM, true, false); + return this; + } + + public void setStatus(String status) { + queryParameter("status", status, ParameterStyle.FORM, true, false); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class UpdatePetWithFormReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/pet/{petId}"; + + private static final String OPERATION_NAME = "updatePetWithForm"; + + public UpdatePetWithFormReceiveActionBuilder(PetApi petApi, String statusCode) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public UpdatePetWithFormReceiveActionBuilder(PetApi petApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class UploadFileSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/pet/{petId}/uploadImage"; + + private static final String OPERATION_NAME = "uploadFile"; + + /** + * Constructor with type safe required parameters. + */ + public UploadFileSendActionBuilder(PetApi petApi, Long petId) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public UploadFileSendActionBuilder(String petIdExpression, PetApi petApi) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public UploadFileSendActionBuilder(PetApi petApi, TestApiClientRequestMessageBuilder messageBuilder, String petIdExpression) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + } + + public UploadFileSendActionBuilder petId(Long petId) { + pathParameter("petId", petId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public UploadFileSendActionBuilder petId(String petIdExpression) { + pathParameter("petId", petIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + public UploadFileSendActionBuilder additionalMetadata(String additionalMetadata) { + queryParameter("additionalMetadata", additionalMetadata, ParameterStyle.FORM, true, false); + return this; + } + + public void setAdditionalMetadata(String additionalMetadata) { + queryParameter("additionalMetadata", additionalMetadata, ParameterStyle.FORM, true, false); + } + + public UploadFileSendActionBuilder body(org.citrusframework.spi.Resource body) { + return this; + } + + public void setBody(org.citrusframework.spi.Resource body) { + } + + public UploadFileSendActionBuilder body(String bodyExpression) { + return this; + } + + public void setBody(String bodyExpression) { + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class UploadFileReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/pet/{petId}/uploadImage"; + + private static final String OPERATION_NAME = "uploadFile"; + + public UploadFileReceiveActionBuilder(PetApi petApi, String statusCode) { + super(petApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public UploadFileReceiveActionBuilder(PetApi petApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(petApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/request/StoreApi.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/request/StoreApi.java new file mode 100644 index 0000000000..3359f32ed3 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/request/StoreApi.java @@ -0,0 +1,618 @@ +package org.citrusframework.openapi.generator.rest.petstore.request; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.citrusframework.util.StringUtils.isEmpty; +import static org.citrusframework.util.StringUtils.isNotEmpty; + +import static org.citrusframework.openapi.generator.rest.petstore.PetStoreOpenApi.petStoreSpecification; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.net.URL; +import java.time.LocalDate; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.UUID; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +import org.citrusframework.openapi.testapi.GeneratedApiOperationInfo; +import org.citrusframework.openapi.testapi.ParameterStyle; +import org.citrusframework.openapi.testapi.RestApiSendMessageActionBuilder; +import org.citrusframework.openapi.testapi.RestApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.testapi.TestApiUtils; +import org.citrusframework.spi.Resource; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; + +import org.citrusframework.openapi.generator.rest.petstore.PetStoreOpenApi; +import org.citrusframework.openapi.generator.rest.petstore.model.Order; + +@SuppressWarnings("unused") +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class StoreApi implements GeneratedApi +{ + + @Value("${" + "petstore.base64-encode-api-key:#{false}}") + private boolean base64EncodeApiKey; + + @Value("${" + "petstore.api-key:#{null}}") + private String defaultApiKey; + + private final List customizers; + + /** + * An optional default endpoint which will be passed into the requests. + */ + private final Endpoint defaultEndpoint; + + public StoreApi(@Nullable Endpoint defaultEndpoint) { + this(defaultEndpoint, emptyList()); + } + + public StoreApi(@Nullable Endpoint defaultEndpoint, @Nullable List customizers) { + this.defaultEndpoint = defaultEndpoint; + this.customizers = customizers; + } + + public static StoreApi storeApi(Endpoint defaultEndpoint) { + return new StoreApi(defaultEndpoint); + } + + @Override + public String getApiTitle() { + return "Swagger Petstore - OpenAPI 3.0"; + } + + @Override + public String getApiVersion() { + return "1.0.19"; + } + + @Override + public String getApiPrefix() { + return "petStore"; + } + + @Override + public Map getApiInfoExtensions() { + return emptyMap(); + } + + @Override + @Nullable + public Endpoint getEndpoint() { + return defaultEndpoint; + } + + @Override + public List getCustomizers() { + return customizers; + } + + /** + * Builder with type safe required parameters. + */ + public DeleteOrderSendActionBuilder sendDeleteOrder(Long orderId) { + return new DeleteOrderSendActionBuilder(this, orderId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public DeleteOrderSendActionBuilder sendDeleteOrder$(String orderIdExpression ) { + return new DeleteOrderSendActionBuilder(orderIdExpression, this); + } + + public DeleteOrderReceiveActionBuilder receiveDeleteOrder(@NotNull HttpStatus statusCode) { + return new DeleteOrderReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public DeleteOrderReceiveActionBuilder receiveDeleteOrder(@NotNull String statusCode) { + return new DeleteOrderReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetInventorySendActionBuilder sendGetInventory() { + GetInventorySendActionBuilder builder = new GetInventorySendActionBuilder(this); + builder.setBase64EncodeApiKey(base64EncodeApiKey); + return builder; + } + + public GetInventoryReceiveActionBuilder receiveGetInventory(@NotNull HttpStatus statusCode) { + return new GetInventoryReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetInventoryReceiveActionBuilder receiveGetInventory(@NotNull String statusCode) { + return new GetInventoryReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetOrderByIdSendActionBuilder sendGetOrderById(Long orderId) { + return new GetOrderByIdSendActionBuilder(this, orderId); + } + + /** + * Builder with required parameters as string, allowing dynamic content using citrus expressions. + */ + public GetOrderByIdSendActionBuilder sendGetOrderById$(String orderIdExpression ) { + return new GetOrderByIdSendActionBuilder(orderIdExpression, this); + } + + public GetOrderByIdReceiveActionBuilder receiveGetOrderById(@NotNull HttpStatus statusCode) { + return new GetOrderByIdReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetOrderByIdReceiveActionBuilder receiveGetOrderById(@NotNull String statusCode) { + return new GetOrderByIdReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public PlaceOrderSendActionBuilder sendPlaceOrder() { + return new PlaceOrderSendActionBuilder(this); + } + + public PlaceOrderReceiveActionBuilder receivePlaceOrder(@NotNull HttpStatus statusCode) { + return new PlaceOrderReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public PlaceOrderReceiveActionBuilder receivePlaceOrder(@NotNull String statusCode) { + return new PlaceOrderReceiveActionBuilder(this, statusCode); + } + + public static class DeleteOrderSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "DELETE"; + + private static final String ENDPOINT = "/api/v3/store/order/{orderId}"; + + private static final String OPERATION_NAME = "deleteOrder"; + + /** + * Constructor with type safe required parameters. + */ + public DeleteOrderSendActionBuilder(StoreApi storeApi, Long orderId) { + super(storeApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("orderId", orderId, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public DeleteOrderSendActionBuilder(String orderIdExpression, StoreApi storeApi) { + super(storeApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("orderId", orderIdExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public DeleteOrderSendActionBuilder(StoreApi storeApi, TestApiClientRequestMessageBuilder messageBuilder, String orderIdExpression) { + super(storeApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("orderId", orderIdExpression, ParameterStyle.SIMPLE, false, false); + } + + public DeleteOrderSendActionBuilder orderId(Long orderId) { + pathParameter("orderId", orderId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public DeleteOrderSendActionBuilder orderId(String orderIdExpression) { + pathParameter("orderId", orderIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class DeleteOrderReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "DELETE"; + + private static final String ENDPOINT = "/api/v3/store/order/{orderId}"; + + private static final String OPERATION_NAME = "deleteOrder"; + + public DeleteOrderReceiveActionBuilder(StoreApi storeApi, String statusCode) { + super(storeApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public DeleteOrderReceiveActionBuilder(StoreApi storeApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(storeApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetInventorySendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/store/inventory"; + + private static final String OPERATION_NAME = "getInventory"; + + @Value("${" + "petstore.base64-encode-api-key:#{false}}") + private boolean base64EncodeApiKey; + + @Value("${" + "petstore.api-key:#{null}}") + private String defaultApiKey; + + private String apiKey; + + /** + * Constructor with type safe required parameters. + */ + public GetInventorySendActionBuilder(StoreApi storeApi) { + super(storeApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + public GetInventorySendActionBuilder(StoreApi storeApi, TestApiClientRequestMessageBuilder messageBuilder) { + super(storeApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + public void setBase64EncodeApiKey(boolean encode) { + this.base64EncodeApiKey = encode; + } + + public GetInventorySendActionBuilder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public SendMessageAction doBuild() { + headerParameter("api_key", getOrDefault(apiKey, defaultApiKey, base64EncodeApiKey)); + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetInventoryReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/store/inventory"; + + private static final String OPERATION_NAME = "getInventory"; + + public GetInventoryReceiveActionBuilder(StoreApi storeApi, String statusCode) { + super(storeApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetInventoryReceiveActionBuilder(StoreApi storeApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(storeApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetOrderByIdSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/store/order/{orderId}"; + + private static final String OPERATION_NAME = "getOrderById"; + + /** + * Constructor with type safe required parameters. + */ + public GetOrderByIdSendActionBuilder(StoreApi storeApi, Long orderId) { + super(storeApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("orderId", orderId, ParameterStyle.SIMPLE, false, false); + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetOrderByIdSendActionBuilder(String orderIdExpression, StoreApi storeApi) { + super(storeApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("orderId", orderIdExpression, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetOrderByIdSendActionBuilder(StoreApi storeApi, TestApiClientRequestMessageBuilder messageBuilder, String orderIdExpression) { + super(storeApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("orderId", orderIdExpression, ParameterStyle.SIMPLE, false, false); + } + + public GetOrderByIdSendActionBuilder orderId(Long orderId) { + pathParameter("orderId", orderId, ParameterStyle.SIMPLE, false, false); + return this; + } + + public GetOrderByIdSendActionBuilder orderId(String orderIdExpression) { + pathParameter("orderId", orderIdExpression, ParameterStyle.SIMPLE, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetOrderByIdReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/store/order/{orderId}"; + + private static final String OPERATION_NAME = "getOrderById"; + + public GetOrderByIdReceiveActionBuilder(StoreApi storeApi, String statusCode) { + super(storeApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetOrderByIdReceiveActionBuilder(StoreApi storeApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(storeApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class PlaceOrderSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/store/order"; + + private static final String OPERATION_NAME = "placeOrder"; + + /** + * Constructor with type safe required parameters. + */ + public PlaceOrderSendActionBuilder(StoreApi storeApi) { + super(storeApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + public PlaceOrderSendActionBuilder(StoreApi storeApi, TestApiClientRequestMessageBuilder messageBuilder) { + super(storeApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + public PlaceOrderSendActionBuilder order(Order order) { + return this; + } + + public void setOrder(Order order) { + } + + public PlaceOrderSendActionBuilder order(String orderExpression) { + return this; + } + + public void setOrder(String orderExpression) { + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class PlaceOrderReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/store/order"; + + private static final String OPERATION_NAME = "placeOrder"; + + public PlaceOrderReceiveActionBuilder(StoreApi storeApi, String statusCode) { + super(storeApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public PlaceOrderReceiveActionBuilder(StoreApi storeApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(storeApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/request/UserApi.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/request/UserApi.java new file mode 100644 index 0000000000..1d8d085cb5 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/request/UserApi.java @@ -0,0 +1,926 @@ +package org.citrusframework.openapi.generator.rest.petstore.request; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.citrusframework.util.StringUtils.isEmpty; +import static org.citrusframework.util.StringUtils.isNotEmpty; + +import static org.citrusframework.openapi.generator.rest.petstore.PetStoreOpenApi.petStoreSpecification; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.net.URL; +import java.time.LocalDate; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.UUID; +import org.citrusframework.actions.ReceiveMessageAction; +import org.citrusframework.actions.SendMessageAction; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +import org.citrusframework.openapi.testapi.GeneratedApiOperationInfo; +import org.citrusframework.openapi.testapi.ParameterStyle; +import org.citrusframework.openapi.testapi.RestApiSendMessageActionBuilder; +import org.citrusframework.openapi.testapi.RestApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.testapi.TestApiUtils; +import org.citrusframework.spi.Resource; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; + +import org.citrusframework.openapi.generator.rest.petstore.PetStoreOpenApi; +import java.time.OffsetDateTime; +import org.citrusframework.openapi.generator.rest.petstore.model.User; + +@SuppressWarnings("unused") +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class UserApi implements GeneratedApi +{ + + @Value("${" + "petstore.base64-encode-api-key:#{false}}") + private boolean base64EncodeApiKey; + + @Value("${" + "petstore.api-key:#{null}}") + private String defaultApiKey; + + private final List customizers; + + /** + * An optional default endpoint which will be passed into the requests. + */ + private final Endpoint defaultEndpoint; + + public UserApi(@Nullable Endpoint defaultEndpoint) { + this(defaultEndpoint, emptyList()); + } + + public UserApi(@Nullable Endpoint defaultEndpoint, @Nullable List customizers) { + this.defaultEndpoint = defaultEndpoint; + this.customizers = customizers; + } + + public static UserApi userApi(Endpoint defaultEndpoint) { + return new UserApi(defaultEndpoint); + } + + @Override + public String getApiTitle() { + return "Swagger Petstore - OpenAPI 3.0"; + } + + @Override + public String getApiVersion() { + return "1.0.19"; + } + + @Override + public String getApiPrefix() { + return "petStore"; + } + + @Override + public Map getApiInfoExtensions() { + return emptyMap(); + } + + @Override + @Nullable + public Endpoint getEndpoint() { + return defaultEndpoint; + } + + @Override + public List getCustomizers() { + return customizers; + } + + /** + * Builder with type safe required parameters. + */ + public CreateUserSendActionBuilder sendCreateUser() { + return new CreateUserSendActionBuilder(this); + } + + public CreateUserReceiveActionBuilder receiveCreateUser(@NotNull HttpStatus statusCode) { + return new CreateUserReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public CreateUserReceiveActionBuilder receiveCreateUser(@NotNull String statusCode) { + return new CreateUserReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public CreateUsersWithListInputSendActionBuilder sendCreateUsersWithListInput() { + return new CreateUsersWithListInputSendActionBuilder(this); + } + + public CreateUsersWithListInputReceiveActionBuilder receiveCreateUsersWithListInput(@NotNull HttpStatus statusCode) { + return new CreateUsersWithListInputReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public CreateUsersWithListInputReceiveActionBuilder receiveCreateUsersWithListInput(@NotNull String statusCode) { + return new CreateUsersWithListInputReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public DeleteUserSendActionBuilder sendDeleteUser(String username) { + return new DeleteUserSendActionBuilder(this, username); + } + + public DeleteUserReceiveActionBuilder receiveDeleteUser(@NotNull HttpStatus statusCode) { + return new DeleteUserReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public DeleteUserReceiveActionBuilder receiveDeleteUser(@NotNull String statusCode) { + return new DeleteUserReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public GetUserByNameSendActionBuilder sendGetUserByName(String username) { + return new GetUserByNameSendActionBuilder(this, username); + } + + public GetUserByNameReceiveActionBuilder receiveGetUserByName(@NotNull HttpStatus statusCode) { + return new GetUserByNameReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public GetUserByNameReceiveActionBuilder receiveGetUserByName(@NotNull String statusCode) { + return new GetUserByNameReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public LoginUserSendActionBuilder sendLoginUser() { + return new LoginUserSendActionBuilder(this); + } + + public LoginUserReceiveActionBuilder receiveLoginUser(@NotNull HttpStatus statusCode) { + return new LoginUserReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public LoginUserReceiveActionBuilder receiveLoginUser(@NotNull String statusCode) { + return new LoginUserReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public LogoutUserSendActionBuilder sendLogoutUser() { + return new LogoutUserSendActionBuilder(this); + } + + public LogoutUserReceiveActionBuilder receiveLogoutUser(@NotNull HttpStatus statusCode) { + return new LogoutUserReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public LogoutUserReceiveActionBuilder receiveLogoutUser(@NotNull String statusCode) { + return new LogoutUserReceiveActionBuilder(this, statusCode); + } + + /** + * Builder with type safe required parameters. + */ + public UpdateUserSendActionBuilder sendUpdateUser(String username) { + return new UpdateUserSendActionBuilder(this, username); + } + + public UpdateUserReceiveActionBuilder receiveUpdateUser(@NotNull HttpStatus statusCode) { + return new UpdateUserReceiveActionBuilder(this, Integer.toString(statusCode.value())); + } + + public UpdateUserReceiveActionBuilder receiveUpdateUser(@NotNull String statusCode) { + return new UpdateUserReceiveActionBuilder(this, statusCode); + } + + public static class CreateUserSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/user"; + + private static final String OPERATION_NAME = "createUser"; + + /** + * Constructor with type safe required parameters. + */ + public CreateUserSendActionBuilder(UserApi userApi) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + public CreateUserSendActionBuilder(UserApi userApi, TestApiClientRequestMessageBuilder messageBuilder) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + public CreateUserSendActionBuilder user(User user) { + return this; + } + + public void setUser(User user) { + } + + public CreateUserSendActionBuilder user(String userExpression) { + return this; + } + + public void setUser(String userExpression) { + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class CreateUserReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/user"; + + private static final String OPERATION_NAME = "createUser"; + + public CreateUserReceiveActionBuilder(UserApi userApi, String statusCode) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public CreateUserReceiveActionBuilder(UserApi userApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class CreateUsersWithListInputSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/user/createWithList"; + + private static final String OPERATION_NAME = "createUsersWithListInput"; + + /** + * Constructor with type safe required parameters. + */ + public CreateUsersWithListInputSendActionBuilder(UserApi userApi) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + public CreateUsersWithListInputSendActionBuilder(UserApi userApi, TestApiClientRequestMessageBuilder messageBuilder) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + public CreateUsersWithListInputSendActionBuilder user(User...user) { + return this; + } + + public void setUser(User...user) { + } + + public CreateUsersWithListInputSendActionBuilder user(String...userExpression) { + return this; + } + + public void setUser(String...userExpression) { + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class CreateUsersWithListInputReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "POST"; + + private static final String ENDPOINT = "/api/v3/user/createWithList"; + + private static final String OPERATION_NAME = "createUsersWithListInput"; + + public CreateUsersWithListInputReceiveActionBuilder(UserApi userApi, String statusCode) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public CreateUsersWithListInputReceiveActionBuilder(UserApi userApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class DeleteUserSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "DELETE"; + + private static final String ENDPOINT = "/api/v3/user/{username}"; + + private static final String OPERATION_NAME = "deleteUser"; + + /** + * Constructor with type safe required parameters. + */ + public DeleteUserSendActionBuilder(UserApi userApi, String username) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("username", username, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public DeleteUserSendActionBuilder(UserApi userApi, TestApiClientRequestMessageBuilder messageBuilder, String usernameExpression) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("username", usernameExpression, ParameterStyle.SIMPLE, false, false); + } + + public DeleteUserSendActionBuilder username(String username) { + pathParameter("username", username, ParameterStyle.SIMPLE, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class DeleteUserReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "DELETE"; + + private static final String ENDPOINT = "/api/v3/user/{username}"; + + private static final String OPERATION_NAME = "deleteUser"; + + public DeleteUserReceiveActionBuilder(UserApi userApi, String statusCode) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public DeleteUserReceiveActionBuilder(UserApi userApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetUserByNameSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/user/{username}"; + + private static final String OPERATION_NAME = "getUserByName"; + + /** + * Constructor with type safe required parameters. + */ + public GetUserByNameSendActionBuilder(UserApi userApi, String username) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("username", username, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public GetUserByNameSendActionBuilder(UserApi userApi, TestApiClientRequestMessageBuilder messageBuilder, String usernameExpression) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("username", usernameExpression, ParameterStyle.SIMPLE, false, false); + } + + public GetUserByNameSendActionBuilder username(String username) { + pathParameter("username", username, ParameterStyle.SIMPLE, false, false); + return this; + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetUserByNameReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/user/{username}"; + + private static final String OPERATION_NAME = "getUserByName"; + + public GetUserByNameReceiveActionBuilder(UserApi userApi, String statusCode) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public GetUserByNameReceiveActionBuilder(UserApi userApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class LoginUserSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/user/login"; + + private static final String OPERATION_NAME = "loginUser"; + + /** + * Constructor with type safe required parameters. + */ + public LoginUserSendActionBuilder(UserApi userApi) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + public LoginUserSendActionBuilder(UserApi userApi, TestApiClientRequestMessageBuilder messageBuilder) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + public LoginUserSendActionBuilder username(String username) { + queryParameter("username", username, ParameterStyle.FORM, true, false); + return this; + } + + public void setUsername(String username) { + queryParameter("username", username, ParameterStyle.FORM, true, false); + } + + public LoginUserSendActionBuilder password(String password) { + queryParameter("password", password, ParameterStyle.FORM, true, false); + return this; + } + + public void setPassword(String password) { + queryParameter("password", password, ParameterStyle.FORM, true, false); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class LoginUserReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/user/login"; + + private static final String OPERATION_NAME = "loginUser"; + + public LoginUserReceiveActionBuilder(UserApi userApi, String statusCode) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public LoginUserReceiveActionBuilder(UserApi userApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class LogoutUserSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/user/logout"; + + private static final String OPERATION_NAME = "logoutUser"; + + /** + * Constructor with type safe required parameters. + */ + public LogoutUserSendActionBuilder(UserApi userApi) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + public LogoutUserSendActionBuilder(UserApi userApi, TestApiClientRequestMessageBuilder messageBuilder) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class LogoutUserReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "GET"; + + private static final String ENDPOINT = "/api/v3/user/logout"; + + private static final String OPERATION_NAME = "logoutUser"; + + public LogoutUserReceiveActionBuilder(UserApi userApi, String statusCode) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public LogoutUserReceiveActionBuilder(UserApi userApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class UpdateUserSendActionBuilder extends + RestApiSendMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "PUT"; + + private static final String ENDPOINT = "/api/v3/user/{username}"; + + private static final String OPERATION_NAME = "updateUser"; + + /** + * Constructor with type safe required parameters. + */ + public UpdateUserSendActionBuilder(UserApi userApi, String username) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("username", username, ParameterStyle.SIMPLE, false, false); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + /** + * Constructor with required parameters as string to allow for dynamic content. + */ + public UpdateUserSendActionBuilder(UserApi userApi, TestApiClientRequestMessageBuilder messageBuilder, String usernameExpression) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + pathParameter("username", usernameExpression, ParameterStyle.SIMPLE, false, false); + } + + public UpdateUserSendActionBuilder username(String username) { + pathParameter("username", username, ParameterStyle.SIMPLE, false, false); + return this; + } + + public UpdateUserSendActionBuilder user(User user) { + return this; + } + + public void setUser(User user) { + } + + public UpdateUserSendActionBuilder user(String userExpression) { + return this; + } + + public void setUser(String userExpression) { + } + + @Override + public SendMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class UpdateUserReceiveActionBuilder extends + RestApiReceiveMessageActionBuilder implements GeneratedApiOperationInfo { + + private static final String METHOD = "PUT"; + + private static final String ENDPOINT = "/api/v3/user/{username}"; + + private static final String OPERATION_NAME = "updateUser"; + + public UpdateUserReceiveActionBuilder(UserApi userApi, String statusCode) { + super(userApi, petStoreSpecification, METHOD, ENDPOINT, OPERATION_NAME, statusCode); + } + + public UpdateUserReceiveActionBuilder(UserApi userApi, OpenApiClientResponseMessageBuilder messageBuilder) { + super(userApi, petStoreSpecification, messageBuilder, messageBuilder.getMessage(), METHOD, ENDPOINT, OPERATION_NAME); + } + + @Override + public String getOperationName() { + return OPERATION_NAME; + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getPath() { + return ENDPOINT; + } + + @Override + public ReceiveMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/spring/PetStoreBeanConfiguration.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/spring/PetStoreBeanConfiguration.java new file mode 100644 index 0000000000..c37e2a3481 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/spring/PetStoreBeanConfiguration.java @@ -0,0 +1,45 @@ +package org.citrusframework.openapi.generator.rest.petstore.spring; + +import static org.citrusframework.openapi.generator.rest.petstore.PetStoreOpenApi.petStoreSpecification; + +import java.util.List; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.OpenApiRepository; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +import org.citrusframework.openapi.generator.rest.petstore.request.PetApi; +import org.citrusframework.openapi.generator.rest.petstore.request.StoreApi; +import org.citrusframework.openapi.generator.rest.petstore.request.UserApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.citrusframework.openapi.generator.rest.petstore.PetStoreOpenApi; + +@Configuration +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class PetStoreBeanConfiguration { + + @Bean + public OpenApiRepository petStoreOpenApiRepository() { + var openApiRepository = new OpenApiRepository(); + openApiRepository.getOpenApiSpecifications().add(petStoreSpecification); + return openApiRepository; + } + + @Bean(name="PetApi") + public PetApi petApi(@Autowired(required = false) @Qualifier("petstore.endpoint") Endpoint defaultEndpoint, @Autowired(required = false) List customizers) { + return new PetApi(defaultEndpoint, customizers); + } + + @Bean(name="StoreApi") + public StoreApi storeApi(@Autowired(required = false) @Qualifier("petstore.endpoint") Endpoint defaultEndpoint, @Autowired(required = false) List customizers) { + return new StoreApi(defaultEndpoint, customizers); + } + + @Bean(name="UserApi") + public UserApi userApi(@Autowired(required = false) @Qualifier("petstore.endpoint") Endpoint defaultEndpoint, @Autowired(required = false) List customizers) { + return new UserApi(defaultEndpoint, customizers); + } + +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/spring/PetStoreNamespaceHandler.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/spring/PetStoreNamespaceHandler.java new file mode 100644 index 0000000000..f441d37cf9 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/rest/petstore/spring/PetStoreNamespaceHandler.java @@ -0,0 +1,158 @@ +package org.citrusframework.openapi.generator.rest.petstore.spring; + +import static org.citrusframework.openapi.generator.rest.petstore.PetStoreOpenApi.petStoreSpecification; + +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.RestApiSendMessageActionBuilder; +import org.citrusframework.openapi.testapi.RestApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.generator.rest.petstore.request.PetApi; +import org.citrusframework.openapi.generator.rest.petstore.request.StoreApi; +import org.citrusframework.openapi.generator.rest.petstore.request.UserApi; +import org.citrusframework.openapi.testapi.spring.RestApiReceiveMessageActionParser; +import org.citrusframework.openapi.testapi.spring.RestApiSendMessageActionParser; +import org.citrusframework.openapi.generator.rest.petstore.PetStoreOpenApi; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:47.794716200+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class PetStoreNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + + registerOperationParsers(PetApi.class,"add-pet", "addPet", "/pet", + PetApi.AddPetSendActionBuilder.class, + PetApi.AddPetReceiveActionBuilder.class, + new String[]{ }, + new String[]{ }); + + registerOperationParsers(PetApi.class,"delete-pet", "deletePet", "/pet/{petId}", + PetApi.DeletePetSendActionBuilder.class, + PetApi.DeletePetReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ "apiKey" }); + + registerOperationParsers(PetApi.class,"find-pets-by-status", "findPetsByStatus", "/pet/findByStatus", + PetApi.FindPetsByStatusSendActionBuilder.class, + PetApi.FindPetsByStatusReceiveActionBuilder.class, + new String[]{ }, + new String[]{ "status" }); + + registerOperationParsers(PetApi.class,"find-pets-by-tags", "findPetsByTags", "/pet/findByTags", + PetApi.FindPetsByTagsSendActionBuilder.class, + PetApi.FindPetsByTagsReceiveActionBuilder.class, + new String[]{ }, + new String[]{ "tags" }); + + registerOperationParsers(PetApi.class,"get-pet-by-id", "getPetById", "/pet/{petId}", + PetApi.GetPetByIdSendActionBuilder.class, + PetApi.GetPetByIdReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ "apiKey" }); + + registerOperationParsers(PetApi.class,"update-pet", "updatePet", "/pet", + PetApi.UpdatePetSendActionBuilder.class, + PetApi.UpdatePetReceiveActionBuilder.class, + new String[]{ }, + new String[]{ }); + + registerOperationParsers(PetApi.class,"update-pet-with-form", "updatePetWithForm", "/pet/{petId}", + PetApi.UpdatePetWithFormSendActionBuilder.class, + PetApi.UpdatePetWithFormReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ "_name", "status" }); + + registerOperationParsers(PetApi.class,"upload-file", "uploadFile", "/pet/{petId}/uploadImage", + PetApi.UploadFileSendActionBuilder.class, + PetApi.UploadFileReceiveActionBuilder.class, + new String[]{ "petId" }, + new String[]{ "additionalMetadata", "ERROR_UNKNOWN" }); + + registerOperationParsers(StoreApi.class,"delete-order", "deleteOrder", "/store/order/{orderId}", + StoreApi.DeleteOrderSendActionBuilder.class, + StoreApi.DeleteOrderReceiveActionBuilder.class, + new String[]{ "orderId" }, + new String[]{ }); + + registerOperationParsers(StoreApi.class,"get-inventory", "getInventory", "/store/inventory", + StoreApi.GetInventorySendActionBuilder.class, + StoreApi.GetInventoryReceiveActionBuilder.class, + new String[]{ }, + new String[]{ "apiKey" }); + + registerOperationParsers(StoreApi.class,"get-order-by-id", "getOrderById", "/store/order/{orderId}", + StoreApi.GetOrderByIdSendActionBuilder.class, + StoreApi.GetOrderByIdReceiveActionBuilder.class, + new String[]{ "orderId" }, + new String[]{ }); + + registerOperationParsers(StoreApi.class,"place-order", "placeOrder", "/store/order", + StoreApi.PlaceOrderSendActionBuilder.class, + StoreApi.PlaceOrderReceiveActionBuilder.class, + new String[]{ }, + new String[]{ "ERROR_UNKNOWN" }); + + registerOperationParsers(UserApi.class,"create-user", "createUser", "/user", + UserApi.CreateUserSendActionBuilder.class, + UserApi.CreateUserReceiveActionBuilder.class, + new String[]{ }, + new String[]{ "ERROR_UNKNOWN" }); + + registerOperationParsers(UserApi.class,"create-users-with-list-input", "createUsersWithListInput", "/user/createWithList", + UserApi.CreateUsersWithListInputSendActionBuilder.class, + UserApi.CreateUsersWithListInputReceiveActionBuilder.class, + new String[]{ }, + new String[]{ "user" }); + + registerOperationParsers(UserApi.class,"delete-user", "deleteUser", "/user/{username}", + UserApi.DeleteUserSendActionBuilder.class, + UserApi.DeleteUserReceiveActionBuilder.class, + new String[]{ "username" }, + new String[]{ }); + + registerOperationParsers(UserApi.class,"get-user-by-name", "getUserByName", "/user/{username}", + UserApi.GetUserByNameSendActionBuilder.class, + UserApi.GetUserByNameReceiveActionBuilder.class, + new String[]{ "username" }, + new String[]{ }); + + registerOperationParsers(UserApi.class,"login-user", "loginUser", "/user/login", + UserApi.LoginUserSendActionBuilder.class, + UserApi.LoginUserReceiveActionBuilder.class, + new String[]{ }, + new String[]{ "username", "password" }); + + registerOperationParsers(UserApi.class,"logout-user", "logoutUser", "/user/logout", + UserApi.LogoutUserSendActionBuilder.class, + UserApi.LogoutUserReceiveActionBuilder.class, + new String[]{ }, + new String[]{ }); + + registerOperationParsers(UserApi.class,"update-user", "updateUser", "/user/{username}", + UserApi.UpdateUserSendActionBuilder.class, + UserApi.UpdateUserReceiveActionBuilder.class, + new String[]{ "username" }, + new String[]{ "ERROR_UNKNOWN" }); + } + + private void registerOperationParsers(Class apiClass, String elementName, String operationName, String path, + Class sendBeanClass, + Class receiveBeanClass, + String[] constructorParameters, + String[] nonConstructorParameters) { + + RestApiSendMessageActionParser sendParser = new RestApiSendMessageActionParser(petStoreSpecification, operationName, + path, + apiClass, + sendBeanClass, + receiveBeanClass, + "petstore.endpoint"); + sendParser.setConstructorParameters(constructorParameters); + sendParser.setNonConstructorParameters(nonConstructorParameters); + registerBeanDefinitionParser("send-"+elementName, sendParser); + + RestApiReceiveMessageActionParser receiveParser = new RestApiReceiveMessageActionParser(petStoreSpecification, + operationName, apiClass, receiveBeanClass, "petstore.endpoint"); + registerBeanDefinitionParser("receive-"+elementName, receiveParser); + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/BookServiceOpenApi.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/BookServiceOpenApi.java new file mode 100644 index 0000000000..1e0bfd40a6 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/BookServiceOpenApi.java @@ -0,0 +1,10 @@ +package org.citrusframework.openapi.generator.soap.bookservice; + +import org.citrusframework.openapi.OpenApiSpecification; + +public class BookServiceOpenApi { + + public static final OpenApiSpecification bookServiceSpecification = OpenApiSpecification + .from(BookServiceOpenApi.class.getResource("BookService_openApi.yaml")); + +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/request/BookServiceSoapApi.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/request/BookServiceSoapApi.java new file mode 100644 index 0000000000..505b022375 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/request/BookServiceSoapApi.java @@ -0,0 +1,311 @@ +package org.citrusframework.openapi.generator.soap.bookservice.request; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; + +import jakarta.annotation.Nullable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.citrusframework.openapi.testapi.GeneratedApiOperationInfo; +import org.citrusframework.ws.actions.ReceiveSoapMessageAction; +import org.citrusframework.ws.actions.SendSoapMessageAction; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.citrusframework.openapi.testapi.SoapApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.testapi.SoapApiSendMessageActionBuilder; + +@SuppressWarnings("unused") +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.920209500+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class BookServiceSoapApi implements GeneratedApi +{ + + /** + * An optional default endpoint which will be passed into the requests. + */ + private final Endpoint defaultEndpoint; + + private final List customizers; + + public BookServiceSoapApi(@Nullable Endpoint defaultEndpoint) { + this(defaultEndpoint, emptyList()); + } + + public BookServiceSoapApi(@Nullable Endpoint defaultEndpoint, @Nullable List customizers) { + this.defaultEndpoint = defaultEndpoint; + this.customizers = customizers; + } + + public static BookServiceSoapApi bookServiceSoapApi(Endpoint defaultEndpoint) { + return new BookServiceSoapApi(defaultEndpoint); + } + + @Override + public String getApiTitle() { + return "Generated api from wsdl"; + } + + @Override + public String getApiVersion() { + return "1.0.0"; + } + + @Override + public String getApiPrefix() { + return "BookService"; + } + + @Override + public Map getApiInfoExtensions() { + return emptyMap(); + } + + @Override + @Nullable + public Endpoint getEndpoint() { + return defaultEndpoint; + } + + @Override + public List getCustomizers() { + return customizers; + } + + public AddBookSendActionBuilder sendAddBook() { + return new AddBookSendActionBuilder(this); + } + + public AddBookReceiveActionBuilder receiveAddBook() { + return new AddBookReceiveActionBuilder(this); + } + + public GetAllBooksSendActionBuilder sendGetAllBooks() { + return new GetAllBooksSendActionBuilder(this); + } + + public GetAllBooksReceiveActionBuilder receiveGetAllBooks() { + return new GetAllBooksReceiveActionBuilder(this); + } + + public GetBookSendActionBuilder sendGetBook() { + return new GetBookSendActionBuilder(this); + } + + public GetBookReceiveActionBuilder receiveGetBook() { + return new GetBookReceiveActionBuilder(this); + } + + public static class AddBookSendActionBuilder extends SoapApiSendMessageActionBuilder implements + GeneratedApiOperationInfo { + + private static final String SOAP_ACTION = "http://www.citrusframework.com/BookService/AddBook"; + + public AddBookSendActionBuilder(BookServiceSoapApi bookServiceSoapApi) { + super(bookServiceSoapApi, SOAP_ACTION); + } + + @Override + public String getOperationName() { + return SOAP_ACTION; + } + + @Override + public String getMethod() { + return "POST"; + } + + @Override + public String getPath() { + return SOAP_ACTION; + } + + @Override + public SendSoapMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class AddBookReceiveActionBuilder extends SoapApiReceiveMessageActionBuilder implements + GeneratedApiOperationInfo { + + private static final String SOAP_ACTION = "http://www.citrusframework.com/BookService/AddBook"; + + public AddBookReceiveActionBuilder(BookServiceSoapApi bookServiceSoapApi) { + super(bookServiceSoapApi, SOAP_ACTION); + } + + @Override + public String getOperationName() { + return SOAP_ACTION; + } + + @Override + public String getMethod() { + return "POST"; + } + + @Override + public String getPath() { + return SOAP_ACTION; + } + + @Override + public ReceiveSoapMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetAllBooksSendActionBuilder extends SoapApiSendMessageActionBuilder implements + GeneratedApiOperationInfo { + + private static final String SOAP_ACTION = "http://www.citrusframework.com/BookService/GetAllBooks"; + + public GetAllBooksSendActionBuilder(BookServiceSoapApi bookServiceSoapApi) { + super(bookServiceSoapApi, SOAP_ACTION); + } + + @Override + public String getOperationName() { + return SOAP_ACTION; + } + + @Override + public String getMethod() { + return "POST"; + } + + @Override + public String getPath() { + return SOAP_ACTION; + } + + @Override + public SendSoapMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetAllBooksReceiveActionBuilder extends SoapApiReceiveMessageActionBuilder implements + GeneratedApiOperationInfo { + + private static final String SOAP_ACTION = "http://www.citrusframework.com/BookService/GetAllBooks"; + + public GetAllBooksReceiveActionBuilder(BookServiceSoapApi bookServiceSoapApi) { + super(bookServiceSoapApi, SOAP_ACTION); + } + + @Override + public String getOperationName() { + return SOAP_ACTION; + } + + @Override + public String getMethod() { + return "POST"; + } + + @Override + public String getPath() { + return SOAP_ACTION; + } + + @Override + public ReceiveSoapMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } + + public static class GetBookSendActionBuilder extends SoapApiSendMessageActionBuilder implements + GeneratedApiOperationInfo { + + private static final String SOAP_ACTION = "http://www.citrusframework.com/BookService/GetBook"; + + public GetBookSendActionBuilder(BookServiceSoapApi bookServiceSoapApi) { + super(bookServiceSoapApi, SOAP_ACTION); + } + + @Override + public String getOperationName() { + return SOAP_ACTION; + } + + @Override + public String getMethod() { + return "POST"; + } + + @Override + public String getPath() { + return SOAP_ACTION; + } + + @Override + public SendSoapMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeRequestBuilder(this, this)); + } + + return super.doBuild(); + } + } + + public static class GetBookReceiveActionBuilder extends SoapApiReceiveMessageActionBuilder implements + GeneratedApiOperationInfo { + + private static final String SOAP_ACTION = "http://www.citrusframework.com/BookService/GetBook"; + + public GetBookReceiveActionBuilder(BookServiceSoapApi bookServiceSoapApi) { + super(bookServiceSoapApi, SOAP_ACTION); + } + + @Override + public String getOperationName() { + return SOAP_ACTION; + } + + @Override + public String getMethod() { + return "POST"; + } + + @Override + public String getPath() { + return SOAP_ACTION; + } + + @Override + public ReceiveSoapMessageAction doBuild() { + + if (getCustomizers() != null) { + getCustomizers().forEach(customizer -> customizer.customizeResponseBuilder(this, this)); + } + + return super.doBuild(); + } + + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/spring/BookServiceBeanConfiguration.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/spring/BookServiceBeanConfiguration.java new file mode 100644 index 0000000000..f27f214ca1 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/spring/BookServiceBeanConfiguration.java @@ -0,0 +1,33 @@ +package org.citrusframework.openapi.generator.soap.bookservice.spring; + +import static org.citrusframework.openapi.generator.soap.bookservice.BookServiceOpenApi.bookServiceSpecification; + +import java.util.List; +import org.citrusframework.endpoint.Endpoint; +import org.citrusframework.openapi.OpenApiRepository; +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.ApiActionBuilderCustomizer; +import org.citrusframework.openapi.generator.soap.bookservice.request.BookServiceSoapApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.citrusframework.openapi.generator.soap.bookservice.BookServiceOpenApi; + +@Configuration +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.920209500+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class BookServiceBeanConfiguration { + + @Bean + public OpenApiRepository bookServiceOpenApiRepository() { + var openApiRepository = new OpenApiRepository(); + openApiRepository.getOpenApiSpecifications().add(bookServiceSpecification); + return openApiRepository; + } + + @Bean(name="BookServiceSoapApi") + public BookServiceSoapApi bookServiceSoapApi(@Autowired(required = false) @Qualifier("bookstore.endpoint") Endpoint defaultEndpoint, @Autowired(required = false) List customizers) { + return new BookServiceSoapApi(defaultEndpoint, customizers); + } + +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/spring/BookServiceNamespaceHandler.java b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/spring/BookServiceNamespaceHandler.java new file mode 100644 index 0000000000..36eac4ee50 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/ExpectedCodeGenIT/expectedgen/soap/bookservice/spring/BookServiceNamespaceHandler.java @@ -0,0 +1,47 @@ +package org.citrusframework.openapi.generator.soap.bookservice.spring; + +import org.citrusframework.openapi.OpenApiSpecification; +import org.citrusframework.openapi.testapi.SoapApiSendMessageActionBuilder; +import org.citrusframework.openapi.testapi.SoapApiReceiveMessageActionBuilder; +import org.citrusframework.openapi.generator.soap.bookservice.request.BookServiceSoapApi; +import org.citrusframework.openapi.testapi.spring.SoapApiReceiveMessageActionParser; +import org.citrusframework.openapi.testapi.spring.SoapApiSendMessageActionParser; +import org.citrusframework.openapi.generator.soap.bookservice.BookServiceOpenApi; +import org.citrusframework.openapi.testapi.GeneratedApi; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +@jakarta.annotation.Generated(value = "org.citrusframework.openapi.generator.CitrusJavaCodegen", date = "2025-01-29T23:14:48.920209500+01:00[Europe/Zurich]", comments = "Generator version: 7.9.0") +public class BookServiceNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + + registerOperationParsers(BookServiceSoapApi.class,"add-book", + BookServiceSoapApi.AddBookSendActionBuilder.class, + BookServiceSoapApi.AddBookReceiveActionBuilder.class); + + registerOperationParsers(BookServiceSoapApi.class,"get-all-books", + BookServiceSoapApi.GetAllBooksSendActionBuilder.class, + BookServiceSoapApi.GetAllBooksReceiveActionBuilder.class); + + registerOperationParsers(BookServiceSoapApi.class,"get-book", + BookServiceSoapApi.GetBookSendActionBuilder.class, + BookServiceSoapApi.GetBookReceiveActionBuilder.class); + } + + private void registerOperationParsers(Class apiClass, String elementName, + Class sendBeanClass, + Class receiveBeanClass) { + + SoapApiSendMessageActionParser sendParser = new SoapApiSendMessageActionParser( + apiClass, + sendBeanClass, + receiveBeanClass, + "bookstore.endpoint"); + registerBeanDefinitionParser("send-"+elementName, sendParser); + + SoapApiReceiveMessageActionParser receiveParser = new SoapApiReceiveMessageActionParser( + apiClass, receiveBeanClass, "bookstore.endpoint"); + registerBeanDefinitionParser("receive-"+elementName, receiveParser); + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/AdditionalData.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/AdditionalData.json new file mode 100644 index 0000000000..a921a4b0c2 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/AdditionalData.json @@ -0,0 +1,6 @@ +{ + "Konto": { + "iban": "DE43100500000920018963", + "amount": 1234 + } +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/MultipartTemplate.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/MultipartTemplate.xml new file mode 100644 index 0000000000..a3dd52a043 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/MultipartTemplate.xml @@ -0,0 +1,4 @@ + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/Schema.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/Schema.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/Schema.json @@ -0,0 +1 @@ +{} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/addPetMessage.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/addPetMessage.json new file mode 100644 index 0000000000..b68ed32a5e --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/addPetMessage.json @@ -0,0 +1,6 @@ +{ + "id": 12, + "name": "Snoopy", + "tags": ["comic dog"], + "photoUrls": ["url1", "url2"] +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/findPetsByStatus_response.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/findPetsByStatus_response.json new file mode 100644 index 0000000000..4e3176fe31 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/findPetsByStatus_response.json @@ -0,0 +1,38 @@ +[ + { + "id": 1, + "name": "hasso", + "category": { + "id": 2, + "name": "citrus:randomEnumValue('dog', 'cat', 'fish')" + }, + "photoUrls": [ + "http://localhost:8080/photos/${petId}" + ], + "tags": [ + { + "id": 3, + "name": "generated" + } + ], + "status": "citrus:randomEnumValue('available', 'pending', 'sold')" + }, + { + "id": 4, + "name": "fluffy", + "category": { + "id": 5, + "name": "citrus:randomEnumValue('dog', 'cat', 'fish')" + }, + "photoUrls": [ + "http://localhost:8080/photos/${petId}" + ], + "tags": [ + { + "id": 6, + "name": "generated" + } + ], + "status": "citrus:randomEnumValue('available', 'pending', 'sold')" + } +] \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetByIdControlMessage1.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetByIdControlMessage1.json new file mode 100644 index 0000000000..b68ed32a5e --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetByIdControlMessage1.json @@ -0,0 +1,6 @@ +{ + "id": 12, + "name": "Snoopy", + "tags": ["comic dog"], + "photoUrls": ["url1", "url2"] +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetByIdControlMessage2.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetByIdControlMessage2.json new file mode 100644 index 0000000000..267e7887a0 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetByIdControlMessage2.json @@ -0,0 +1,6 @@ +{ + "id": 12, + "name": "Garfield", + "tags": ["comic cat"], + "photoUrls": ["url1", "url2"] +} diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json new file mode 100644 index 0000000000..53fb68a708 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response.json @@ -0,0 +1,18 @@ +{ + "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')" +} \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response_validation.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response_validation.json new file mode 100644 index 0000000000..38b4703838 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/getPetById_response_validation.json @@ -0,0 +1,18 @@ +{ + "id": ${petId}, + "name": "@matches('hasso|cutie|fluffy')@", + "category": { + "id": ${petId}, + "name": "@matches('dog|cat|fish')@" + }, + "photoUrls": [ + "http://localhost:8080/photos/${petId}" + ], + "tags": [ + { + "id": ${petId}, + "name": "generated" + } + ], + "status": "@matches('available|pending|sold')@" +} \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/invalidGetPetById_response.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/invalidGetPetById_response.json new file mode 100644 index 0000000000..c1657e0b6f --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/invalidGetPetById_response.json @@ -0,0 +1,14 @@ +{ + "category": { + "name": "citrus:randomEnumValue('dog', 'cat', 'fish')" + }, + "photoUrls": [ + "http://localhost:8080/photos/${petId}" + ], + "tags": [ + { + "name": "generated" + } + ], + "status": "citrus:randomEnumValue('available', 'pending', 'sold')" +} \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/pet.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/pet.json new file mode 100644 index 0000000000..0d4e504a8f --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/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/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/thisAintNoPed.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/thisAintNoPed.json new file mode 100644 index 0000000000..9414dcaab0 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/thisAintNoPed.json @@ -0,0 +1 @@ +{"description": "this ain't no ped"} \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationAdditionalData.json b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationAdditionalData.json new file mode 100644 index 0000000000..c1bc78ed04 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationAdditionalData.json @@ -0,0 +1 @@ +{"lastVaccinationDate": "2024-04-02","vaccinationCount": 5} \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationReport.pdf b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationReport.pdf new file mode 100644 index 0000000000..fe879fe28c Binary files /dev/null and b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationReport.pdf differ diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationTemplate.bin b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationTemplate.bin new file mode 100644 index 0000000000..82090ee2cb --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/payloads/vaccinationTemplate.bin @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withActorTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withActorTest.xml new file mode 100644 index 0000000000..ed2765f31b --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withActorTest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withApiCookieTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withApiCookieTest.xml new file mode 100644 index 0000000000..a051f6d1fc --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withApiCookieTest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withApiKeysFromPropertiesTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withApiKeysFromPropertiesTest.xml new file mode 100644 index 0000000000..e9ddb58090 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withApiKeysFromPropertiesTest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withApiKeysOverridingPropertiesTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withApiKeysOverridingPropertiesTest.xml new file mode 100644 index 0000000000..43b0053054 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withApiKeysOverridingPropertiesTest.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withArrayQueryDataTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withArrayQueryDataTest.xml new file mode 100644 index 0000000000..d2f4a05551 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withArrayQueryDataTest.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + tag1 + ${tag2} + ${nick1} + ${nick2} + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicAuthenticationFromPropertiesTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicAuthenticationFromPropertiesTest.xml new file mode 100644 index 0000000000..770080fb5e --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicAuthenticationFromPropertiesTest.xml @@ -0,0 +1,76 @@ + + + + + + + + + name + previous-owner + MrX + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicAuthenticationOverridingPropertiesTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicAuthenticationOverridingPropertiesTest.xml new file mode 100644 index 0000000000..21a4f9ee0f --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicAuthenticationOverridingPropertiesTest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicBearerAuthenticationFromPropertiesTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicBearerAuthenticationFromPropertiesTest.xml new file mode 100644 index 0000000000..1645d3348f --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicBearerAuthenticationFromPropertiesTest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicBearerAuthenticationOverridingPropertiesTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicBearerAuthenticationOverridingPropertiesTest.xml new file mode 100644 index 0000000000..1b84deaa98 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBasicBearerAuthenticationOverridingPropertiesTest.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBodyAsPlainTextTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBodyAsPlainTextTest.xml new file mode 100644 index 0000000000..d5555d4947 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBodyAsPlainTextTest.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBodyFromResourceTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBodyFromResourceTest.xml new file mode 100644 index 0000000000..cf2290bf08 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withBodyFromResourceTest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnBodyDataValidationTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnBodyDataValidationTest.xml new file mode 100644 index 0000000000..10f6bd7964 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnBodyDataValidationTest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"description": "no pet"} + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnBodyResourceValidationTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnBodyResourceValidationTest.xml new file mode 100644 index 0000000000..93ccf20f5e --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnBodyResourceValidationTest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnInvalidResponseTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnInvalidResponseTest.xml new file mode 100644 index 0000000000..437a5f5af1 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnInvalidResponseTest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnJsonPathInvalidTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnJsonPathInvalidTest.xml new file mode 100644 index 0000000000..685ba5e94c --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnJsonPathInvalidTest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnReasonPhraseTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnReasonPhraseTest.xml new file mode 100644 index 0000000000..2e735d0b00 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnReasonPhraseTest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnStatusTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnStatusTest.xml new file mode 100644 index 0000000000..9a198cf628 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnStatusTest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnVersionTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnVersionTest.xml new file mode 100644 index 0000000000..1eae539d76 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFailOnVersionTest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFileUploadTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFileUploadTest.xml new file mode 100644 index 0000000000..4131a3c0b5 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFileUploadTest.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + ${file} + + + + + + + + + [102, 105, 108, 101, 100, 97, 116, 97] + + + + + + + + + + + + {"code": 12, "type":"post-image-ok", "message":"image successfully uploaded"} + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFormDataTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFormDataTest.xml new file mode 100644 index 0000000000..86b439bd74 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFormDataTest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFormUrlEncodedTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFormUrlEncodedTest.xml new file mode 100644 index 0000000000..b0957d3149 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withFormUrlEncodedTest.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + tag1 + ${tag2} + ${nick1} + ${nick2} + ^`{|}~ }rd]]> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withJsonPathExtractionTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withJsonPathExtractionTest.xml new file mode 100644 index 0000000000..69b88deed9 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withJsonPathExtractionTest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String contentType = context.getVariable("varContentType") + assert contentType == "application/json" : "Expected variable 'varContentType' value 'application/json' but was '${varContentType}'" + + String name = context.getVariable("varName") + List allowedNames = ["cutie", "fluffy", "hasso"] + if (!allowedNames.contains(name)) { + throw new AssertionError("Expected variable 'varName' to be one of " + + allowedNames.join(", ") + " but was '" + name + "'") + } + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withJsonPathValidationTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withJsonPathValidationTest.xml new file mode 100644 index 0000000000..2ae560b2f8 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withJsonPathValidationTest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withMultipartTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withMultipartTest.xml new file mode 100644 index 0000000000..8354c20185 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withMultipartTest.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + import static org.assertj.core.api.Assertions.assertThat + import static java.util.Map.of + import static org.citrusframework.openapi.generator.util.MultipartConverter.multipartMessageToMa + + def multiMap = org.citrusframework.openapi.generator.util.MultipartConverter.multipartMessageToMap((org.citrusframework.http.message.HttpMessage) receivedMessage) + + org.assertj.core.api.Assertions.assertThat(multiMap) + .containsExactlyInAnyOrderEntriesOf(java.util.Map.of( + "additionalData", """${additionalData}""", + "reqIntVal", "1", + "template", new byte[]{1,2,3,4}, + "optIntVal", "100", + "optBoolVal","true", + "optDateVal","2024-12-1", + "optNumberVal","1", + "optStringVal","a")); + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveDefaultClientSoapTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveDefaultClientSoapTest.xml new file mode 100644 index 0000000000..a7f65cbb36 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveDefaultClientSoapTest.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + J.R. Tolkien + 0815 + Lord of the Rings + + + + + + + + + J.R. Tolkien + 0815 + Lord of the Rings + + + + + + + + + + + + J.R. Tolkien + 0815 + Lord of the Rings + + + + + + + + + + + J.R. Tolkien + 0815 + Lord of the Rings + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveInXmlWithDefaultClientTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveInXmlWithDefaultClientTest.xml new file mode 100644 index 0000000000..f22a908920 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveInXmlWithDefaultClientTest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveInXmlWithOtherClientTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveInXmlWithOtherClientTest.xml new file mode 100644 index 0000000000..0e8dc314d1 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveInXmlWithOtherClientTest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveInXmlWithUriTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveInXmlWithUriTest.xml new file mode 100644 index 0000000000..74a9cc425d --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNestedReceiveInXmlWithUriTest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNonApiQueryParamTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNonApiQueryParamTest.xml new file mode 100644 index 0000000000..a83d2e6859 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withNonApiQueryParamTest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withReceiveBodyFromPlainTextTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withReceiveBodyFromPlainTextTest.xml new file mode 100644 index 0000000000..4be7dccdc7 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withReceiveBodyFromPlainTextTest.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withReceiveBodyFromResourceTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withReceiveBodyFromResourceTest.xml new file mode 100644 index 0000000000..2b878dff5f --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withReceiveBodyFromResourceTest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withReceiveNonApiCookieTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withReceiveNonApiCookieTest.xml new file mode 100644 index 0000000000..eaef099f0a --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withReceiveNonApiCookieTest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withResponseValidationDisabledTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withResponseValidationDisabledTest.xml new file mode 100644 index 0000000000..8bc1df7b68 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withResponseValidationDisabledTest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withSoapTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withSoapTest.xml new file mode 100644 index 0000000000..f253752b0b --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withSoapTest.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + J.R. Tolkien + 0815 + Lord of the Rings + + + + + + + + + + J.R. Tolkien + 0815 + Lord of the Rings + + + + + + + + + + + J.R. Tolkien + 0815 + Lord of the Rings + + + + + + + + + + + + J.R. Tolkien + 0815 + Lord of the Rings + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withSpecificEndpointTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withSpecificEndpointTest.xml new file mode 100644 index 0000000000..cc1c66036d --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withSpecificEndpointTest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withSpecificUriTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withSpecificUriTest.xml new file mode 100644 index 0000000000..b83b3ee399 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withSpecificUriTest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withoutOperationIdTest.xml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withoutOperationIdTest.xml new file mode 100644 index 0000000000..1a7ce6d1f5 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/GeneratedApiTest/withoutOperationIdTest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookDatatypes.xsd b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookDatatypes.xsd new file mode 100644 index 0000000000..3a735e7dd8 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookDatatypes.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookService-generated.yaml b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookService-generated.yaml new file mode 100644 index 0000000000..f70656312d --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookService-generated.yaml @@ -0,0 +1,43 @@ +--- +info: + contact: + name: "org.citrusframework.openapi.generator.SimpleWsdlToOpenApiTransformer" + description: "This api has been generated from the following wsdl 'BookService.wsdl'.\ + \ It's purpose is solely to serve as input for SOAP API generation. Note that\ + \ only operations are extracted from the WSDL. No schema information whatsoever\ + \ is generated!" + title: "Generated api from wsdl" + version: "1.0.0" +openapi: "3.0.1" +paths: + /GetBook: + post: + description: "This operation retrieves details for a specific book identified\ + \ by its ID." + operationId: "GetBook" + responses: + default: + description: "Generic Response" + summary: "http://www.citrusframework.com/BookService/GetBook" + tags: + - "BookServiceSOAP" + /AddBook: + post: + description: "" + operationId: "AddBook" + responses: + default: + description: "Generic Response" + summary: "http://www.citrusframework.com/BookService/AddBook" + tags: + - "BookServiceSOAP" + /GetAllBooks: + post: + description: "" + operationId: "GetAllBooks" + responses: + default: + description: "Generic Response" + summary: "http://www.citrusframework.com/BookService/GetAllBooks" + tags: + - "BookServiceSOAP" \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookService.wsdl b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookService.wsdl new file mode 100644 index 0000000000..d16857172e --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-core/src/test/resources/org/citrusframework/openapi/generator/SimpleWsdlToOpenApiTransformerTest/BookService.wsdl @@ -0,0 +1,113 @@ + + + Definition for a web service called BookService, + which can be used to add or retrieve books from a collection. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This binding defines the SOAP over HTTP transport for BookService operations. + + + This operation retrieves details for a specific book identified by its ID. + + Detailed Soap Operation documentation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/pom.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/pom.xml new file mode 100644 index 0000000000..34b7aca69c --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/pom.xml @@ -0,0 +1,143 @@ + + 4.0.0 + + + citrus-test-api-generator + org.citrusframework + 4.6.0-SNAPSHOT + ../pom.xml + + + citrus-test-api-generator-maven-plugin + maven-plugin + + Citrus :: Test API Generator :: Maven Plugin + Maven Plugin for generation of Citrus Test API + + + 2.2.21 + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.9.0 + + true + + + + mojo-descriptor + process-classes + + descriptor + + + + help-goal + + helpmojo + + + + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + ${maven.plugin.plugin.version} + + true + + + + + + + + + org.citrusframework + citrus-test-api-generator-core + ${project.version} + + + org.openapitools + openapi-generator + ${org.openapitools.version} + compile + + + org.openapitools + openapi-generator-maven-plugin + ${org.openapitools.version} + + + commons-io + commons-io + ${commons.io.version} + + + io.swagger.core.v3 + swagger-core + ${swagger.version} + + + io.swagger.core.v3 + swagger-models-jakarta + ${swagger.version} + + + org.apache.maven + maven-artifact + ${maven.version} + provided + + + org.apache.maven + maven-core + ${maven.version} + provided + + + org.apache.maven + maven-plugin-api + ${maven.version} + provided + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${maven.plugin.annotations.version} + compile + + + + + org.apache.maven + maven-compat + ${maven.version} + test + + + org.apache.maven.plugin-testing + maven-plugin-testing-harness + ${maven.plugin.testing.harness.version} + test + + + org.junit.jupiter + junit-jupiter-params + test + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/main/java/org/citrusframework/maven/plugin/CodeGenMojoWrapper.java b/test-api-generator/citrus-test-api-generator-maven-plugin/src/main/java/org/citrusframework/maven/plugin/CodeGenMojoWrapper.java new file mode 100644 index 0000000000..9e89b3409d --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/main/java/org/citrusframework/maven/plugin/CodeGenMojoWrapper.java @@ -0,0 +1,211 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.maven.plugin; + +import static java.lang.String.format; +import static org.citrusframework.openapi.generator.CitrusJavaCodegen.CODEGEN_NAME; +import static org.citrusframework.openapi.generator.CitrusJavaCodegen.GENERATED_SCHEMA_FOLDER; +import static org.citrusframework.openapi.generator.CitrusJavaCodegen.ROOT_CONTEXT_PATH; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import org.apache.maven.model.Resource; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.project.MavenProject; +import org.openapitools.codegen.plugin.CodeGenMojo; + +/** + * Wrapper class that uses reflection to expose several properties of the {@link CodeGenMojo} for + * explicit assignment. + */ +public class CodeGenMojoWrapper extends CodeGenMojo { + + private MavenProject localMavenProject; + + @SuppressWarnings("rawtypes") + private final Map configOptionsProperties = new HashMap<>(); + + public CodeGenMojoWrapper() throws MojoExecutionException { + setFixedConfigOptions(); + setPrivateField("configOptions", configOptionsProperties); + } + + @Override + public void execute() throws MojoExecutionException { + super.execute(); + addResourceRootIfConfigured(); + } + + private void addResourceRootIfConfigured() throws MojoExecutionException { + Resource resource = new Resource(); + resource.setDirectory(getResourceRoot()); + resource.addInclude("**/*"); // Include all files by default + resource.setFiltering(false); // Ensure no filtering of resources + + if (getPrivateField("addCompileSourceRoot")) { + localMavenProject.addResource(resource); + } else if (getPrivateField("addTestCompileSourceRoot")) { + localMavenProject.addTestResource(resource); + } + } + + private String getResourceRoot() throws MojoExecutionException { + final Object resourceFolderObject = configOptionsProperties + .get(TestApiGeneratorMojo.RESOURCE_FOLDER); + final String resourceFolder = + resourceFolderObject != null ? resourceFolderObject.toString() : "src/main/resources"; + + return ((File) getPrivateField("output")).getPath() + File.separatorChar + resourceFolder; + } + + + private void setFixedConfigOptions() throws MojoExecutionException { + setPrivateField("generateSupportingFiles", true); + setPrivateField("generatorName", CODEGEN_NAME); + } + + public CodeGenMojoWrapper project(MavenProject mavenProject) throws MojoExecutionException { + this.localMavenProject = mavenProject; + setPrivateField("project", mavenProject); + return this; + } + + public CodeGenMojoWrapper output(File output) throws MojoExecutionException { + setPrivateField("output", output); + return this; + } + + public CodeGenMojoWrapper inputSpec(String inputSpec) throws MojoExecutionException { + setPrivateField("inputSpec", inputSpec); + return this; + } + + public CodeGenMojoWrapper mojoExecution(MojoExecution mojoExecution) + throws MojoExecutionException { + setPrivateField("mojo", mojoExecution); + return this; + } + + public CodeGenMojoWrapper configOptions( + @SuppressWarnings("rawtypes") Map configProperties) throws MojoExecutionException { + //noinspection unchecked + configOptionsProperties.putAll(configProperties); + + propagateContextPathToCodegen(); + propagateMojoConfigurationParameters(configProperties); + return this; + } + + /** + * In version 7.9 of the code generator, the basePath cannot be configured directly. + * Additionally, if the OpenAPI server specifies a hostname, that hostname becomes part of the + * basePath. This behavior is incompatible with the API generator, as the host is already + * provided by the Citrus endpoint. Therefore, the contextPath is used instead of the basePath. + * The contextPath is passed to the code generator via additional-properties to ensure proper + * configuration. + */ + private void propagateContextPathToCodegen() { + // Pass in contextPath as contextPath into the generator + if (configOptionsProperties.containsKey(ROOT_CONTEXT_PATH)) { + // Note that an null value indicates "no context path". + String contextPath = (String) configOptionsProperties.get(ROOT_CONTEXT_PATH); + contextPath = contextPath == null ? "" : contextPath; + + // Additional properties are stored as comma separated key-value pairs. + // See org.openapitools.codegen.config.CodegenConfiguratorUtils.applyAdditionalPropertiesKvp for details. + //noinspection unchecked + configOptionsProperties.put("additional-properties", + "rootContextPath=" + contextPath); + } + } + + private void propagateMojoConfigurationParameters(Map configProperties) + throws MojoExecutionException { + + for (Field field : CodeGenMojo.class.getDeclaredFields()) { + String name = field.getName(); + if (configProperties.containsKey(name)) { + String valueAsString = configProperties.get(name); + Object value; + if (valueAsString != null) { + if (field.getType() == String.class) { + value = valueAsString; + } else if (field.getType() == Boolean.class || field.getType() == boolean.class) { + value = Boolean.valueOf(valueAsString); + } else if (field.getType() == File.class) { + value = new File(valueAsString); + } else { + throw new IllegalArgumentException( + format("Cannot convert '%s' to type '%s'", valueAsString, + field.getType())); + } + setPrivateField(name, value); + } + } + } + } + + public CodeGenMojoWrapper globalProperties(@SuppressWarnings("rawtypes") Map globalProperties) { + //noinspection unchecked + this.globalProperties.putAll(globalProperties); + return this; + } + + public CodeGenMojoWrapper schemaFolder(String schemaFolder) { + //noinspection unchecked + configOptionsProperties.put(GENERATED_SCHEMA_FOLDER, schemaFolder); + return this; + } + + // Accessibility bypass + @SuppressWarnings("java:S3011") + private void setPrivateField(String fieldName, Object fieldValue) + throws MojoExecutionException { + try { + var field = CodeGenMojo.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(this, fieldValue); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new MojoExecutionException( + format("Could not reflectively set field value '%s' for field '%s'", fieldValue, + fieldName)); + } + } + + // Accessibility bypass + @SuppressWarnings("java:S3011") + private T getPrivateField(String fieldName) + throws MojoExecutionException { + try { + var field = CodeGenMojo.class.getDeclaredField(fieldName); + field.setAccessible(true); + //noinspection unchecked + return (T) field.get(this); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new MojoExecutionException( + format("Could not reflectively get value for field '%s'", fieldName)); + } + } + + public CodeGenMojoWrapper skip(Boolean skip) throws MojoExecutionException { + setPrivateField("skip", skip); + return this; + } +} diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/main/java/org/citrusframework/maven/plugin/SpringMetaFileGenerator.java b/test-api-generator/citrus-test-api-generator-maven-plugin/src/main/java/org/citrusframework/maven/plugin/SpringMetaFileGenerator.java new file mode 100644 index 0000000000..6cf9ec3c6a --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/main/java/org/citrusframework/maven/plugin/SpringMetaFileGenerator.java @@ -0,0 +1,172 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.maven.plugin; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.CITRUS_TEST_SCHEMA; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.CITRUS_TEST_SCHEMA_KEEP_HINT; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.replaceDynamicVars; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.replaceDynamicVarsToLowerCase; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import org.apache.maven.plugin.MojoExecutionException; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.maven.plugin.TestApiGeneratorMojo.ApiConfig; + +/** + * Utility class responsible for generating the Spring meta files 'spring.handlers' and 'spring.schemas', used + * in Spring integration testing. These meta files define mappings between XML namespace URIs and corresponding + * handler classes. The class provides methods to generate these meta files based on the configuration provided. + *

      + * The generated meta files can be created either in the generated folder or in the main resources folder. See + * {@link TestApiGeneratorMojo#RESOURCE_FOLDER_PROPERTY} for details. The implemented algorithm carefully updates these + * files and tries to keep non generated information unchanged. Therefore, a special segment in the namespace uri is used, namely + * {@link TestApiGeneratorMojo#CITRUS_TEST_SCHEMA}. + *

      + */ +public class SpringMetaFileGenerator { + + private final TestApiGeneratorMojo testApiGeneratorMojo; + + private String lineEnding = "\n"; + + public SpringMetaFileGenerator(TestApiGeneratorMojo testApiGeneratorMojo) { + this.testApiGeneratorMojo = testApiGeneratorMojo; + } + + /** + * Reads the lines from the specified file and filters out lines indicating a generated test API, + * while maintaining all non-generated test API lines. This method is used to process files + * containing both generated and non-generated test APIs, allowing seamless integration and + * modification of both types of APIs in the same source files. + * + *

      + * Generated test API lines are identified by the presence of the {@code CITRUS_TEST_SCHEMA} + * string and excluded from the output of this method, while all other lines are preserved. + * This enables the algorithm to operate on files that are not purely generated, for example, + * when mixing generated with non-generated APIs in 'src/main/META-INF'. + *

      + * + * @param file the file to read and filter + * @return a list of filtered lines, excluding lines indicating a generated test API + * @throws CitrusRuntimeException if an error occurs while reading the file + */ + private List readAndFilterLines(File file) { + if (!file.exists()) { + return emptyList(); + } + + List filteredLines = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + + if (lineEnding.contains("\r")) { + lineEnding = "\r\n"; + } + + if (line.contains(CITRUS_TEST_SCHEMA_KEEP_HINT)) { + filteredLines.add(reader.readLine()); + } else if (!line.contains(CITRUS_TEST_SCHEMA)) { + filteredLines.add(line); + } + } + } catch (IOException e) { + throw new CitrusRuntimeException(format("Unable to read file file: '%s'", file.getPath()), e); + } + + return filteredLines; + } + + public void generateSpringIntegrationMetaFiles() throws MojoExecutionException { + + File metaFolder = new File(testApiGeneratorMojo.getMavenProject().getBasedir(), testApiGeneratorMojo.getMetaInfFolder()); + + if (!metaFolder.exists() && !metaFolder.mkdirs()) { + throw new CitrusRuntimeException( + format("Unable to create spring meta file directory: '%s'", testApiGeneratorMojo.getMetaInfFolder())); + } + + try { + writeSpringSchemaMetaFile(metaFolder); + writeSpringHandlerMetaFile(metaFolder); + } catch (MetaFileWriteException e) { + throw new MojoExecutionException(e); + } + } + + private void writeSpringSchemaMetaFile(File springMetafileDirectory) throws MojoExecutionException { + String filename = "spring.schemas"; + writeSpringMetaFile(springMetafileDirectory, filename, (fileWriter, apiConfig) -> { + String targetXmlnsNamespace = replaceDynamicVarsToLowerCase(apiConfig.getTargetXmlnsNamespace(), apiConfig.getPrefix(), apiConfig.getVersion()); + String schemaFolderPath = replaceDynamicVars(testApiGeneratorMojo.schemaFolder(apiConfig), apiConfig.getPrefix(), apiConfig.getVersion()); + String schemaPath = format("%s/%s-api.xsd", schemaFolderPath, apiConfig.getPrefix().toLowerCase()); + appendLine(fileWriter, format("%s.xsd=%s%n", targetXmlnsNamespace.replace("http://", "http\\://"), schemaPath), filename); + }); + } + + private void writeSpringHandlerMetaFile(File springMetafileDirectory) throws MojoExecutionException { + String filename = "spring.handlers"; + writeSpringMetaFile(springMetafileDirectory, filename, (fileWriter, apiConfig) -> { + String targetXmlnsNamespace = replaceDynamicVarsToLowerCase(apiConfig.getTargetXmlnsNamespace(), apiConfig.getPrefix(), apiConfig.getVersion()); + String invokerPackage = replaceDynamicVarsToLowerCase(apiConfig.getInvokerPackage(), apiConfig.getPrefix(), apiConfig.getVersion()); + String namespaceHandlerClass = invokerPackage + ".spring." + apiConfig.getPrefix() + "NamespaceHandler"; + appendLine(fileWriter, format("%s=%s%n", targetXmlnsNamespace.replace("http://", "http\\://"), namespaceHandlerClass), filename); + }); + } + + private void writeSpringMetaFile(File springMetafileDirectory, String filename, BiConsumer contentFormatter) throws MojoExecutionException { + File handlerFile = new File(format("%s/%s", springMetafileDirectory.getPath(), filename)); + List filteredLines = readAndFilterLines(handlerFile); + + try (FileWriter fileWriter = new FileWriter(handlerFile)) { + for (String line : filteredLines) { + fileWriter.write(format("%s%s", line, lineEnding)); + } + + for (ApiConfig apiConfig : testApiGeneratorMojo.getApiConfigs()) { + contentFormatter.accept(fileWriter, apiConfig); + } + + } catch (IOException e) { + throw new MojoExecutionException("Unable to write spring meta file!", e); + } + } + + private void appendLine(FileWriter fileWriter, String format, String filename) { + try { + fileWriter.append(format); + } catch (IOException e) { + throw new MetaFileWriteException(format("Unable to write spring meta file '%s'!", filename), e); + } + } + + private static final class MetaFileWriteException extends RuntimeException { + + public MetaFileWriteException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/main/java/org/citrusframework/maven/plugin/TestApiGeneratorMojo.java b/test-api-generator/citrus-test-api-generator-maven-plugin/src/main/java/org/citrusframework/maven/plugin/TestApiGeneratorMojo.java new file mode 100644 index 0000000000..dcd546ca06 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/main/java/org/citrusframework/maven/plugin/TestApiGeneratorMojo.java @@ -0,0 +1,646 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.maven.plugin; + +import static java.lang.Boolean.TRUE; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.citrusframework.openapi.generator.CitrusJavaCodegen.API_ENDPOINT; +import static org.citrusframework.openapi.generator.CitrusJavaCodegen.API_TYPE; +import static org.citrusframework.openapi.generator.CitrusJavaCodegen.PREFIX; +import static org.citrusframework.openapi.generator.CitrusJavaCodegen.ROOT_CONTEXT_PATH; +import static org.citrusframework.openapi.generator.CitrusJavaCodegen.TARGET_XMLNS_NAMESPACE; +import static org.springframework.util.ReflectionUtils.findField; +import static org.springframework.util.ReflectionUtils.makeAccessible; +import static org.springframework.util.ReflectionUtils.setField; + +import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.maven.model.Resource; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; +import org.citrusframework.openapi.generator.WsdlToOpenApiTransformer; +import org.citrusframework.openapi.generator.exception.WsdlToOpenApiTransformationException; +import org.citrusframework.util.StringUtils; +import org.openapitools.codegen.plugin.CodeGenMojo; +import org.sonatype.plexus.build.incremental.BuildContext; +import org.sonatype.plexus.build.incremental.DefaultBuildContext; + +/** + * The Citrus OpenAPI Generator Maven Plugin is designed to facilitate the integration of multiple + * OpenAPI specifications into the Citrus testing framework by automatically generating necessary + * test classes and XSDs. This plugin wraps the {@code CodeGenMojo} and extends its functionality to + * support multiple API configurations. + *

      + * Features: - Multiple API Configurations: Easily configure multiple OpenAPI specifications to + * generate test APIs with specific prefixes. - Citrus Integration: Generates classes and XSDs + * tailored for use within the Citrus framework, streamlining the process of creating robust + * integration tests. + *

      + */ +@Mojo( + name = "create-test-api", + defaultPhase = LifecyclePhase.GENERATE_TEST_SOURCES, + requiresDependencyCollection = ResolutionScope.TEST, + requiresDependencyResolution = ResolutionScope.TEST, + threadSafe = true +) +public class TestApiGeneratorMojo extends AbstractMojo { + + public static final String RESOURCE_FOLDER = "resourceFolder"; + + public static final String DEFAULT_BASE_PACKAGE = "org.citrusframework.automation.%PREFIX%.%VERSION%"; + public static final String DEFAULT_INVOKER_PACKAGE = DEFAULT_BASE_PACKAGE; + public static final String DEFAULT_API_PACKAGE = DEFAULT_BASE_PACKAGE + ".api"; + public static final String DEFAULT_MODEL_PACKAGE = DEFAULT_BASE_PACKAGE + ".model"; + public static final String DEFAULT_SCHEMA_FOLDER_TEMPLATE = "schema/xsd/%VERSION%"; + public static final ApiType DEFAULT_API_TYPE = ApiType.REST; + + /** + * Marker fragment in the schema name of a generated schema. Used to distinguish generated from + * non generated values, when manipulating spring meta-data files. + */ + public static final String CITRUS_TEST_SCHEMA = "citrus-test-schema"; + + /** + * Marker text to mark the next line of a spring.handlers or spring.schemas file not to be + * removed by the generator. + */ + public static final String CITRUS_TEST_SCHEMA_KEEP_HINT = "citrus-test-schema-keep"; + + /** + * Specifies the default target namespace template. When changing the default value, it's + * important to maintain the 'citrus-test-schema' part, as this name serves to differentiate + * between generated and non-generated schemas. This differentiation aids in the creation of + * supporting Spring files such as 'spring.handlers' and 'spring.schemas'. + */ + public static final String DEFAULT_TARGET_NAMESPACE_TEMPLATE = + "http://www.citrusframework.org/" + CITRUS_TEST_SCHEMA + "/%VERSION%/%PREFIX%-api"; + + /** + * resourceFolder: specifies the location to which the resources are generated. Defaults to + * 'generated-test-resources'. + */ + public static final String RESOURCE_FOLDER_PROPERTY = "citrus.test.api.generator.resource.folder"; + + /** + * schemaFolder: specifies the location for the generated xsd schemas. Defaults to + * 'schema/xsd/%VERSION%' + */ + public static final String API_SCHEMA_FOLDER = "citrus.test.api.generator.schema.folder"; + + /** + * metaInfFolder: specifies the location to which the resources are generated. Defaults to + * 'generated-resources'. + */ + public static final String META_INF_FOLDER = "citrus.test.api.generator.meta.inf.folder"; + + /** + * resourceFolder: specifies the location to which the resources are generated. Defaults to + * 'generated-resources'. + */ + public static final String GENERATE_SPRING_INTEGRATION_FILES = "citrus.test.api.generator.generate.spring.integration.files"; + + @Component + private final BuildContext buildContext = new DefaultBuildContext(); + + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject mavenProject; + + @Parameter(defaultValue = "${mojoExecution}", readonly = true) + private MojoExecution mojoExecution; + + @Parameter(name = "output", property = "openapi.generator.maven.plugin.output") + private File output; + + @Parameter(property = API_SCHEMA_FOLDER, defaultValue = DEFAULT_SCHEMA_FOLDER_TEMPLATE) + @SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"}) // Maven injected + private String schemaFolder = DEFAULT_SCHEMA_FOLDER_TEMPLATE; + + @Parameter(property = META_INF_FOLDER) + @SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"}) // Maven injected + private String metaInfFolder; + + @Parameter(property = GENERATE_SPRING_INTEGRATION_FILES, defaultValue = "true") + @SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"}) // Maven injected + private boolean generateSpringIntegrationFiles = true; + + @Parameter(name = "skip", property = "codegen.skip", defaultValue = "false") + @SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"}) // Maven injected + private Boolean skip; + + @Parameter + private List apis; + + /** + * Config options used for all APIS may be overwritten by api configOptions + */ + @Parameter + protected Map globalConfigOptions = new HashMap<>(); + + @Parameter + protected Map globalProperties = new HashMap<>(); + + /** + * Replace the placeholders '%PREFIX%' and '%VERSION%' in the given text. + */ + static String replaceDynamicVars(String text, String prefix, String version) { + if (text == null) { + return null; + } + + return text.replace("%PREFIX%", prefix) + .replace(".%VERSION%", version != null ? "." + version : "") + .replace("/%VERSION%", version != null ? "/" + version : "") + .replace("-%VERSION%", version != null ? "-" + version : "") + .replace("%VERSION%", version != null ? version : ""); + } + + /** + * Replace the placeholders '%PREFIX%' and '%VERSION%' in the given text, performing a + * toLowerCase on the prefix. + */ + static String replaceDynamicVarsToLowerCase(String text, String prefix, String version) { + return replaceDynamicVars(text, prefix.toLowerCase(), version); + } + + protected MavenProject getMavenProject() { + return mavenProject; + } + + protected void setMavenProject(MavenProject mavenProject) { + this.mavenProject = mavenProject; + } + + public List getApiConfigs() { + return apis; + } + + @VisibleForTesting + void setMojoExecution(MojoExecution mojoExecution) { + this.mojoExecution = mojoExecution; + } + + /** + * Returns the fully qualified schema folder + */ + public String schemaFolder(ApiConfig apiConfig) { + return replaceDynamicVars(schemaFolder, apiConfig.getPrefix(), apiConfig.getVersion()); + } + + @Override + public void execute() throws MojoExecutionException { + + for (int index = 0; index < apis.size(); index++) { + ApiConfig apiConfig = apis.get(index); + validateApiConfig(index, apiConfig); + delegateExecution(configureCodeGenMojo(apiConfig)); + } + + if (!TRUE.equals(skip) && generateSpringIntegrationFiles) { + new SpringMetaFileGenerator(this).generateSpringIntegrationMetaFiles(); + } + } + + @Nonnull + public String getMetaInfFolder() { + if (metaInfFolder == null) { + Path basePath = Paths.get(mavenProject.getBasedir().toURI()); + Path metaInfResourceFolderPath = Paths.get( + new File(mavenProject.getBuild().getDirectory(), + LifecyclePhase.GENERATE_TEST_SOURCES.id() + .equals(mojoExecution.getLifecyclePhase()) ? + "generated-test-sources/openapi/src/main/resources" + : "generated-sources/openapi/src/main/resources").toURI()); + Resource metaInfResource = new Resource(); + metaInfResource.setDirectory(metaInfResourceFolderPath.toString()); + mavenProject.addResource(metaInfResource); + metaInfFolder = + basePath.relativize(metaInfResourceFolderPath) + File.separator + + "META-INF"; + } + return metaInfFolder; + } + + @VisibleForTesting + void delegateExecution(CodeGenMojo codeGenMojo) throws MojoExecutionException { + codeGenMojo.execute(); + } + + CodeGenMojo configureCodeGenMojo(ApiConfig apiConfig) throws MojoExecutionException { + + if (apiConfig.getSource().toUpperCase().trim().endsWith(".WSDL")) { + apiConfig.source = convertToOpenApi(apiConfig.getSource()); + } + + Map apiConfigOptions = createApiConfigOptions(apiConfig); + + CodeGenMojo codeGenMojo = new CodeGenMojoWrapper() + .schemaFolder(schemaFolder(apiConfig)) + .skip(skip) + .output(output) + .mojoExecution(mojoExecution) + .project(mavenProject) + .inputSpec(apiConfig.getSource()) + .globalProperties(globalProperties) + .configOptions(apiConfigOptions); + + + codeGenMojo.setPluginContext(getPluginContext()); + + if (StringUtils.hasText(apiConfig.rootContextPath)) { + List properties = new ArrayList<>(); + if (apiConfig.additionalProperties != null) { + properties.addAll(apiConfig.additionalProperties); + } + apiConfig.additionalProperties = properties; + apiConfig.additionalProperties.add( + format("%s=%s", ROOT_CONTEXT_PATH, apiConfig.rootContextPath)); + } + + propagateBuildContext(codeGenMojo); + propagateAdditionalProperties(codeGenMojo, apiConfig.additionalProperties); + + return codeGenMojo; + } + + private Map createApiConfigOptions(ApiConfig apiConfig) { + Map configOptions = new HashMap<>(); + + if (LifecyclePhase.GENERATE_TEST_SOURCES.id().equals(mojoExecution.getLifecyclePhase())) { + configOptions.put("addCompileSourceRoot", "false"); + configOptions.put("addTestCompileSourceRoot", "true"); + } + + //noinspection unchecked + configOptions.putAll(apiConfig.toConfigOptionsProperties(globalConfigOptions)); + return configOptions; + } + + private String convertToOpenApi(String source) throws MojoExecutionException { + String apiFile = source; + String path = source.replace("\\", "/"); + + int lastSegmentIndex = path.lastIndexOf("/"); + if (lastSegmentIndex > 0) { + apiFile = path.substring(lastSegmentIndex + 1); + } + + File resourceFile; + if (new File(source).isAbsolute()) { + resourceFile = new File(source); + if (!resourceFile.exists()) { + throw new MojoExecutionException( + "File not found at the provided absolute path: " + source); + } + } else { + URL resourceUrl = getClass().getClassLoader().getResource(source); + if (resourceUrl == null) { + throw new MojoExecutionException("Resource not found in classpath: " + source); + } + resourceFile = new File(resourceUrl.getFile()); + } + + File tmpDir = new File(mavenProject.getBuild().getDirectory() + "/wsdlToOpenApi"); + if (!tmpDir.exists() && !tmpDir.mkdirs()) { + throw new MojoExecutionException( + "Unable to create directory for temporary storage of converted WSDL: " + + tmpDir.getAbsolutePath()); + } + + try { + WsdlToOpenApiTransformer transformer = new WsdlToOpenApiTransformer( + resourceFile.toURI()); + + String openApiContent = transformer.transformToOpenApi(); + + Path openApiPath = Paths.get(tmpDir.getAbsolutePath(), apiFile); + Files.writeString(openApiPath, openApiContent, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + + return openApiPath.toString(); + } catch (WsdlToOpenApiTransformationException | IOException e) { + throw new MojoExecutionException( + String.format("Unable to transform WSDL %s to OpenAPI", source), e); + } + } + + /** + * Reflectively inject the buildContext. This is set by maven and needs to be propagated into + * the plugin, that actually performs the work. + */ + private void propagateBuildContext(CodeGenMojo codeGenMojo) { + + Field buildContextField = findField(CodeGenMojo.class, "buildContext"); + makeAccessible(requireNonNull(buildContextField, + "Could not retrieve 'buildContext' field from CodeGenMojo delegate.")); + setField(buildContextField, codeGenMojo, buildContext); + } + + /** + * Reflectively inject the additionProperties. + */ + private void propagateAdditionalProperties(CodeGenMojo codeGenMojo, + List additionalProperties) { + + Field buildContextField = findField(CodeGenMojo.class, "additionalProperties"); + makeAccessible(requireNonNull(buildContextField, + "Could not retrieve 'additionalProperties' field from CodeGenMojo delegate.")); + setField(buildContextField, codeGenMojo, additionalProperties); + } + + private void validateApiConfig(int apiIndex, ApiConfig apiConfig) + throws MojoExecutionException { + requireNonBlankParameter("prefix", apiIndex, apiConfig.getPrefix()); + requireNonBlankParameter("source", apiIndex, apiConfig.getSource()); + } + + private void requireNonBlankParameter(String name, int index, String parameterValue) + throws MojoExecutionException { + if (isBlank(parameterValue)) { + throw new MojoExecutionException( + format("Required parameter '%s' not set for api at index '%d'!", name, index)); + } + } + + public enum ApiType { + REST, SOAP + } + + /** + * Note that the default values are not properly set by maven processor. Therefore, the default + * values have been assigned additionally on field level. + */ + public static class ApiConfig { + + public static final String DEFAULT_ENDPOINT = "PREFIX_ENDPOINT"; + + /** + * prefix: specifies the prefixed used for the test api. Typically, an acronym for the + * application which is being tested. + */ + public static final String API_PREFIX_PROPERTY = "citrus.test.api.generator.prefix"; + + /** + * rootContextPath: specifies the context path that will be prepended to all OpenAPI + * operation paths. + */ + public static final String ROOT_CONTEXT_PATH = "citrus.test.api.generator.root-context-path"; + + /** + * source: specifies the source of the test api. + */ + public static final String API_SOURCE_PROPERTY = "citrus.test.api.generator.source"; + + /** + * version: specifies the version of the api. May be null. + */ + public static final String API_VERSION_PROPERTY = "citrus.test.api.generator.version"; + + /** + * endpoint: specifies the endpoint of the test api. Defaults to 'prefixEndpoint'. + */ + public static final String API_ENDPOINT_PROPERTY = "citrus.test.api.generator.endpoint"; + + /** + * type: specifies the type of the test api. Defaults to 'REST' + */ + public static final String API_TYPE_PROPERTY = "citrus.test.api.generator.type"; + + /** + * useTags: specifies whether tags should be used by the generator. Defaults to 'true'. If + * useTags is set to true, the generator will organize the generated code based on the tags + * defined in your API specification. + */ + public static final String API_USE_TAGS_PROPERTY = "citrus.test.api.generator.use.tags"; + + /** + * invokerPackage: specifies the package for the test api classes. Defaults to + * 'org.citrusframework.automation.%PREFIX%.%VERSION%'. + */ + public static final String API_INVOKER_PACKAGE_PROPERTY = "citrus.test.api.generator.invoker.package"; + + /** + * apiPackage: specifies the package for the test api classes. Defaults to + * 'org.citrusframework.automation.%PREFIX%.%VERSION%.api'. + */ + public static final String API_API_PACKAGE_PROPERTY = "citrus.test.api.generator.api.package"; + + /** + * modelPackage: specifies the package for the test api classes. Defaults to + * 'org.citrusframework.automation.%PREFIX%.%VERSION%.model'. + */ + public static final String API_MODEL_PACKAGE_PROPERTY = "citrus.test.api.generator.model.package"; + + /** + * targetXmlNamespace: specifies the xml namespace to be used by the api. Defaults to + * 'http://www.citrusframework.org/schema/%VERSION%/%PREFIX%-api' + */ + @SuppressWarnings("JavadocLinkAsPlainText") + public static final String API_NAMESPACE_PROPERTY = "citrus.test.api.generator.namespace"; + + @Parameter(required = true, property = API_PREFIX_PROPERTY) + private String prefix; + + @Parameter(required = true, property = ROOT_CONTEXT_PATH) + private String rootContextPath; + + @Parameter(required = true, property = API_SOURCE_PROPERTY) + private String source; + + @Parameter(property = API_VERSION_PROPERTY) + private String version; + + @Parameter(property = API_ENDPOINT_PROPERTY, defaultValue = DEFAULT_ENDPOINT) + private String endpoint = DEFAULT_ENDPOINT; + + @Parameter(property = API_TYPE_PROPERTY, defaultValue = "REST") + private ApiType type = DEFAULT_API_TYPE; + + @Parameter(property = API_USE_TAGS_PROPERTY, defaultValue = "true") + private boolean useTags = true; + + @Parameter(property = API_INVOKER_PACKAGE_PROPERTY, defaultValue = DEFAULT_INVOKER_PACKAGE) + private String invokerPackage = DEFAULT_INVOKER_PACKAGE; + + @Parameter(property = API_API_PACKAGE_PROPERTY, defaultValue = DEFAULT_API_PACKAGE) + private String apiPackage = DEFAULT_API_PACKAGE; + + @Parameter(property = API_MODEL_PACKAGE_PROPERTY, defaultValue = DEFAULT_MODEL_PACKAGE) + private String modelPackage = DEFAULT_MODEL_PACKAGE; + + @Parameter(property = API_NAMESPACE_PROPERTY, defaultValue = DEFAULT_TARGET_NAMESPACE_TEMPLATE) + private String targetXmlnsNamespace = DEFAULT_TARGET_NAMESPACE_TEMPLATE; + + @SuppressWarnings("rawtypes") + @Parameter(name = "apiConfigOptions") + private Map apiConfigOptions; + + @SuppressWarnings("rawtypes") + @Parameter(name = "additionalProperties") + private List additionalProperties; + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public ApiType getType() { + return type; + } + + public void setType(ApiType type) { + this.type = type; + } + + public boolean isUseTags() { + return useTags; + } + + public void setUseTags(boolean useTags) { + this.useTags = useTags; + } + + public String getInvokerPackage() { + return invokerPackage; + } + + public void setInvokerPackage(String invokerPackage) { + this.invokerPackage = invokerPackage; + } + + public String getApiPackage() { + return apiPackage; + } + + public void setApiPackage(String apiPackage) { + this.apiPackage = apiPackage; + } + + public String getModelPackage() { + return modelPackage; + } + + public void setModelPackage(String modelPackage) { + this.modelPackage = modelPackage; + } + + public String getTargetXmlnsNamespace() { + return targetXmlnsNamespace; + } + + public void setTargetXmlnsNamespace(String targetXmlnsNamespace) { + this.targetXmlnsNamespace = targetXmlnsNamespace; + } + + public String qualifiedEndpoint() { + return DEFAULT_ENDPOINT.equals(endpoint) ? getPrefix().toLowerCase() + "Endpoint" + : endpoint; + } + + public void setRootContextPath(String rootContextPath) { + this.rootContextPath = rootContextPath; + } + + public void setApiConfigOptions(Map apiConfigOptions) { + this.apiConfigOptions = apiConfigOptions; + } + + public Map getApiConfigOptions() { + return apiConfigOptions; + } + + public void setAdditionalProperties(List additionalProperties) { + this.additionalProperties = additionalProperties; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + Map toConfigOptionsProperties(Map globalConfigOptions) { + Map configOptionsProperties = new HashMap<>(); + configOptionsProperties.put(PREFIX, prefix); + configOptionsProperties.put(API_ENDPOINT, qualifiedEndpoint()); + configOptionsProperties.put(API_TYPE, type.toString()); + configOptionsProperties.put(TARGET_XMLNS_NAMESPACE, + replaceDynamicVarsToLowerCase(targetXmlnsNamespace, prefix, version)); + configOptionsProperties.put("invokerPackage", + replaceDynamicVarsToLowerCase(invokerPackage, prefix, version)); + configOptionsProperties.put("apiPackage", + replaceDynamicVarsToLowerCase(apiPackage, prefix, version)); + configOptionsProperties.put("modelPackage", + replaceDynamicVarsToLowerCase(modelPackage, prefix, version)); + configOptionsProperties.put("useTags", useTags); + + if (globalConfigOptions != null) { + configOptionsProperties.putAll(globalConfigOptions); + } + + if (apiConfigOptions != null) { + configOptionsProperties.putAll(apiConfigOptions); + } + + return configOptionsProperties; + } + + } +} diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/SpringMetaFileGeneratorTest.java b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/SpringMetaFileGeneratorTest.java new file mode 100644 index 0000000000..409f582cf2 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/SpringMetaFileGeneratorTest.java @@ -0,0 +1,63 @@ +package org.citrusframework.maven.plugin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import java.io.File; +import java.util.List; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.project.MavenProject; +import org.citrusframework.maven.plugin.TestApiGeneratorMojo.ApiConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SpringMetaFileGeneratorTest { + + @Mock + private TestApiGeneratorMojo testApiGeneratorMojo = mock(); + + private SpringMetaFileGenerator sut; + + @BeforeEach + void beforeEach() { + sut = new SpringMetaFileGenerator(testApiGeneratorMojo); + } + @Test + void generateMetaFiles() throws MojoExecutionException { + String userDir = System.getProperty("user.dir"); + + if (!userDir.endsWith("target")) { + userDir = userDir + "/target"; + } + + MavenProject mavenProject = mock(); + doReturn(new File(userDir)).when(mavenProject).getBasedir(); + doReturn(mavenProject).when(testApiGeneratorMojo).getMavenProject(); + doReturn("/test-classes/SpringMetaFileGeneratorTest/META-INF").when(testApiGeneratorMojo).getMetaInfFolder(); + + ApiConfig apiConfig = new ApiConfig(); + apiConfig.setPrefix("PrefixA"); + doReturn(List.of(apiConfig)).when(testApiGeneratorMojo).getApiConfigs(); + + sut.generateSpringIntegrationMetaFiles(); + + assertThat(new File(userDir+"/test-classes/SpringMetaFileGeneratorTest/META-INF/spring.schemas")) + .isFile() + .exists() + .hasContent(""" + http\\://www.citrusframework.org/schema/restdocs/config/citrus-restdocs-config.xsd=org/citrusframework/schema/citrus-restdocs-config.xsd + http\\://www.citrusframework.org/citrus-test-schema/prefixa-api.xsd=null/prefixa-api.xsd"""); + assertThat(new File(userDir+"/test-classes/SpringMetaFileGeneratorTest/META-INF/spring.handlers")) + .isFile() + .exists() + .hasContent(""" + http\\://www.citrusframework.org/schema/restdocs/config=org.citrusframework.restdocs.config.handler.RestDocConfigNamespaceHandler + http\\://www.citrusframework.org/citrus-test-schema/prefixa-api=org.citrusframework.automation.prefixa.spring.PrefixANamespaceHandler"""); + + } +} diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/TestApiGeneratorMojoIntegrationTest.java b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/TestApiGeneratorMojoIntegrationTest.java new file mode 100644 index 0000000000..4b6d471597 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/TestApiGeneratorMojoIntegrationTest.java @@ -0,0 +1,404 @@ +package org.citrusframework.maven.plugin; + +import static com.google.common.collect.Streams.concat; +import static java.lang.Boolean.TRUE; +import static java.util.Arrays.stream; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.CITRUS_TEST_SCHEMA; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.replaceDynamicVars; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.replaceDynamicVarsToLowerCase; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.springframework.test.util.ReflectionTestUtils.getField; + +import jakarta.validation.constraints.NotNull; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.testing.AbstractMojoTestCase; +import org.apache.maven.project.MavenProject; +import org.assertj.core.api.Assertions; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.exceptions.TestCaseFailedException; +import org.citrusframework.maven.plugin.TestApiGeneratorMojo.ApiConfig; +import org.citrusframework.maven.plugin.stubs.CitrusOpenApiGeneratorMavenProjectStub; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.openapitools.codegen.plugin.CodeGenMojo; +import org.springframework.test.util.ReflectionTestUtils; + +public class TestApiGeneratorMojoIntegrationTest extends AbstractMojoTestCase { + + public static final String OTHER_META_FILE_CONTENT = "somenamespace=somevalue"; + + public static final String OTHER_CITRUS_META_FILE_CONTENT = String.format( + "somenamespace/%s/aa=somevalue", CITRUS_TEST_SCHEMA); + + /** + * Array containing path templates for each generated file, specified with tokens. Tokens can be + * replaced with values of the respective testing scenario. + */ + private static final String[] STANDARD_FILE_PATH_TEMPLATES = new String[]{ + "%TARGET_FOLDER%/%GENERATED_SOURCES_FOLDER%/%INVOKER_FOLDER%/%CAMEL_PREFIX%OpenApi.java", + "%TARGET_FOLDER%/%GENERATED_SOURCES_FOLDER%/%INVOKER_FOLDER%/spring/%CAMEL_PREFIX%NamespaceHandler.java", + "%TARGET_FOLDER%/%GENERATED_SOURCES_FOLDER%/%INVOKER_FOLDER%/spring/%CAMEL_PREFIX%BeanConfiguration.java", + "%TARGET_FOLDER%/%GENERATED_SOURCES_FOLDER%/%MODEL_FOLDER%/PingReqType.java", + "%TARGET_FOLDER%/%GENERATED_SOURCES_FOLDER%/%MODEL_FOLDER%/PingRespType.java", + "%TARGET_FOLDER%/%GENERATED_SOURCES_FOLDER%/%REQUEST_FOLDER%/PingApi.java", + "%TARGET_FOLDER%/%GENERATED_SOURCES_FOLDER%/%REQUEST_FOLDER%/PungApi.java", + "%TARGET_FOLDER%/%GENERATED_RESOURCES_FOLDER%/%SCHEMA_FOLDER%/%LOWER_PREFIX%-api.xsd" + }; + + /** + * Array containing path templates for each generated spring meta file, specified with tokens. + * Tokens can be replaced with values of the respective testing scenario. + */ + private static final String[] SPRING_META_FILE_TEMPLATES = new String[]{ + "%BASE_FOLDER%/%META_INF_FOLDER%/spring.handlers", + "%BASE_FOLDER%/%META_INF_FOLDER%/spring.schemas" + }; + + private TestApiGeneratorMojo fixture; + + static Stream executeMojoWithConfigurations() { + return Stream.of( + arguments("pom-missing-prefix", + new MojoExecutionException( + "Required parameter 'prefix' not set for api at index '0'!"), + emptyMap(), emptyMap(), emptyList()), + arguments("pom-missing-source", + new MojoExecutionException( + "Required parameter 'source' not set for api at index '0'!"), + emptyMap(), emptyMap(), emptyList()), + arguments("pom-minimal-config", null, + emptyMap(), emptyMap(), emptyList()), + arguments("pom-minimal-with-version-config", null, + emptyMap(), emptyMap(), emptyList()), + arguments("pom-multi-config", null, + emptyMap(), emptyMap(), emptyList()), + arguments("pom-full-config", null, + Map.of("debugOpenAPI", "true", "debugModels", "true"), + Map.of("a", "b", "other", "otherOption"), + List.of("a=b", "c=d", "rootContextPath=/a/b/c/d")), + arguments("pom-full-with-version-config", null, + emptyMap(), emptyMap(), emptyList()), + arguments("pom-soap-config", null, + emptyMap(), emptyMap(), emptyList()), + arguments("pom-with-global-properties", null, + Map.of("debugOpenAPI", "true", "debugModels", "true"), + emptyMap(), emptyList()), + arguments("pom-with-global-config", null, + emptyMap(), + Map.of("a", "b", "other", "otherOption"), emptyList()), + arguments("pom-with-overriding-config", null, + Map.of("debugOpenAPI", "true", "debugModels", "true"), + Map.of("a", "b", "c", "d", "other", "otherOption"), emptyList()), + arguments("pom-with-additional-properties", null, + emptyMap(), emptyMap(), + List.of("a=b", "c=d")), + arguments("pom-soap-from-wsdl-config", null, + emptyMap(), emptyMap(), emptyList()) + + ); + } + + @BeforeEach + @SuppressWarnings("JUnitMixedFramework") + void beforeEachSetup() throws Exception { + setUp(); + } + + @ParameterizedTest + @MethodSource + void executeMojoWithConfigurations(String configName, Exception expectedException, + Map expectedGlobalProperties, Map expectedConfigOptions, + List expectedAdditionalProperties) throws Exception { + try { + fixture = fixtureFromPom(configName); + } catch (MojoExecutionException | MojoFailureException e) { + Assertions.fail("Test setup failed!", e); + } + + @SuppressWarnings("unchecked") + List apiConfigs = (List) getField(fixture, "apis"); + + assertThat(apiConfigs).isNotNull(); + + if (expectedException == null) { + // Given + writeSomeValuesToSpringMetaFiles(apiConfigs); + + // When + assertThatCode(() -> fixture.execute()).doesNotThrowAnyException(); + + // Then + for (ApiConfig apiConfig : apiConfigs) { + assertFilesGenerated(apiConfig); + assertSpecificFileContent(apiConfig); + } + + ArgumentCaptor codegenMojoCaptor = ArgumentCaptor.captor(); + verify(fixture, atLeastOnce()).delegateExecution(codegenMojoCaptor.capture()); + + CodeGenMojo codeGenMojo = codegenMojoCaptor.getValue(); + @SuppressWarnings("rawtypes") + Map globalProperties = (Map) ReflectionTestUtils.getField(codeGenMojo, + "globalProperties"); + @SuppressWarnings("rawtypes") + Map configOptions = (Map) ReflectionTestUtils.getField(codeGenMojo, "configOptions"); + + //noinspection unchecked + assertThat(globalProperties).containsExactlyInAnyOrderEntriesOf(expectedGlobalProperties); + //noinspection unchecked + assertThat(configOptions).containsAllEntriesOf(expectedConfigOptions); + //noinspection unchecked + List additionalPropertyList = (List) ReflectionTestUtils.getField( + codeGenMojo, "additionalProperties"); + + if (expectedAdditionalProperties.isEmpty()) { + assertThat(additionalPropertyList).isNull(); + } else { + assertThat(additionalPropertyList).containsExactlyElementsOf(expectedAdditionalProperties); + } + + } else { + // When/Then + assertThatThrownBy(() -> fixture.execute()) + .isInstanceOf(expectedException.getClass()) + .hasMessage(expectedException.getMessage()); + } + } + + /** + * Writes values to spring meta files, to make sure existing non generated and existing + * generated values are treated properly. + */ + private void writeSomeValuesToSpringMetaFiles(List apiConfigs) { + for (ApiConfig apiConfig : apiConfigs) { + for (String filePathTemplate : SPRING_META_FILE_TEMPLATES) { + String filePath = resolveFilePath(apiConfig, filePathTemplate); + File file = new File(filePath); + if (!file.getParentFile().exists() && !new File(filePath).getParentFile() + .mkdirs()) { + Assertions.fail("Unable to prepare test data."); + } + + try (FileWriter fileWriter = new FileWriter(filePath)) { + fileWriter.append(String.format("%s%n", OTHER_META_FILE_CONTENT)); + fileWriter.append(String.format("%s%n", OTHER_CITRUS_META_FILE_CONTENT)); + } catch (IOException e) { + throw new CitrusRuntimeException("Unable to write spring meta files", e); + } + } + } + } + + private void assertFilesGenerated(ApiConfig apiConfig) { + + if (apiConfig.getSource().contains("test-api.yml")) { + for (String filePathTemplate : STANDARD_FILE_PATH_TEMPLATES) { + String filePath = resolveFilePath(apiConfig, filePathTemplate); + assertThat(new File(filePath)).isFile().exists(); + } + + if (TRUE.equals(getField(fixture, "generateSpringIntegrationFiles"))) { + for (String filePathTemplate : SPRING_META_FILE_TEMPLATES) { + String filePath = resolveFilePath(apiConfig, filePathTemplate); + assertThat(new File(filePath)).isFile().exists(); + } + } + } + } + + private void assertSpecificFileContent(ApiConfig apiConfig) { + try { + assertEndpointName(apiConfig); + assertTargetNamespace(apiConfig); + assertSchemasInSpringSchemas(apiConfig); + assertHandlersInSpringHandlers(apiConfig); + } catch (IOException e) { + throw new TestCaseFailedException(e); + } + } + + private void assertHandlersInSpringHandlers(ApiConfig apiConfig) throws IOException { + String targetNamespace = replaceDynamicVarsToLowerCase(apiConfig.getTargetXmlnsNamespace(), + apiConfig.getPrefix(), apiConfig.getVersion()); + targetNamespace = targetNamespace.replace(":", "\\:"); + String invokerPackage = replaceDynamicVarsToLowerCase(apiConfig.getInvokerPackage(), + apiConfig.getPrefix(), apiConfig.getVersion()); + + String text = String.format("%s=%s.spring.%sNamespaceHandler", targetNamespace, + invokerPackage, apiConfig.getPrefix()); + + assertThat(getContentOfFile(apiConfig, "spring.handlers")).contains(text); + + // Other specific meta info should be retained + assertThat(getContentOfFile(apiConfig, "spring.handlers")).contains( + OTHER_META_FILE_CONTENT); + // Other citrus generated meta info should be deleted + assertThat(getContentOfFile(apiConfig, "spring.handlers")).doesNotContain( + OTHER_CITRUS_META_FILE_CONTENT); + } + + private void assertSchemasInSpringSchemas(ApiConfig apiConfig) throws IOException { + String targetNamespace = replaceDynamicVarsToLowerCase(apiConfig.getTargetXmlnsNamespace(), + apiConfig.getPrefix(), apiConfig.getVersion()); + targetNamespace = targetNamespace.replace(":", "\\:"); + String schemaPath = replaceDynamicVarsToLowerCase( + (String) getField(fixture, "schemaFolder"), apiConfig.getPrefix(), + apiConfig.getVersion()); + + String text = String.format("%s.xsd=%s/%s-api.xsd", targetNamespace, schemaPath, + apiConfig.getPrefix().toLowerCase()); + + // Other specific meta info should be retained assertThat(getContentOfFile(apiConfig, "spring.schemas")).contains(OTHER_META_FILE_CONTENT); + assertThat(getContentOfFile(apiConfig, "spring.schemas")).contains( + String.format("%s", text)); + // Other citrus generated meta info should be deleted + assertThat(getContentOfFile(apiConfig, "spring.schemas")).doesNotContain( + OTHER_CITRUS_META_FILE_CONTENT); + } + + private void assertTargetNamespace(ApiConfig apiConfig) throws IOException { + assertThat(getContentOfFile(apiConfig, "-api.xsd")) + .contains(String.format("targetNamespace=\"%s\"", + replaceDynamicVarsToLowerCase(apiConfig.getTargetXmlnsNamespace(), + apiConfig.getPrefix(), apiConfig.getVersion()))); + } + + private void assertEndpointName(ApiConfig apiConfig) throws IOException { + assertThat(getContentOfFile(apiConfig, "BeanConfiguration")) + .contains(String.format("@Qualifier(\"%s\")", apiConfig.qualifiedEndpoint())); + } + + private String getContentOfFile(ApiConfig apiConfig, String fileIdentifier) throws IOException { + String filePathTemplate = getTemplateContaining(fileIdentifier); + String filePath = resolveFilePath(apiConfig, filePathTemplate); + + File file = new File(filePath); + + assertThat(file).exists(); + Path path = Paths.get(filePath); + byte[] bytes = Files.readAllBytes(path); + return new String(bytes, StandardCharsets.UTF_8); + } + + private String getTemplateContaining(String text) { + return concat(stream(STANDARD_FILE_PATH_TEMPLATES), stream(SPRING_META_FILE_TEMPLATES)) + .filter(path -> path.contains(text)) + .findFirst() + .orElseThrow(() -> new AssertionError( + String.format("Can't find file template with content: '%s'", text))); + } + + @NotNull + private String resolveFilePath(ApiConfig apiConfig, String filePathTemplate) { + String lowerCasePrefix = apiConfig.getPrefix().toLowerCase(); + char[] prefixCharArray = apiConfig.getPrefix().toCharArray(); + prefixCharArray[0] = Character.toUpperCase(prefixCharArray[0]); + String camelCasePrefix = new String(prefixCharArray); + + String invokerFolder = toFolder( + replaceDynamicVarsToLowerCase(apiConfig.getInvokerPackage(), apiConfig.getPrefix(), + apiConfig.getVersion())); + String modelFolder = toFolder( + replaceDynamicVarsToLowerCase(apiConfig.getModelPackage(), apiConfig.getPrefix(), + apiConfig.getVersion())); + String requestFolder = toFolder( + replaceDynamicVarsToLowerCase(apiConfig.getApiPackage(), apiConfig.getPrefix(), + apiConfig.getVersion())); + String schemaFolder = toFolder( + replaceDynamicVars((String) getField(fixture, "schemaFolder"), apiConfig.getPrefix(), + apiConfig.getVersion())); + + + Map apiConfigOptions = apiConfig.getApiConfigOptions(); + + String targetFolder = fixture.getMavenProject().getBuild().getDirectory(); + String generatedSourcesFolder = "generated-sources/openapi/src/main/java"; + String generatedResourcesFolder = "generated-sources/openapi/src/main/resources"; + + if (apiConfigOptions != null) { + String output = apiConfigOptions.get("output"); + if (output != null) { + // Due to the special nature of the nested build directories, explicit output paths need + // to be prefixed with the path to the nested target folder - e.g. target/pom-full-config. + // These two segments are also specified in the baseFolderPath and thus need to be removed + // for proper resolution. + Path baseFolderPath = Path.of(fixture.getMavenProject().getBasedir().getPath()); + Path outputPath = Path.of(output); + targetFolder = baseFolderPath.getParent().getParent().resolve(outputPath).toString(); + } + + String sourceFolder = apiConfigOptions.get("sourceFolder"); + if (sourceFolder != null) { + generatedSourcesFolder = sourceFolder; + } + + String resourceFolder = apiConfigOptions.get("resourceFolder"); + if (resourceFolder != null) { + generatedResourcesFolder = resourceFolder; + } + + } + + return filePathTemplate + .replace("%BASE_FOLDER%", fixture.getMavenProject().getBasedir().getPath()) + .replace("%TARGET_FOLDER%", targetFolder) + .replace("%SOURCE_FOLDER%", fixture.getMavenProject().getBuild().getSourceDirectory()) + .replace("%GENERATED_SOURCES_FOLDER%", generatedSourcesFolder) + .replace("%GENERATED_RESOURCES_FOLDER%", generatedResourcesFolder) + .replace("%INVOKER_FOLDER%", invokerFolder) + .replace("%MODEL_FOLDER%", modelFolder) + .replace("%REQUEST_FOLDER%", requestFolder) + .replace("%SCHEMA_FOLDER%", schemaFolder) + .replace("%LOWER_PREFIX%", lowerCasePrefix) + .replace("%CAMEL_PREFIX%", camelCasePrefix) + .replace("%META_INF_FOLDER%", fixture.getMetaInfFolder()); + } + + private String toFolder(String text) { + if (text == null) { + return ""; + } + + return text.replace(".", "/"); + } + + private TestApiGeneratorMojo fixtureFromPom(String configName) throws Exception { + String goal = "create-test-api"; + + File pomFile = new File(getBasedir(), + String.format("src/test/resources/%s/%s", getClass().getSimpleName(), + configName + ".xml")); + assertThat(pomFile).exists(); + + MavenProject mavenProject = new CitrusOpenApiGeneratorMavenProjectStub(configName); + + TestApiGeneratorMojo testApiGeneratorMojo = (TestApiGeneratorMojo) lookupMojo(goal, + pomFile); + testApiGeneratorMojo.setMavenProject(mavenProject); + testApiGeneratorMojo.setMojoExecution(newMojoExecution(goal)); + + return spy(testApiGeneratorMojo); + } +} diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/TestApiGeneratorMojoUnitTest.java b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/TestApiGeneratorMojoUnitTest.java new file mode 100644 index 0000000000..9d313cc84d --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/TestApiGeneratorMojoUnitTest.java @@ -0,0 +1,305 @@ +package org.citrusframework.maven.plugin; + +import static java.lang.Boolean.TRUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.DEFAULT_API_PACKAGE; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.DEFAULT_API_TYPE; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.DEFAULT_INVOKER_PACKAGE; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.DEFAULT_MODEL_PACKAGE; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.DEFAULT_SCHEMA_FOLDER_TEMPLATE; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.DEFAULT_TARGET_NAMESPACE_TEMPLATE; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.replaceDynamicVars; +import static org.citrusframework.maven.plugin.TestApiGeneratorMojo.replaceDynamicVarsToLowerCase; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.doReturn; +import static org.springframework.test.util.ReflectionTestUtils.getField; + +import jakarta.validation.constraints.NotNull; +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.apache.maven.model.Build; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.testing.AbstractMojoTestCase; +import org.apache.maven.project.MavenProject; +import org.citrusframework.maven.plugin.TestApiGeneratorMojo.ApiConfig; +import org.citrusframework.maven.plugin.TestApiGeneratorMojo.ApiType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openapitools.codegen.plugin.CodeGenMojo; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings({"JUnitMalformedDeclaration"}) +class TestApiGeneratorMojoUnitTest extends AbstractMojoTestCase { + + @InjectMocks + private TestApiGeneratorMojo fixture = new TestApiGeneratorMojo(); + + @Mock + private Build buildMock; + + @Mock + private MavenProject mavenProjectMock; + + @Mock + private MojoExecution mojoExecutionMock; + + static Stream replaceDynamicVarsInPattern() { + return Stream.of( + arguments("%PREFIX%-aa-%VERSION%", "MyPrefix", "1", false, "MyPrefix-aa-1"), + arguments("%PREFIX%-aa-%VERSION%", "MyPrefix", null, false, "MyPrefix-aa"), + arguments("%PREFIX%/aa/%VERSION%", "MyPrefix", "1", false, "MyPrefix/aa/1"), + arguments("%PREFIX%/aa/%VERSION%", "MyPrefix", null, false, "MyPrefix/aa"), + arguments("%PREFIX%.aa.%VERSION%", "MyPrefix", "1", true, "myprefix.aa.1"), + arguments("%PREFIX%.aa.%VERSION%", "MyPrefix", null, true, "myprefix.aa") + ); + } + + static Stream configureMojo() { + return Stream.of( + arguments("DefaultConfigWithoutVersion", createMinimalApiConfig(null), + createMinimalCodeGenMojoParams( + "schema/xsd", + "org.citrusframework.automation.mydefaultprefix", + "org.citrusframework.automation.mydefaultprefix.model", + "org.citrusframework.automation.mydefaultprefix.api", + "http://www.citrusframework.org/citrus-test-schema/mydefaultprefix-api" + )), + arguments("DefaultConfigWithVersion", createMinimalApiConfig("v1"), + createMinimalCodeGenMojoParams( + "schema/xsd/v1", + "org.citrusframework.automation.mydefaultprefix.v1", + "org.citrusframework.automation.mydefaultprefix.v1.model", + "org.citrusframework.automation.mydefaultprefix.v1.api", + "http://www.citrusframework.org/citrus-test-schema/v1/mydefaultprefix-api" + )), + arguments("CustomConfigWithoutVersion", createFullApiConfig(null), + createCustomCodeGenMojoParams( + "schema/xsd", + "my.mycustomprefix.invoker.package", + "my.mycustomprefix.model.package", + "my.mycustomprefix.api.package", + "myNamespace/citrus-test-schema/mycustomprefix" + )), + arguments("CustomConfigWithVersion", createFullApiConfig("v1"), + createCustomCodeGenMojoParams( + "schema/xsd/v1", + "my.mycustomprefix.v1.invoker.package", + "my.mycustomprefix.v1.model.package", + "my.mycustomprefix.v1.api.package", + "myNamespace/citrus-test-schema/mycustomprefix/v1" + )) + ); + } + + /** + * Create an {@link ApiConfig} with the minimal configuration, that is required. All other + * values will be chosen as defaults. + */ + @NotNull + private static ApiConfig createMinimalApiConfig(String version) { + ApiConfig apiConfig = new ApiConfig(); + apiConfig.setPrefix("MyDefaultPrefix"); + apiConfig.setSource("myDefaultSource"); + apiConfig.setVersion(version); + + return apiConfig; + } + + /** + * Create an {@link ApiConfig} with all possible configurations, no defaults. + */ + @NotNull + private static ApiConfig createFullApiConfig(String version) { + ApiConfig apiConfig = new ApiConfig(); + apiConfig.setPrefix("MyCustomPrefix"); + apiConfig.setSource("myCustomSource"); + apiConfig.setApiPackage("my.%PREFIX%.%VERSION%.api.package"); + apiConfig.setInvokerPackage("my.%PREFIX%.%VERSION%.invoker.package"); + apiConfig.setModelPackage("my.%PREFIX%.%VERSION%.model.package"); + apiConfig.setEndpoint("myEndpoint"); + apiConfig.setTargetXmlnsNamespace("myNamespace/citrus-test-schema/%PREFIX%/%VERSION%"); + apiConfig.setUseTags(false); + apiConfig.setType(ApiType.SOAP); + apiConfig.setVersion(version); + apiConfig.setApiConfigOptions( + Map.of("optA", "A", "optB", "B", "output", "my-target", "sourceFolder", + "mySourceFolder", + "resourceFolder", "myResourceFolder")); + apiConfig.setAdditionalProperties(List.of("a=b", "c=d")); + apiConfig.setRootContextPath("/a/b/c/d"); + + return apiConfig; + } + + @NotNull + private static CodeGenMojoParams createMinimalCodeGenMojoParams(String schemaFolder, + String invokerPackage, String modelPackage, String apiPackage, + String targetXmlnsNamespace) { + + Map configOptionsControlMap = new HashMap<>(); + configOptionsControlMap.put("prefix", "MyDefaultPrefix"); + configOptionsControlMap.put("generatedSchemaFolder", schemaFolder); + configOptionsControlMap.put("invokerPackage", invokerPackage); + configOptionsControlMap.put("apiPackage", apiPackage); + configOptionsControlMap.put("modelPackage", modelPackage); + configOptionsControlMap.put("apiEndpoint", "mydefaultprefixEndpoint"); + configOptionsControlMap.put("targetXmlnsNamespace", targetXmlnsNamespace); + configOptionsControlMap.put("apiType", "REST"); + configOptionsControlMap.put("useTags", true); + + return new CodeGenMojoParams("myDefaultSource", configOptionsControlMap, + Collections.emptyList()); + } + + @NotNull + private static CodeGenMojoParams createCustomCodeGenMojoParams(String schemaFolder, + String invokerPackage, String modelPackage, String apiPackage, + String targetXmlnsNamespace) { + + Map configOptionsControlMap = new HashMap<>(); + configOptionsControlMap.put("prefix", "MyCustomPrefix"); + configOptionsControlMap.put("generatedSchemaFolder", schemaFolder); + configOptionsControlMap.put("invokerPackage", invokerPackage); + configOptionsControlMap.put("modelPackage", modelPackage); + configOptionsControlMap.put("apiPackage", apiPackage); + configOptionsControlMap.put("resourceFolder", "myResourceFolder"); + configOptionsControlMap.put("sourceFolder", "mySourceFolder"); + configOptionsControlMap.put("apiEndpoint", "myEndpoint"); + configOptionsControlMap.put("targetXmlnsNamespace", targetXmlnsNamespace); + configOptionsControlMap.put("apiType", "SOAP"); + configOptionsControlMap.put("useTags", false); + configOptionsControlMap.put("optA", "A"); + configOptionsControlMap.put("optB", "B"); + configOptionsControlMap.put("output", "my-target"); + + return new CodeGenMojoParams("myCustomSource", configOptionsControlMap, + List.of("a=b", "c=d", "rootContextPath=/a/b/c/d")); + } + + @ParameterizedTest + @MethodSource + void replaceDynamicVarsInPattern(String pattern, String prefix, String version, + boolean toLowerCasePrefix, String expectedResult) { + + if (toLowerCasePrefix) { + assertThat( + replaceDynamicVarsToLowerCase(pattern, prefix, version)).isEqualTo(expectedResult); + } else { + assertThat( + replaceDynamicVars(pattern, prefix, version)).isEqualTo(expectedResult); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource + void configureMojo(String name, ApiConfig apiConfig, CodeGenMojoParams controlParams) + throws MojoExecutionException { + fixture.setMavenProject(mavenProjectMock); + fixture.setMojoExecution(mojoExecutionMock); + + CodeGenMojo codeGenMojo = fixture.configureCodeGenMojo(apiConfig); + assertThat(getField(codeGenMojo, "project")).isEqualTo(mavenProjectMock); + assertThat(getField(codeGenMojo, "mojo")).isEqualTo(mojoExecutionMock); + + if (controlParams.configOptions.get("output") != null) { + assertThat(((File) getField(codeGenMojo, "output")).getPath()).isEqualTo( + controlParams.configOptions.get("output")); + } + + assertThat(getField(codeGenMojo, "inputSpec")).isEqualTo(controlParams.source); + assertThat(getField(codeGenMojo, "generateSupportingFiles")).isEqualTo(TRUE); + assertThat(getField(codeGenMojo, "generatorName")).isEqualTo("java-citrus"); + + if (controlParams.additionalProperties.isEmpty()) { + //noinspection unchecked + assertThat((List) getField(codeGenMojo, "additionalProperties")) + .isNull(); + } else { + //noinspection unchecked + assertThat((List) getField(codeGenMojo, "additionalProperties")) + .containsExactlyElementsOf(controlParams.additionalProperties); + } + + //noinspection unchecked + assertThat((Map) getField(codeGenMojo, "configOptions")) + .containsExactlyInAnyOrderEntriesOf(controlParams.configOptions); + } + + private record CodeGenMojoParams(String source, Map configOptions, + List additionalProperties) { + + } + + @Nested + class ApiConfigTest { + + private ApiConfig configFixture; + + @BeforeEach + void beforeEach() { + configFixture = new ApiConfig(); + } + + @Test + void apiPackagePathDefault() { + assertThat(configFixture.getApiPackage()).isEqualTo(DEFAULT_API_PACKAGE); + } + + @Test + void invokerPackagePathDefault() { + assertThat(configFixture.getInvokerPackage()).isEqualTo(DEFAULT_INVOKER_PACKAGE); + } + + @Test + void modelPackagePathDefault() { + assertThat(configFixture.getModelPackage()).isEqualTo(DEFAULT_MODEL_PACKAGE); + } + + @Test + void targetXmlnsNamespaceDefault() { + assertThat(configFixture.getTargetXmlnsNamespace()).isEqualTo( + DEFAULT_TARGET_NAMESPACE_TEMPLATE); + } + + @Test + void endpointDefault() { + configFixture.setPrefix("MyPrefix"); + assertThat(configFixture.qualifiedEndpoint()).isEqualTo("myprefixEndpoint"); + } + + @Test + void apiTypeDefault() { + assertThat(configFixture.getType()).isEqualTo(DEFAULT_API_TYPE); + } + + @Test + void schemaFolderDefault() { + assertThat((String) getField(fixture, "schemaFolder")).isEqualTo( + DEFAULT_SCHEMA_FOLDER_TEMPLATE); + } + + @Test + void metaInfFolderDefault() { + File baseDirFile = new File("/a/b/c"); + doReturn(new File(baseDirFile, "target").getPath()).when(buildMock).getDirectory(); + doReturn(buildMock).when(mavenProjectMock).getBuild(); + doReturn(baseDirFile).when(mavenProjectMock).getBasedir(); + + assertThat(fixture.getMetaInfFolder().replace("\\", "/")).isEqualTo( + ("target/generated-sources/openapi/src/main/resources/META-INF")); + } + } +} diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/stubs/CitrusOpenApiGeneratorMavenProjectStub.java b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/stubs/CitrusOpenApiGeneratorMavenProjectStub.java new file mode 100644 index 0000000000..4ad183393b --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/java/org/citrusframework/maven/plugin/stubs/CitrusOpenApiGeneratorMavenProjectStub.java @@ -0,0 +1,43 @@ +package org.citrusframework.maven.plugin.stubs; + +import org.apache.maven.model.Build; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.apache.maven.plugin.testing.stubs.MavenProjectStub; +import org.codehaus.plexus.PlexusTestCase; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +public class CitrusOpenApiGeneratorMavenProjectStub extends MavenProjectStub { + + private final String config; + + public CitrusOpenApiGeneratorMavenProjectStub(String config) { + this.config = config; + initModel(); + initBuild(); + } + + private void initBuild() { + Build build = new Build(); + build.setDirectory(getBasedir() + "/target"); + build.setSourceDirectory(getBasedir() + "/src"); + setBuild(build); + } + + private void initModel() { + MavenXpp3Reader reader = new MavenXpp3Reader(); + try { + setModel(reader.read(new FileReader("pom.xml"))); + } catch (IOException | XmlPullParserException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public File getBasedir() { + return new File(PlexusTestCase.getBasedir() + "/target/" + config); + } +} diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/SpringMetaFileGeneratorTest/META-INF/spring.handlers b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/SpringMetaFileGeneratorTest/META-INF/spring.handlers new file mode 100644 index 0000000000..d7a1144786 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/SpringMetaFileGeneratorTest/META-INF/spring.handlers @@ -0,0 +1 @@ +http\://www.citrusframework.org/schema/restdocs/config=org.citrusframework.restdocs.config.handler.RestDocConfigNamespaceHandler \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/SpringMetaFileGeneratorTest/META-INF/spring.schemas b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/SpringMetaFileGeneratorTest/META-INF/spring.schemas new file mode 100644 index 0000000000..05246d5451 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/SpringMetaFileGeneratorTest/META-INF/spring.schemas @@ -0,0 +1 @@ +http\://www.citrusframework.org/schema/restdocs/config/citrus-restdocs-config.xsd=org/citrusframework/schema/citrus-restdocs-config.xsd \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-full-config.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-full-config.xml new file mode 100644 index 0000000000..4e447e6f8c --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-full-config.xml @@ -0,0 +1,57 @@ + + 4.0.0 + + full-config + + + + + citrus-test-api-generator-maven-plugin + + + b + otherOption + + + true + true + + + + org.mypackage.%PREFIX%.api + myEndpoint + org.mypackage.%PREFIX%.invoker + org.mypackage.%PREFIX%.model + Full + api/test-api.yml + "http://myNamespace" + a=b,c=d + /a/b/c/d + + b + d + target/pom-full-config/target/generated-sources-mod/openapi-mod + src/main/java-mod + src/main/resource-mod + + + + myschema/xsd + src/main/resource-mod/META-INF-MOD + + + + + create-test-api + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-full-with-version-config.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-full-with-version-config.xml new file mode 100644 index 0000000000..f0a12a717c --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-full-with-version-config.xml @@ -0,0 +1,45 @@ + + 4.0.0 + + full-config + + + + + citrus-test-api-generator-maven-plugin + + + + org.mypackage.%PREFIX%.api + myEndpoint + org.mypackage.%PREFIX%.invoker + org.mypackage.%PREFIX%.model + FullWithVersion + api/test-api.yml + "http://myNamespace" + v1 + + target/pom-full-with-version-config/target/generated-sources-mod/openapi-mod + src/main/java-mod + src/main/resource-mod + + + + myschema/xsd + main-mod/resources-mod/META-INF-MOD + + + + + create-test-api + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-minimal-config.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-minimal-config.xml new file mode 100644 index 0000000000..ffb294db1d --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-minimal-config.xml @@ -0,0 +1,32 @@ + + 4.0.0 + + minimal-config + + + + + citrus-test-api-generator-maven-plugin + + + + Minimal + api/test-api.yml + + + + + + + create-test-api + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-minimal-with-version-config.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-minimal-with-version-config.xml new file mode 100644 index 0000000000..90ee2d6e4d --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-minimal-with-version-config.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + minimal-config + + + + + citrus-test-api-generator-maven-plugin + + + + MinimalWithVersion + api/test-api.yml + v2 + + + + + + + create-test-api + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-missing-prefix.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-missing-prefix.xml new file mode 100644 index 0000000000..fbdd07bd0d --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-missing-prefix.xml @@ -0,0 +1,32 @@ + + 4.0.0 + + missing-prefix + + + + + citrus-test-api-generator-maven-plugin + + + + mySource + + + + + + + create-test-api + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-missing-source.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-missing-source.xml new file mode 100644 index 0000000000..03f9f49074 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-missing-source.xml @@ -0,0 +1,32 @@ + + 4.0.0 + + missing-source + + + + + citrus-test-api-generator-maven-plugin + + + + MissingSource + + + + + + + create-test-api + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-multi-config.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-multi-config.xml new file mode 100644 index 0000000000..f71d499ba4 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-multi-config.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + multi-config + + + + + citrus-test-api-generator-maven-plugin + + + + Multi1 + api/test-api.yml + + + Multi2 + api/test-api.yml + + + Multi3 + api/test-api.yml + + + + + + + create-test-api + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-soap-config.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-soap-config.xml new file mode 100644 index 0000000000..434de65370 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-soap-config.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + soap-config + + + + + citrus-test-api-generator-maven-plugin + + + + Minimal + api/test-api.yml + SOAP + + + + + + + create-test-api + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-soap-from-wsdl-config.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-soap-from-wsdl-config.xml new file mode 100644 index 0000000000..abfa885518 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-soap-from-wsdl-config.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + soap-config + + + + + citrus-test-api-generator-maven-plugin + + + + FromWsdl + api/BookService.wsdl + SOAP + + + + + + + create-test-api + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-additional-properties.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-additional-properties.xml new file mode 100644 index 0000000000..896e16e063 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-additional-properties.xml @@ -0,0 +1,38 @@ + + 4.0.0 + + full-config + + + + + citrus-test-api-generator-maven-plugin + + + /base/a/b + otherOption + + + + Minimal + api/test-api.yml + a=b,c=d + + + + + + + create-test-api + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-global-config.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-global-config.xml new file mode 100644 index 0000000000..fc80fa5f45 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-global-config.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + full-config + + + + + citrus-test-api-generator-maven-plugin + + + b + otherOption + + + + Minimal + api/test-api.yml + + + + + + + create-test-api + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-global-properties.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-global-properties.xml new file mode 100644 index 0000000000..68b7486966 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-global-properties.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + full-config + + + + + citrus-test-api-generator-maven-plugin + + + true + true + + + + Minimal + api/test-api.yml + + + + + + + create-test-api + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-overriding-config.xml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-overriding-config.xml new file mode 100644 index 0000000000..754c2fcffa --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/TestApiGeneratorMojoIntegrationTest/pom-with-overriding-config.xml @@ -0,0 +1,45 @@ + + 4.0.0 + + full-config + + + + + citrus-test-api-generator-maven-plugin + + + true + true + + + a + otherOption + + + + Minimal + api/test-api.yml + + b + d + + + + + + + + create-test-api + + + + + + + + diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/api/BookDatatypes.xsd b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/api/BookDatatypes.xsd new file mode 100644 index 0000000000..3a735e7dd8 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/api/BookDatatypes.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/api/BookService.wsdl b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/api/BookService.wsdl new file mode 100644 index 0000000000..d16857172e --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/api/BookService.wsdl @@ -0,0 +1,113 @@ + + + Definition for a web service called BookService, + which can be used to add or retrieve books from a collection. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This binding defines the SOAP over HTTP transport for BookService operations. + + + This operation retrieves details for a specific book identified by its ID. + + Detailed Soap Operation documentation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/api/test-api.yml b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/api/test-api.yml new file mode 100644 index 0000000000..eea3f01335 --- /dev/null +++ b/test-api-generator/citrus-test-api-generator-maven-plugin/src/test/resources/api/test-api.yml @@ -0,0 +1,125 @@ +openapi: 3.0.3 +info: + title: Schema Test API + version: 1.0.0 + description: | + A very simple test OpenAPI specification that is compliant with the random response generator (e.g. only contains responses of + media-type `application/json`). + +servers: + - url: http://localhost:9000/services/rest/ping/v1 + - url: http://localhost:9000/ping/v1 + +paths: + /ping/{id}: + put: + tags: + - ping + summary: Do the ping + operationId: doPing + parameters: + - name: id + in: path + description: Id to ping + required: true + explode: true + schema: + type: integer + format: int64 + - name: q1 + in: query + description: Some queryParameter + required: true + explode: true + schema: + type: integer + format: int64 + - name: api_key + in: header + required: true + schema: + type: string + requestBody: + description: ping data + content: + application/json: + schema: + $ref: '#/components/schemas/PingReqType' + required: true + responses: + 200: + description: successful operation + headers: + Ping-Time: + required: false + description: response time + schema: + type: integer + format: int64 + content: + application/json: + schema: + $ref: '#/components/schemas/PingRespType' + 201: + description: successful operation + headers: + Ping-Time: + required: false + description: response time + schema: + type: integer + format: int64 + content: + application/xml: + schema: + $ref: '#/components/schemas/PingRespType' + application/json: + schema: + $ref: '#/components/schemas/PingRespType' + 400: + description: Some error + content: + text/plain: + schema: + type: string + /pung/{id}: + get: + tags: + - pung + summary: Do the pung + operationId: doPung + parameters: + - name: id + in: path + description: Id to pung + required: true + explode: true + schema: + type: integer + format: int64 + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/PingRespType' + 400: + description: Invalid status value + content: {} +components: + schemas: + PingReqType: + type: object + properties: + id: + type: integer + format: int64 + PingRespType: + type: object + properties: + id: + type: integer + format: int64 + value: + type: string diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/test-classes/SpringMetaFileGeneratorTest/META-INF/spring.handlers b/test-api-generator/citrus-test-api-generator-maven-plugin/test-classes/SpringMetaFileGeneratorTest/META-INF/spring.handlers new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test-api-generator/citrus-test-api-generator-maven-plugin/test-classes/SpringMetaFileGeneratorTest/META-INF/spring.schemas b/test-api-generator/citrus-test-api-generator-maven-plugin/test-classes/SpringMetaFileGeneratorTest/META-INF/spring.schemas new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test-api-generator/pom.xml b/test-api-generator/pom.xml new file mode 100644 index 0000000000..017243f68b --- /dev/null +++ b/test-api-generator/pom.xml @@ -0,0 +1,79 @@ + + 4.0.0 + + + org.citrusframework + citrus + 4.6.0-SNAPSHOT + ../pom.xml + + + citrus-test-api-generator + Citrus :: Test API Generator + Citrus Test API Generator + pom + + + citrus-test-api-core + citrus-test-api-generator-core + citrus-test-api-generator-maven-plugin + + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + + org.junit.jupiter + junit-jupiter-params + ${junit.jupiter.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + *IntegrationTest.java + *IT.java + + + + + org.apache.maven.surefire + surefire-junit-platform + ${maven.surefire.plugin.version} + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + *IntegrationTest.java + *IT.java + + + + + org.apache.maven.surefire + surefire-junit-platform + ${maven.surefire.plugin.version} + + + + + + diff --git a/tools/jbang/dist/CitrusJBang.java b/tools/jbang/dist/CitrusJBang.java index 665c9e924d..c2d5c5a44e 100755 --- a/tools/jbang/dist/CitrusJBang.java +++ b/tools/jbang/dist/CitrusJBang.java @@ -18,8 +18,8 @@ //JAVA 17+ //REPOS mavencentral -//DEPS org.citrusframework:citrus-bom:${citrus.jbang.version:4.5.2}@pom -//DEPS org.citrusframework:citrus-jbang:${citrus.jbang.version:4.5.2} +//DEPS org.citrusframework:citrus-bom:${citrus.jbang.version:4.6.0-SNAPSHOT}@pom +//DEPS org.citrusframework:citrus-jbang:${citrus.jbang.version:4.6.0-SNAPSHOT} //DEPS org.citrusframework:citrus-jbang-connector //DEPS org.citrusframework:citrus-groovy //DEPS org.citrusframework:citrus-xml diff --git a/validation/citrus-validation-json/src/main/java/org/citrusframework/json/JsonSchemaRepository.java b/validation/citrus-validation-json/src/main/java/org/citrusframework/json/JsonSchemaRepository.java index 754bb63652..08fa5e8cce 100644 --- a/validation/citrus-validation-json/src/main/java/org/citrusframework/json/JsonSchemaRepository.java +++ b/validation/citrus-validation-json/src/main/java/org/citrusframework/json/JsonSchemaRepository.java @@ -16,92 +16,48 @@ package org.citrusframework.json; -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -import org.citrusframework.common.InitializingPhase; -import org.citrusframework.common.Named; -import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.json.schema.SimpleJsonSchema; -import org.citrusframework.spi.ClasspathResourceResolver; +import org.citrusframework.repository.BaseRepository; import org.citrusframework.spi.Resource; -import org.citrusframework.spi.Resources; -import org.citrusframework.util.FileUtils; -import org.citrusframework.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; + /** * Schema repository holding a set of json schema resources known in the test scope. * @since 2.7.3 */ -public class JsonSchemaRepository implements Named, InitializingPhase { - - /** This repositories name in the Spring application context */ - private String name; - - /** List of schema resources */ - private List schemas = new ArrayList<>(); - - /** List of location patterns that will be translated to schema resources */ - private List locations = new ArrayList<>(); +public class JsonSchemaRepository extends BaseRepository { /** Logger */ private static Logger logger = LoggerFactory.getLogger(JsonSchemaRepository.class); - @Override - public void setName(String name) { - this.name = name; - } + private static final String DEFAULT_NAME = "jsonSchemaRepository"; - @Override - public void initialize() { - try { - ClasspathResourceResolver resourceResolver = new ClasspathResourceResolver(); - for (String location : locations) { - Resource found = Resources.create(location); - if (found.exists()) { - addSchemas(found); - } else { - Set findings; - if (StringUtils.hasText(FileUtils.getFileExtension(location))) { - String fileNamePattern = FileUtils.getFileName(location).replace(".", "\\.").replace("*", ".*"); - String basePath = FileUtils.getBasePath(location); - findings = resourceResolver.getResources(basePath, fileNamePattern); - } else { - findings = resourceResolver.getResources(location); - } + /** List of schema resources */ + private List schemas = new ArrayList<>(); - for (Path resource : findings) { - addSchemas(Resources.fromClasspath(resource.toString())); - } - } - } - } catch (IOException e) { - throw new CitrusRuntimeException("Failed to initialize Json schema repository", e); - } + public JsonSchemaRepository() { + super(DEFAULT_NAME); } - private void addSchemas(Resource resource) { + @Override + protected void addRepository(Resource resource) { if (resource.getLocation().endsWith(".json")) { if (logger.isDebugEnabled()) { - logger.debug("Loading json schema resource " + resource.getLocation()); + logger.debug("Loading json schema resource '{}'", resource.getLocation()); } + SimpleJsonSchema simpleJsonSchema = new SimpleJsonSchema(resource); simpleJsonSchema.initialize(); schemas.add(simpleJsonSchema); } else { - logger.warn("Skipped resource other than json schema for repository (" + resource.getLocation() + ")"); + logger.warn("Skipped resource other than json schema for repository '{}'", resource.getLocation()); } } - public String getName() { - return name; - } - public List getSchemas() { return schemas; } @@ -118,11 +74,7 @@ public static void setLog(Logger logger) { JsonSchemaRepository.logger = logger; } - public List getLocations() { - return locations; - } - - public void setLocations(List locations) { - this.locations = locations; + public void addSchema(SimpleJsonSchema simpleJsonSchema) { + schemas.add(simpleJsonSchema); } } diff --git a/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/schema/JsonSchemaValidation.java b/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/schema/JsonSchemaValidation.java index 8cdfc89673..f80f146670 100644 --- a/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/schema/JsonSchemaValidation.java +++ b/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/schema/JsonSchemaValidation.java @@ -16,11 +16,10 @@ package org.citrusframework.validation.json.schema; -import static java.util.Collections.emptySet; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.ValidationMessage; +import org.citrusframework.CitrusSettings; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.exceptions.ValidationException; @@ -31,6 +30,7 @@ import org.citrusframework.util.IsJsonPredicate; import org.citrusframework.validation.SchemaValidator; import org.citrusframework.validation.json.JsonMessageValidationContext; +import org.citrusframework.validation.json.JsonMessageValidationContext.Builder; import org.citrusframework.validation.json.report.GraciousProcessingReport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +40,8 @@ import java.util.List; import java.util.Set; +import static java.util.Collections.emptySet; + /** * This class is responsible for the validation of json messages against json schemas / json schema repositories. * @@ -71,7 +73,10 @@ public void validate(Message message, TestContext context, JsonMessageValidation context.getReferenceResolver()); if (!report.isSuccess()) { - logger.error("Failed to validate Json schema for message:\n{}", message.getPayload(String.class)); + if (logger.isErrorEnabled()) { + logger.error("Failed to validate Json schema for message:\n{}", + message.getPayload(String.class)); + } throw new ValidationException(constructErrorMessage(report)); } @@ -167,4 +172,30 @@ public boolean supportsMessageType(String messageType, Message message) { return "JSON".equals(messageType) || (message != null && IsJsonPredicate.getInstance().test(message.getPayload(String.class))); } + + @Override + public boolean canValidate(Message message, boolean schemaValidationEnabled) { + return (isJsonSchemaValidationEnabled() || schemaValidationEnabled) + && IsJsonPredicate.getInstance().test(message.getPayload(String.class)); + } + + /** + * Get setting to determine if json schema validation is enabled by default. + * @return + */ + private static boolean isJsonSchemaValidationEnabled() { + return Boolean.getBoolean(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_PROPERTY) + || Boolean.getBoolean(CitrusSettings.OUTBOUND_JSON_SCHEMA_VALIDATION_ENABLED_PROPERTY) + || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_ENV)) + || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_JSON_SCHEMA_VALIDATION_ENABLED_ENV)); + } + + @Override + public void validate(Message message, TestContext context, String schemaRepository, String schema) { + JsonMessageValidationContext validationContext = Builder.json() + .schemaValidation(true) + .schema(schema) + .schemaRepository(schemaRepository).build(); + validate(message, context, validationContext); + } } diff --git a/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/SendMessageActionTest.java b/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/SendMessageActionTest.java index c4850d40c1..e787b41f92 100644 --- a/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/SendMessageActionTest.java +++ b/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/SendMessageActionTest.java @@ -16,10 +16,6 @@ package org.citrusframework.validation.json; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - import org.citrusframework.actions.SendMessageAction; import org.citrusframework.context.TestContext; import org.citrusframework.context.TestContextFactory; @@ -30,6 +26,7 @@ import org.citrusframework.message.MessageType; import org.citrusframework.message.builder.DefaultPayloadBuilder; import org.citrusframework.messaging.Producer; +import org.citrusframework.spi.ReferenceResolver; import org.citrusframework.testng.AbstractTestNGUnitTest; import org.citrusframework.validation.DefaultMessageHeaderValidator; import org.citrusframework.validation.MessageValidatorRegistry; @@ -40,11 +37,18 @@ import org.testng.Assert; import org.testng.annotations.Test; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; public class SendMessageActionTest extends AbstractTestNGUnitTest { @@ -70,10 +74,11 @@ public void testSendMessageOverwriteMessageElementsJsonPath() { overwriteElements.put("$.TestRequest.Message", "Hello World!"); JsonPathMessageProcessor processor = new JsonPathMessageProcessor.Builder() - .expressions(overwriteElements) - .build(); + .expressions(overwriteElements) + .build(); - final Message controlMessage = new DefaultMessage("{\"TestRequest\":{\"Message\":\"Hello World!\"}}"); + final Message controlMessage = new DefaultMessage( + "{\"TestRequest\":{\"Message\":\"Hello World!\"}}"); reset(endpoint, producer, endpointConfiguration); when(endpoint.createProducer()).thenReturn(producer); @@ -87,31 +92,37 @@ public void testSendMessageOverwriteMessageElementsJsonPath() { when(endpoint.getActor()).thenReturn(null); SendMessageAction sendAction = new SendMessageAction.Builder() - .endpoint(endpoint) - .message(messageBuilder) - .type(MessageType.JSON) - .process(processor) - .build(); + .endpoint(endpoint) + .message(messageBuilder) + .type(MessageType.JSON) + .process(processor) + .build(); sendAction.execute(context); - } @Test public void testSendJsonMessageWithValidation() { - - AtomicBoolean validated = new AtomicBoolean(false); + AtomicBoolean validated = new AtomicBoolean(false); SchemaValidator schemaValidator = mock(SchemaValidator.class); when(schemaValidator.supportsMessageType(eq("JSON"), any())).thenReturn(true); - doAnswer(invocation-> { - JsonMessageValidationContext argument = invocation.getArgument(2, JsonMessageValidationContext.class); + doAnswer(invocation -> { - Assert.assertEquals(argument.getSchema(), "fooSchema"); - Assert.assertEquals(argument.getSchemaRepository(), "fooRepository"); + Assert.assertEquals(invocation.getArgument(3, String.class), "fooSchema"); + Assert.assertEquals(invocation.getArgument(2, String.class), "fooRepository"); validated.set(true); + return null; - }).when(schemaValidator).validate(any(), any(), any()); + }).when(schemaValidator) + .validate(isA(Message.class), isA(TestContext.class), isA(String.class), + isA(String.class)); + doReturn(true).when(schemaValidator).canValidate(isA(Message.class), isA(Boolean.class)); + + ReferenceResolver referenceResolverSpy = spy(context.getReferenceResolver()); + context.setReferenceResolver(referenceResolverSpy); + + doReturn(Map.of("jsonSchemaValidator", schemaValidator)).when(referenceResolverSpy).resolveAll(SchemaValidator.class); context.getMessageValidatorRegistry().addSchemaValidator("JSON", schemaValidator); @@ -125,20 +136,21 @@ public void testSendJsonMessageWithValidation() { when(endpoint.getActor()).thenReturn(null); SendMessageAction sendAction = new SendMessageAction.Builder() - .endpoint(endpoint) - .message(messageBuilder) - .schemaValidation(true) - .schema("fooSchema") - .schemaRepository("fooRepository") - .type(MessageType.JSON) - .build(); + .endpoint(endpoint) + .message(messageBuilder) + .schemaValidation(true) + .schema("fooSchema") + .schemaRepository("fooRepository") + .type(MessageType.JSON) + .build(); sendAction.execute(context); Assert.assertTrue(validated.get()); } private void validateMessageToSend(Message toSend, Message controlMessage) { - Assert.assertEquals(toSend.getPayload(String.class).trim(), controlMessage.getPayload(String.class).trim()); + Assert.assertEquals(toSend.getPayload(String.class).trim(), + controlMessage.getPayload(String.class).trim()); DefaultMessageHeaderValidator validator = new DefaultMessageHeaderValidator(); validator.validateMessage(toSend, controlMessage, context, new HeaderValidationContext()); } diff --git a/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/schema/JsonSchemaValidationTest.java b/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/schema/JsonSchemaValidationTest.java index 9ca46b5290..1ec1183b83 100644 --- a/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/schema/JsonSchemaValidationTest.java +++ b/validation/citrus-validation-json/src/test/java/org/citrusframework/validation/json/schema/JsonSchemaValidationTest.java @@ -34,6 +34,7 @@ import org.citrusframework.validation.json.report.GraciousProcessingReport; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -58,12 +59,21 @@ public class JsonSchemaValidationTest { private JsonSchemaValidation fixture; + private AutoCloseable mocks; + @BeforeMethod void beforeMethodSetup() { - MockitoAnnotations.openMocks(this); + mocks = MockitoAnnotations.openMocks(this); fixture = new JsonSchemaValidation(jsonSchemaFilterMock); } + @AfterMethod + void afterMethod() throws Exception { + if (mocks != null) { + mocks.close(); + } + } + @Test public void testValidJsonMessageSuccessfullyValidated() { // Setup json schema repositories @@ -263,7 +273,7 @@ public void testJsonSchemaFilterIsCalled() { @Test public void testLookup() { Map> validators = SchemaValidator.lookup(); - assertEquals(validators.size(), 1L); + assertEquals(1L, validators.size()); assertNotNull(validators.get("defaultJsonSchemaValidator")); assertEquals(validators.get("defaultJsonSchemaValidator").getClass(), JsonSchemaValidation.class); } diff --git a/validation/citrus-validation-text/src/main/java/org/citrusframework/validation/text/PlainTextMessageValidator.java b/validation/citrus-validation-text/src/main/java/org/citrusframework/validation/text/PlainTextMessageValidator.java index 42e53308e2..b6b4dde371 100644 --- a/validation/citrus-validation-text/src/main/java/org/citrusframework/validation/text/PlainTextMessageValidator.java +++ b/validation/citrus-validation-text/src/main/java/org/citrusframework/validation/text/PlainTextMessageValidator.java @@ -16,9 +16,6 @@ package org.citrusframework.validation.text; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import org.citrusframework.CitrusSettings; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.ValidationException; @@ -29,8 +26,12 @@ import org.citrusframework.validation.context.ValidationContext; import org.citrusframework.validation.matcher.ValidationMatcherUtils; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import static java.lang.Boolean.parseBoolean; import static java.lang.Integer.parseInt; +import static org.citrusframework.message.MessagePayloadUtils.normalizeWhitespace; /** * Plain text validator using simple String comparison. @@ -58,8 +59,8 @@ public void validateMessage(Message receivedMessage, Message controlMessage, Tes logger.debug("Start text message validation"); try { - String resultValue = normalizeWhitespace(receivedMessage.getPayload(String.class).trim()); - String controlValue = normalizeWhitespace(context.replaceDynamicContentInString(controlMessage.getPayload(String.class).trim())); + String resultValue = normalizeWhitespace(receivedMessage.getPayload(String.class).trim(), ignoreWhitespace, ignoreNewLineType); + String controlValue = normalizeWhitespace(context.replaceDynamicContentInString(controlMessage.getPayload(String.class).trim()), ignoreWhitespace, ignoreNewLineType); controlValue = processIgnoreStatements(controlValue, resultValue); controlValue = processVariableStatements(controlValue, resultValue, context); @@ -180,38 +181,6 @@ private void validateText(String receivedMessagePayload, String controlMessagePa } } - /** - * Normalize whitespace characters if appropriate. Based on system property settings this method normalizes - * new line characters exclusively or filters all whitespaces such as double whitespaces and new lines. - * - * @param payload - * @return - */ - private String normalizeWhitespace(String payload) { - if (ignoreWhitespace) { - StringBuilder result = new StringBuilder(); - boolean lastWasSpace = true; - for (int i = 0; i < payload.length(); i++) { - char c = payload.charAt(i); - if (Character.isWhitespace(c)) { - if (!lastWasSpace) { - result.append(' '); - } - lastWasSpace = true; - } else { - result.append(c); - lastWasSpace = false; - } - } - return result.toString().trim(); - } - - if (ignoreNewLineType) { - return payload.replaceAll("\\r(\\n)?", "\n"); - } - - return payload; - } @Override public boolean supportsMessageType(String messageType, Message message) { diff --git a/validation/citrus-validation-xml/src/main/java/org/citrusframework/util/XMLUtils.java b/validation/citrus-validation-xml/src/main/java/org/citrusframework/util/XMLUtils.java index ee01180dbd..0c2d9857ea 100644 --- a/validation/citrus-validation-xml/src/main/java/org/citrusframework/util/XMLUtils.java +++ b/validation/citrus-validation-xml/src/main/java/org/citrusframework/util/XMLUtils.java @@ -16,6 +16,8 @@ package org.citrusframework.util; +import static org.citrusframework.util.StringUtils.isEmpty; + import java.io.ByteArrayInputStream; import java.io.StringWriter; import java.io.UnsupportedEncodingException; @@ -63,7 +65,6 @@ private XMLUtils() { /** * Initializes XML utilities with custom configurer. - * @param xmlConfigurer */ public static void initialize(XmlConfigurer xmlConfigurer) { configurer = xmlConfigurer; @@ -71,7 +72,6 @@ public static void initialize(XmlConfigurer xmlConfigurer) { /** * Creates basic parser instance. - * @return */ public static LSParser createLSParser() { return configurer.createLSParser(); @@ -79,7 +79,6 @@ public static LSParser createLSParser() { /** * Creates basic serializer instance. - * @return */ public static LSSerializer createLSSerializer() { return configurer.createLSSerializer(); @@ -87,7 +86,6 @@ public static LSSerializer createLSSerializer() { /** * Creates LSInput from dom implementation. - * @return */ public static LSInput createLSInput() { return configurer.createLSInput(); @@ -95,7 +93,6 @@ public static LSInput createLSInput() { /** * Creates LSOutput from dom implementation. - * @return */ public static LSOutput createLSOutput() { return configurer.createLSOutput(); @@ -165,13 +162,14 @@ public static Node findNodeByName(Document doc, String pathExpression) { * @param element the root node to normalize. */ public static void stripWhitespaceNodes(Node element) { - Node node, child; + Node node; + Node child; for (child = element.getFirstChild(); child != null; child = node) { node = child.getNextSibling(); stripWhitespaceNodes(child); } - if (element.getNodeType() == Node.TEXT_NODE && element.getNodeValue().trim().length()==0) { + if (element.getNodeType() == Node.TEXT_NODE && element.getNodeValue().trim().isEmpty()) { element.getParentNode().removeChild(element); } } @@ -185,43 +183,42 @@ public static void stripWhitespaceNodes(Node element) { * @return the path expression representing the node in DOM tree. */ public static String getNodesPathName(Node node) { - final StringBuffer buffer = new StringBuffer(); + final StringBuilder builder = new StringBuilder(); if (node.getNodeType() == Node.ATTRIBUTE_NODE) { - buildNodeName(((Attr) node).getOwnerElement(), buffer); - buffer.append("."); - buffer.append(node.getLocalName()); + buildNodeName(((Attr) node).getOwnerElement(), builder); + builder.append("."); + builder.append(node.getLocalName()); } else { - buildNodeName(node, buffer); + buildNodeName(node, builder); } - return buffer.toString(); + return builder.toString(); } /** * Builds the node path expression for a node in the DOM tree. * @param node in a DOM tree. - * @param buffer string buffer. + * @param builder string builder. */ - private static void buildNodeName(Node node, StringBuffer buffer) { + private static void buildNodeName(Node node, StringBuilder builder) { if (node.getParentNode() == null) { return; } - buildNodeName(node.getParentNode(), buffer); + buildNodeName(node.getParentNode(), builder); if (node.getParentNode() != null && node.getParentNode().getParentNode() != null) { - buffer.append("."); + builder.append("."); } - buffer.append(node.getLocalName()); + builder.append(node.getLocalName()); } /** * Serializes a DOM document - * @param doc - * @throws CitrusRuntimeException + * * @return serialized XML string */ public static String serialize(Document doc) { @@ -240,12 +237,15 @@ public static String serialize(Document doc) { } /** - * Pretty prints a XML string. - * @param xml - * @throws CitrusRuntimeException + * Pretty prints an XML string. * @return pretty printed XML string */ public static String prettyPrint(String xml) { + + if (isEmpty(xml)) { + return xml; + } + LSParser parser = configurer.createLSParser(); configurer.setParserConfigParameter(parser, XmlConfigurer.VALIDATE_IF_SCHEMA, false); @@ -306,8 +306,6 @@ public static Map lookupNamespaces(Node referenceNode) { /** * Parse message payload with DOM implementation. - * @param messagePayload - * @throws CitrusRuntimeException * @return DOM document. */ public static Document parseMessagePayload(String messagePayload) { @@ -327,8 +325,6 @@ public static Document parseMessagePayload(String messagePayload) { /** * Try to find encoding for document node. Also supports Citrus default encoding set * as System property. - * @param doc - * @return */ public static Charset getTargetCharset(Document doc) { String defaultEncoding = System.getProperty(CitrusSettings.CITRUS_FILE_ENCODING_PROPERTY, @@ -397,8 +393,6 @@ private static Charset getTargetCharset(String messagePayload) throws Unsupporte /** * Removes leading XML declaration from xml if present. - * @param xml - * @return */ public static String omitXmlDeclaration(String xml) { if (xml.startsWith("")) { diff --git a/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/DomXmlMessageValidator.java b/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/DomXmlMessageValidator.java index 8609a53463..2ff3c013eb 100644 --- a/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/DomXmlMessageValidator.java +++ b/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/DomXmlMessageValidator.java @@ -78,7 +78,12 @@ public DomXmlMessageValidator(XmlSchemaValidation schemaValidator) { @Override public void validateMessage(Message receivedMessage, Message controlMessage, TestContext context, XmlMessageValidationContext validationContext) throws ValidationException { - logger.debug("Start XML message validation: {}", prettyPrint(receivedMessage.getPayload(String.class))); + + if (logger.isDebugEnabled()) { + logger.debug("Start XML message validation: {}", + prettyPrint(receivedMessage.getPayload(String.class))); + } + try { if (validationContext.isSchemaValidationEnabled()) { schemaValidator.validate(receivedMessage, context, validationContext); @@ -110,9 +115,6 @@ public void validateMessage(Message receivedMessage, Message controlMessage, * Validate namespaces in message. The method compares namespace declarations in the root * element of the received message to expected namespaces. Prefixes are important too, so * differing namespace prefixes will fail the validation. - * - * @param expectedNamespaces - * @param receivedMessage */ protected void validateNamespaces(Map expectedNamespaces, Message receivedMessage) { if (expectedNamespaces == null || expectedNamespaces.isEmpty()) { @@ -188,10 +190,6 @@ private void doElementNamespaceValidation(Node received, Node source) { /** * Validate message payloads by comparing to a control message. - * - * @param receivedMessage - * @param validationContext - * @param context */ protected void validateMessageContent(Message receivedMessage, Message controlMessage, XmlMessageValidationContext validationContext, TestContext context) { @@ -232,10 +230,6 @@ protected void validateMessageContent(Message receivedMessage, Message controlMe /** * Validates XML header fragment data. - * @param receivedHeaderData - * @param controlHeaderData - * @param validationContext - * @param context */ private void validateXmlHeaderFragment(String receivedHeaderData, String controlHeaderData, XmlMessageValidationContext validationContext, TestContext context) { @@ -257,10 +251,6 @@ private void validateXmlHeaderFragment(String receivedHeaderData, String control /** * Walk the XML tree and validate all nodes. - * - * @param received - * @param source - * @param validationContext */ private void validateXmlTree(Node received, Node source, XmlMessageValidationContext validationContext, NamespaceContext namespaceContext, TestContext context) { @@ -289,10 +279,6 @@ private void validateXmlTree(Node received, Node source, /** * Handle document type definition with validation of publicId and systemId. - * @param received - * @param source - * @param validationContext - * @param namespaceContext */ private void doDocumentTypeDefinition(Node received, Node source, XmlMessageValidationContext validationContext, @@ -336,10 +322,6 @@ private void doDocumentTypeDefinition(Node received, Node source, /** * Handle element node. - * - * @param received - * @param source - * @param validationContext */ private void doElement(Node received, Node source, XmlMessageValidationContext validationContext, NamespaceContext namespaceContext, TestContext context) { @@ -397,9 +379,6 @@ private void doElement(Node received, Node source, /** * Handle text node during validation. - * - * @param received - * @param source */ private void doText(Element received, Element source) { logger.debug("Validating node value for element: {}", received.getLocalName()); @@ -412,16 +391,13 @@ private void doText(Element received, Element source) { + received.getLocalName() + "'", sourceText.trim(), receivedText.trim())); } - logger.debug("Node value '{}': OK", receivedText.trim()); + if (logger.isDebugEnabled()) { + logger.debug("Node value '{}': OK", receivedText.trim()); + } } /** * Handle attribute node during validation. - * - * @param receivedElement - * @param receivedAttribute - * @param sourceElement - * @param validationContext */ private void doAttribute(Node receivedElement, Node receivedAttribute, Node sourceElement, XmlMessageValidationContext validationContext, NamespaceContext namespaceContext, TestContext context) { @@ -466,10 +442,6 @@ private void doAttribute(Node receivedElement, Node receivedAttribute, Node sour /** * Perform validation on namespace qualified attribute values if present. This includes the validation of namespace presence * and equality. - * @param receivedElement - * @param receivedAttribute - * @param sourceElement - * @param sourceAttribute */ private void doNamespaceQualifiedAttributeValidation(Node receivedElement, Node receivedAttribute, Node sourceElement, Node sourceAttribute) { String receivedValue = receivedAttribute.getNodeValue(); @@ -511,8 +483,6 @@ private void doNamespaceQualifiedAttributeValidation(Node receivedElement, Node /** * Handle processing instruction during validation. - * - * @param received */ private void doPI(Node received) { logger.debug("Ignored processing instruction ({}={})", received.getLocalName(), received.getNodeValue()); @@ -537,7 +507,6 @@ private int countAttributes(NamedNodeMap attributesR) { /** * Checks whether the given node contains a validation matcher - * @param node * @return true if node value contains validation matcher, false if not */ private boolean isValidationMatcherExpression(Node node) { @@ -565,8 +534,6 @@ public boolean supportsMessageType(String messageType, Message message) { /** * Get explicit namespace context builder set on this class or obtain instance from reference resolver. - * @param context - * @return */ private NamespaceContextBuilder getNamespaceContextBuilder(TestContext context) { if (namespaceContextBuilder != null) { @@ -578,7 +545,6 @@ private NamespaceContextBuilder getNamespaceContextBuilder(TestContext context) /** * Sets the namespace context builder. - * @param namespaceContextBuilder */ public void setNamespaceContextBuilder(NamespaceContextBuilder namespaceContextBuilder) { this.namespaceContextBuilder = namespaceContextBuilder; diff --git a/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/schema/XmlSchemaValidation.java b/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/schema/XmlSchemaValidation.java index 75d9a67bc4..d0182d4de2 100644 --- a/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/schema/XmlSchemaValidation.java +++ b/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/schema/XmlSchemaValidation.java @@ -16,6 +16,7 @@ package org.citrusframework.validation.xml.schema; +import org.citrusframework.CitrusSettings; import org.citrusframework.XmlValidationHelper; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; @@ -29,6 +30,7 @@ import org.citrusframework.util.XMLUtils; import org.citrusframework.validation.SchemaValidator; import org.citrusframework.validation.xml.XmlMessageValidationContext; +import org.citrusframework.validation.xml.XmlMessageValidationContext.Builder; import org.citrusframework.xml.XsdSchemaRepository; import org.citrusframework.xml.schema.AbstractSchemaCollection; import org.citrusframework.xml.schema.WsdlXsdSchema; @@ -53,6 +55,7 @@ import static java.lang.String.format; import static org.citrusframework.validation.xml.schema.ValidationStrategy.FAIL; +import static org.citrusframework.xml.schema.AbstractSchemaCollection.W3C_XML_SCHEMA_NS_URI; public class XmlSchemaValidation implements SchemaValidator { @@ -85,10 +88,6 @@ public XmlSchemaValidation(ValidationStrategy noSchemaFoundStrategy) { /** * Validate message with an XML schema. - * - * @param message - * @param context - * @param validationContext */ @Override public void validate(Message message, TestContext context, XmlMessageValidationContext validationContext) { @@ -160,14 +159,18 @@ private void validateSchema(Message message, TestContext context, XmlMessageVali .stream() .map(AbstractSchemaCollection::toSpringResource) .toList() - .toArray(new org.springframework.core.io.Resource[]{}), WsdlXsdSchema.W3C_XML_SCHEMA_NS_URI); + .toArray(new org.springframework.core.io.Resource[]{}), W3C_XML_SCHEMA_NS_URI); } SAXParseException[] results = validator.validate(new DOMSource(doc)); if (results.length == 0) { logger.debug("XML schema validation successful: All values OK"); } else { - logger.error("XML schema validation failed for message:\n{}", XMLUtils.prettyPrint(message.getPayload(String.class))); + + if (logger.isErrorEnabled()) { + logger.error("XML schema validation failed for message:\n{}", + XMLUtils.prettyPrint(message.getPayload(String.class))); + } // Report all parsing errors logger.debug("Found {} schema validation errors", results.length); @@ -176,7 +179,10 @@ private void validateSchema(Message message, TestContext context, XmlMessageVali errors.append(e.toString()); errors.append("\n"); } - logger.debug(errors.toString()); + + if (logger.isDebugEnabled()) { + logger.debug(errors.toString()); + } throw new ValidationException("XML schema validation failed:", results[0]); } @@ -186,9 +192,6 @@ private void validateSchema(Message message, TestContext context, XmlMessageVali } /** - * - * @param messageType - * @param message * @return true if the message or message type is supported by this validator */ @Override @@ -222,4 +225,29 @@ private static Optional extractEnvOrProperty(SystemProvider systemProvid return systemProvider.getEnv(envVarName) .or(() -> systemProvider.getProperty(fallbackPropertyName)); } + + @Override + public boolean canValidate(Message message, boolean schemaValidationEnabled) { + return (isXmlSchemaValidationEnabled() || schemaValidationEnabled) + && IsXmlPredicate.getInstance().test(message.getPayload(String.class)); + } + + /** + * Get setting to determine if xml schema validation is enabled by default. + */ + private static boolean isXmlSchemaValidationEnabled() { + return Boolean.getBoolean(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_PROPERTY) + || Boolean.getBoolean(CitrusSettings.OUTBOUND_XML_SCHEMA_VALIDATION_ENABLED_PROPERTY) + || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_SCHEMA_VALIDATION_ENABLED_ENV)) + || Boolean.parseBoolean(System.getenv(CitrusSettings.OUTBOUND_XML_SCHEMA_VALIDATION_ENABLED_ENV)); + } + + @Override + public void validate(Message message, TestContext context, String schemaRepository, String schema) { + XmlMessageValidationContext validationContext = Builder.xml() + .schemaValidation(true) + .schema(schema) + .schemaRepository(schemaRepository).build(); + validate(message, context, validationContext); + } } diff --git a/validation/citrus-validation-xml/src/main/java/org/citrusframework/xml/XsdSchemaRepository.java b/validation/citrus-validation-xml/src/main/java/org/citrusframework/xml/XsdSchemaRepository.java index a8847e9995..097c010d88 100644 --- a/validation/citrus-validation-xml/src/main/java/org/citrusframework/xml/XsdSchemaRepository.java +++ b/validation/citrus-validation-xml/src/main/java/org/citrusframework/xml/XsdSchemaRepository.java @@ -17,20 +17,14 @@ package org.citrusframework.xml; import java.io.IOException; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.Set; import javax.xml.parsers.ParserConfigurationException; - -import org.citrusframework.common.InitializingPhase; -import org.citrusframework.common.Named; import org.citrusframework.exceptions.CitrusRuntimeException; -import org.citrusframework.spi.ClasspathResourceResolver; +import org.citrusframework.repository.BaseRepository; import org.citrusframework.spi.Resource; import org.citrusframework.spi.Resources; import org.citrusframework.util.FileUtils; -import org.citrusframework.util.StringUtils; import org.citrusframework.xml.schema.TargetNamespaceSchemaMappingStrategy; import org.citrusframework.xml.schema.WsdlXsdSchema; import org.citrusframework.xml.schema.XsdSchemaMappingStrategy; @@ -47,22 +41,23 @@ * */ @SuppressWarnings("unused") -public class XsdSchemaRepository implements Named, InitializingPhase { - /** The name of the repository */ - private String name = "schemaRepository"; +public class XsdSchemaRepository extends BaseRepository { + + private static final String DEFAULT_NAME = "schemaRepository"; /** List of schema resources */ private List schemas = new ArrayList<>(); - /** List of location patterns that will be translated to schema resources */ - private List locations = new ArrayList<>(); - /** Mapping strategy */ private XsdSchemaMappingStrategy schemaMappingStrategy = new TargetNamespaceSchemaMappingStrategy(); /** Logger */ private static final Logger logger = LoggerFactory.getLogger(XsdSchemaRepository.class); + public XsdSchemaRepository() { + super(DEFAULT_NAME); + } + /** * Find the matching schema for document using given schema mapping strategy. * @param doc the document instance to validate. @@ -75,28 +70,8 @@ public boolean canValidate(Document doc) { @Override public void initialize() { + super.initialize(); try { - ClasspathResourceResolver resourceResolver = new ClasspathResourceResolver(); - for (String location : locations) { - Resource found = Resources.create(location); - if (found.exists()) { - addSchemas(found); - } else { - Set findings; - if (StringUtils.hasText(FileUtils.getFileExtension(location))) { - String fileNamePattern = FileUtils.getFileName(location).replace(".", "\\.").replace("*", ".*"); - String basePath = FileUtils.getBasePath(location); - findings = resourceResolver.getResources(basePath, fileNamePattern); - } else { - findings = resourceResolver.getResources(location); - } - - for (Path resource : findings) { - addSchemas(Resources.fromClasspath(resource.toString())); - } - } - } - // Add default Citrus message schemas if available on classpath addCitrusSchema("citrus-http-message"); addCitrusSchema("citrus-mail-message"); @@ -104,7 +79,7 @@ public void initialize() { addCitrusSchema("citrus-ssh-message"); addCitrusSchema("citrus-rmi-message"); addCitrusSchema("citrus-jmx-message"); - } catch (SAXException | ParserConfigurationException | IOException e) { + } catch (SAXException | ParserConfigurationException e) { throw new CitrusRuntimeException("Failed to initialize Xsd schema repository", e); } } @@ -113,26 +88,27 @@ public void initialize() { * Adds Citrus message schema to repository if available on classpath. * @param schemaName The name of the schema within the citrus schema package */ - protected void addCitrusSchema(String schemaName) throws IOException, SAXException, ParserConfigurationException { + protected void addCitrusSchema(String schemaName) throws SAXException, ParserConfigurationException { Resource resource = Resources.fromClasspath("classpath:org/citrusframework/schema/" + schemaName + ".xsd"); if (resource.exists()) { addXsdSchema(resource); } } - private void addSchemas(Resource resource) { + @Override + protected void addRepository(Resource resource) { if (resource.getLocation().endsWith(".xsd")) { addXsdSchema(resource); } else if (resource.getLocation().endsWith(".wsdl")) { addWsdlSchema(resource); } else { - logger.warn("Skipped resource other than XSD schema for repository (" + resource.getLocation() + ")"); + logger.warn("Skipped resource other than XSD schema for repository '{}'", resource.getLocation()); } } private void addWsdlSchema(Resource resource) { if (logger.isDebugEnabled()) { - logger.debug("Loading WSDL schema resource " + resource.getLocation()); + logger.debug("Loading WSDL schema resource '{}'", resource.getLocation()); } WsdlXsdSchema wsdl = new WsdlXsdSchema(resource); @@ -142,7 +118,7 @@ private void addWsdlSchema(Resource resource) { private void addXsdSchema(Resource resource) { if (logger.isDebugEnabled()) { - logger.debug("Loading XSD schema resource " + resource.getLocation()); + logger.debug("Loading XSD schema resource '{}'", resource.getLocation()); } SimpleXsdSchema schema = new SimpleXsdSchema(new ByteArrayResource(FileUtils.copyToByteArray(resource))); @@ -185,33 +161,4 @@ public void setSchemaMappingStrategy(XsdSchemaMappingStrategy schemaMappingStrat public XsdSchemaMappingStrategy getSchemaMappingStrategy() { return schemaMappingStrategy; } - - @Override - public void setName(String name) { - this.name = name; - } - - /** - * Gets the name. - * @return the name to get. - */ - public String getName() { - return name; - } - - /** - * Gets the locations. - * @return the locations to get. - */ - public List getLocations() { - return locations; - } - - /** - * Sets the locations. - * @param locations the locations to set - */ - public void setLocations(List locations) { - this.locations = locations; - } } diff --git a/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/SendMessageActionTest.java b/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/SendMessageActionTest.java index a5da64c31b..871c37301c 100644 --- a/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/SendMessageActionTest.java +++ b/validation/citrus-validation-xml/src/test/java/org/citrusframework/validation/xml/SendMessageActionTest.java @@ -234,15 +234,14 @@ public void testSendXmlMessageWithValidation() { when(schemaValidator.supportsMessageType(eq("XML"), any())).thenReturn(true); doAnswer(invocation-> { - Object argument = invocation.getArgument(2); - - Assert.assertTrue(argument instanceof XmlMessageValidationContext); - Assert.assertEquals(((XmlMessageValidationContext)argument).getSchema(), "fooSchema"); - Assert.assertEquals(((XmlMessageValidationContext)argument).getSchemaRepository(), "fooRepository"); + Assert.assertEquals(invocation.getArgument(3, String.class), "fooSchema"); + Assert.assertEquals(invocation.getArgument(2, String.class), "fooRepository"); validated.set(true); return null; - }).when(schemaValidator).validate(any(), any(), any()); + }).when(schemaValidator) + .validate(isA(Message.class), isA(TestContext.class), isA(String.class), isA(String.class)); + doReturn(true).when(schemaValidator).canValidate(isA(Message.class), isA(Boolean.class)); context.getMessageValidatorRegistry().addSchemaValidator("XML", schemaValidator);