diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..30f5741
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,17 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+
+## [1.1.0-beta-1] - 2026-07-02
+
+### Added
+- `readOnly`: when `true`, only generates GET and OPTIONS test cases.
+- `serverPattern`: selects the OpenAPI server whose URL matches the substring (e.g. `%dev%`); defaults to the first server if none match, or all servers if omitted.
+- `minimalEndpoints`: when `false` (default), adds a valid (200) and an invalid (400) test case per optional query parameter. When `true`, only generates the `testCaseNames`-based cases.
+- `microcksHeaders`: when `true`, adds an `X-Microcks-Response-Name` header with the matching response example name (or `default`).
+- `generateOneOfAnyOf`: when `true`, resolves `oneOf`/`anyOf` to their first candidate when generating example bodies (`allOf` is always merged).
+- `examples`: optional `{ successful, wrong }` object with custom `string`/`number`/`boolean`/`date`/`dateTime` values. `successful` feeds example bodies and valid query-param values; `wrong` feeds the invalid query-param values from `minimalEndpoints`. Unset fields keep the existing defaults.
diff --git a/pom.xml b/pom.xml
index 93fc651..fa29395 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
net.cloudappi
openapi2soapui
- 1.0.3
+ 1.1.0-beta-1
${packaging.type}
openapi2soapui
@@ -52,7 +52,8 @@
1.5.6
5.6.0
- 2.0.24
+ 2.1.39
+ 1.18.30
diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java
index 1822b67..3d3a90a 100644
--- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java
+++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java
@@ -30,4 +30,12 @@ private Constants() {
public static final String HEADERS_KEY = "headers";
public static final String AUTHENTICATION_PROFILES_KEY = "oAuth2Profiles";
public static final String TEST_CASE_NAMES_KEY = "testCaseNames";
+
+ public static final String VALID_HTTP_STATUS_CODES_ASSERTION = "Valid HTTP Status Codes";
+ public static final String SUCCESS_STATUS_CODE = "200";
+ public static final String WRONG_STATUS_CODE = "400";
+ public static final String QUERY_PARAM_VARIANT_PREFIX = "queryString ";
+ public static final String QUERY_PARAM_VARIANT_WRONG_SUFFIX = " wrong";
+
+ public static final String MICROCKS_RESPONSE_NAME_HEADER = "X-Microcks-Response-Name";
}
diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java
index c263789..a0047a2 100644
--- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java
+++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java
@@ -34,8 +34,7 @@ public String newSoapUIProject(@Valid @RequestBody SoapUIProjectRequest newSoapU
if (openAPI != null && openAPI.getInfo() != null && openAPI.getInfo().getVersion() == null) {
throw new APIVersionNotFoundException("Version not found in OpenAPI");
}
- SoapUIProject soapUIProject = soapUIProjectService.createSoapUIProject(newSoapUIProject.getApiName(), openAPI,
- newSoapUIProject.getOAuth2Profiles(), newSoapUIProject.getHeaders(), newSoapUIProject.getTestCaseNames());
+ SoapUIProject soapUIProject = soapUIProjectService.createSoapUIProject(newSoapUIProject, openAPI);
String projectContent = soapUIProject.getFileContent();
soapUIProject.deleteTemporaryFile();
return projectContent;
diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java
index 12e0e48..c508a37 100644
--- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java
+++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java
@@ -13,6 +13,12 @@
import static org.apiaddicts.apitools.openapi2soapui.constants.Constants.DEFAULT;
import static org.apiaddicts.apitools.openapi2soapui.constants.Constants.JSON;
import static org.apiaddicts.apitools.openapi2soapui.constants.Constants.SUCCESS_TEST_CASE;
+import static org.apiaddicts.apitools.openapi2soapui.constants.Constants.VALID_HTTP_STATUS_CODES_ASSERTION;
+import static org.apiaddicts.apitools.openapi2soapui.constants.Constants.SUCCESS_STATUS_CODE;
+import static org.apiaddicts.apitools.openapi2soapui.constants.Constants.WRONG_STATUS_CODE;
+import static org.apiaddicts.apitools.openapi2soapui.constants.Constants.QUERY_PARAM_VARIANT_PREFIX;
+import static org.apiaddicts.apitools.openapi2soapui.constants.Constants.QUERY_PARAM_VARIANT_WRONG_SUFFIX;
+import static org.apiaddicts.apitools.openapi2soapui.constants.Constants.MICROCKS_RESPONSE_NAME_HEADER;
import java.io.File;
import java.io.IOException;
@@ -20,10 +26,13 @@
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Date;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.net.MalformedURLException;
@@ -57,7 +66,10 @@
import com.eviware.soapui.impl.wsdl.WsdlProject;
import com.eviware.soapui.impl.wsdl.WsdlTestSuite;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCase;
+import com.eviware.soapui.impl.wsdl.teststeps.RestTestRequestStep;
+import com.eviware.soapui.impl.wsdl.teststeps.WsdlTestStep;
import com.eviware.soapui.impl.wsdl.teststeps.registry.RestRequestStepFactory;
+import com.eviware.soapui.security.assertion.ValidHttpStatusCodesAssertion;
import com.eviware.soapui.support.SoapUIException;
import com.eviware.soapui.support.types.StringToStringMap;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -86,6 +98,9 @@
import lombok.Getter;
import org.apiaddicts.apitools.openapi2soapui.request.GrantType;
import org.apiaddicts.apitools.openapi2soapui.request.Header;
+import org.apiaddicts.apitools.openapi2soapui.request.ExampleValues;
+import org.apiaddicts.apitools.openapi2soapui.request.ExamplesConfig;
+import org.apiaddicts.apitools.openapi2soapui.util.QueryParamExampleUtils;
import org.apiaddicts.apitools.openapi2soapui.util.RefResolver;
/**
@@ -126,7 +141,34 @@ public class SoapUIProject {
* Test case names from request body
*/
private Set testCaseNames;
-
+ /**
+ * When true, only GET and OPTIONS test cases are generated
+ */
+ private boolean readOnly;
+ /**
+ * When false, an extra valid/invalid test case pair is generated for each optional query parameter
+ */
+ private boolean minimalEndpoints;
+ /**
+ * When true, adds an X-Microcks-Response-Name header to each request, in addition to any custom headers
+ */
+ private boolean microcksHeaders;
+ /**
+ * When true, oneOf/anyOf schemas are resolved using their first candidate when generating example bodies.
+ * allOf schemas are always merged into a single object, regardless of this flag.
+ */
+ private boolean generateOneOfAnyOf;
+ /**
+ * Custom example values from request body, used before falling back to internal defaults
+ */
+ private ExamplesConfig examples;
+ /**
+ * OpenAPI Operation for each generated Method, keyed by a stable path+httpMethod key (not by RestMethod
+ * object identity, which is not guaranteed stable across SoapUI accessor calls), used to build optional
+ * query parameter variant requests
+ */
+ private Map operationByMethodKey = new HashMap<>();
+
/**
* SoapUIProject constructor
* Set default test case names if testCaseNames is null or empty
@@ -143,23 +185,34 @@ public class SoapUIProject {
* @param oAuth2Profiles authentication profiles from request body
* @param headers from request body
* @param testCaseNames from request body
+ * @param readOnly if true, only GET and OPTIONS test cases are generated
+ * @param minimalEndpoints if false, an extra valid/invalid test case pair is generated for each optional query parameter
+ * @param microcksHeaders if true, adds an X-Microcks-Response-Name header to each request, in addition to any custom headers
+ * @param generateOneOfAnyOf if true, oneOf/anyOf schemas are resolved using their first candidate when generating example bodies
+ * @param examples custom example values from request body, used before falling back to internal defaults
* @throws IOException
* @throws XmlException
* @throws SoapUIException
*/
- public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List headers, Set testCaseNames) throws IOException, XmlException, SoapUIException {
+ public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List headers, Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf, ExamplesConfig examples) throws IOException, XmlException, SoapUIException {
this.apiName = apiName;
this.openAPI = openAPI;
this.headers = headers;
-
+ this.examples = examples;
+
this.apiVersion = openAPI.getInfo().getVersion();
-
+
if (testCaseNames == null || testCaseNames.isEmpty()) {
this.testCaseNames = new HashSet<>(Arrays.asList(SUCCESS_TEST_CASE));
} else {
this.testCaseNames = testCaseNames;
}
-
+
+ this.readOnly = Boolean.TRUE.equals(readOnly);
+ this.minimalEndpoints = Boolean.TRUE.equals(minimalEndpoints);
+ this.microcksHeaders = Boolean.TRUE.equals(microcksHeaders);
+ this.generateOneOfAnyOf = Boolean.TRUE.equals(generateOneOfAnyOf);
+
createTempFile();
project = new WsdlProject();
@@ -172,7 +225,7 @@ public SoapUIProject(String apiName, OpenAPI openAPI, List servers) {
- for (Server server : servers) {
+ private void setRestServiceEndpoints(List servers, String serverPattern) {
+ if (servers == null || servers.isEmpty()) return;
+ List filtered = servers;
+ if (serverPattern != null && !serverPattern.isBlank()) {
+ String cleanPattern = serverPattern.replace("%", "");
+ Optional match = servers.stream()
+ .filter(s -> s.getUrl().contains(cleanPattern))
+ .findFirst();
+ filtered = match.isPresent()
+ ? Collections.singletonList(match.get())
+ : Collections.singletonList(servers.get(0));
+ }
+ for (Server server : filtered) {
String serverUrl = server.getUrl();
try {
URL url = new URL(serverUrl);
@@ -404,6 +468,7 @@ private void setMethodsRequests(String pathName, PathItem pathItem) {
pathItem.readOperationsMap().forEach((httpMethod, operation) -> {
RestMethod restMethod = restResource.getRestMethodByName((operation.getOperationId() != null) ? operation.getOperationId() : httpMethod.name());
if (restMethod == null) return;
+ operationByMethodKey.put(methodKey(restResource.getPath(), httpMethod.name()), operation);
RestRequest restRequest = restMethod.addNewRequest(DEFAULT_REQUEST_NAME);
RestRequestConfig restRequestConfig = restRequest.getConfig();
@@ -424,7 +489,7 @@ private void setMethodsRequests(String pathName, PathItem pathItem) {
}
}
- setRequestHeaders(restRequest);
+ setRequestHeaders(restRequest, operation);
});
}
}
@@ -432,16 +497,64 @@ private void setMethodsRequests(String pathName, PathItem pathItem) {
/**
* Set Request Headers
* Iterate headers received in request body and set to Request
+ * If microcksHeaders is true, additionally set the X-Microcks-Response-Name header
* @param restRequest instance of Method Request
+ * @param operation instance of OpenAPI Operation, used to resolve the Microcks response example name
*/
- private void setRequestHeaders(RestRequest restRequest) {
+ private void setRequestHeaders(RestRequest restRequest, Operation operation) {
+ StringToStringMap requestHeaders = new StringToStringMap();
if (headers != null && !headers.isEmpty()) {
- StringToStringMap requestHeaders = new StringToStringMap();
headers.forEach(header -> requestHeaders.put(header.getKey(), header.getValue()));
+ }
+ if (microcksHeaders && !requestHeaders.containsKey(MICROCKS_RESPONSE_NAME_HEADER)) {
+ String exampleName = getMicrocksExampleName(operation);
+ requestHeaders.put(MICROCKS_RESPONSE_NAME_HEADER, exampleName != null ? exampleName : DEFAULT);
+ }
+ if (!requestHeaders.isEmpty()) {
restRequest.setRequestHeaders(requestHeaders);
}
}
+ /**
+ * Get Microcks Example Name
+ * Look for the first named example defined on the operation's responses, checking 2xx responses in
+ * spec declaration order first, then the "default" response, and every media type within each response
+ * @param operation instance of OpenAPI Operation
+ * @return example name, or null if the operation has no response example
+ */
+ private String getMicrocksExampleName(Operation operation) {
+ if (operation.getResponses() == null) return null;
+ List candidateResponses = new ArrayList<>();
+ operation.getResponses().forEach((code, response) -> {
+ if (code.startsWith("2")) candidateResponses.add(response);
+ });
+ if (operation.getResponses().getDefault() != null) {
+ candidateResponses.add(operation.getResponses().getDefault());
+ }
+
+ for (ApiResponse response : candidateResponses) {
+ String exampleName = getFirstExampleName(response);
+ if (exampleName != null) return exampleName;
+ }
+ return null;
+ }
+
+ /**
+ * Get First Example Name
+ * Iterate every media type of the Response content and return the key of the first non-empty examples map
+ * @param response instance of OpenAPI Response
+ * @return example name, or null if no media type of this response has named examples
+ */
+ private String getFirstExampleName(ApiResponse response) {
+ if (response == null || response.getContent() == null || response.getContent().isEmpty()) return null;
+ for (MediaType mediaType : response.getContent().values()) {
+ if (mediaType.getExamples() != null && !mediaType.getExamples().isEmpty()) {
+ return mediaType.getExamples().entrySet().iterator().next().getKey();
+ }
+ }
+ return null;
+ }
+
/**
* Set Request Content
* Get OpenAPI Request Body example and set as Request Content
@@ -476,7 +589,7 @@ private void setRequestContent(RestRequest restRequest, Content content) {
@SuppressWarnings("rawtypes")
private Object getRequestExample(MediaType mediaType, RefResolver refResolver) {
Object example;
- Schema> schema = refResolver.resolveSchema(mediaType.getSchema());
+ Schema> schema = resolveComposedSchema(refResolver.resolveSchema(mediaType.getSchema()), refResolver);
if (mediaType.getExample() != null) {
example = mediaType.getExample();
} else if (mediaType.getExamples() != null && !mediaType.getExamples().isEmpty()) {
@@ -524,39 +637,142 @@ private JSONObject iterateProperties(Map properties, RefResolver
* @throws JSONException
*/
@SuppressWarnings("rawtypes")
- private Object getPropertyExample(Schema property, RefResolver refResolver) throws JSONException {
+ private Object getPropertyExample(Schema property, RefResolver refResolver) throws JSONException {
Object example = property.getExample();
-
- if (example == null) {
- if (property instanceof ObjectSchema) {
- example = iterateProperties(((ObjectSchema) property).getProperties(), refResolver);
- } else if (property instanceof ArraySchema) {
- JSONArray jsonArray = new JSONArray();
- Schema> items = refResolver.resolveSchema(((ArraySchema) property).getItems());
- jsonArray.put(getPropertyExample(items, refResolver));
- example = jsonArray;
- } else if (property instanceof IntegerSchema) {
- example = 0;
- } else if (property instanceof NumberSchema) {
- example = 0;
- } else if (property instanceof BooleanSchema) {
- example = true;
- } else if (property instanceof DateSchema) {
- example = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
- } else if (property instanceof StringSchema) {
- StringSchema stringProperty = (StringSchema) property;
- List enums = stringProperty.getEnum();
- if (enums != null && !enums.isEmpty()) {
- example = enums.get(0);
- } else {
- example = "";
- }
- } else {
- example = "";
+ if (example != null) {
+ return example;
+ }
+ return getExampleForResolvedType(resolveComposedSchema(property, refResolver), refResolver);
+ }
+
+ /**
+ * Get example for a resolved (non-composed) schema, dispatching by concrete schema type
+ * @param property resolved Schema
+ * @param refResolver instance of RefResolver
+ * @return example value for the schema's type
+ * @throws JSONException
+ */
+ @SuppressWarnings("rawtypes")
+ private Object getExampleForResolvedType(Schema property, RefResolver refResolver) throws JSONException {
+ if (property instanceof ObjectSchema) {
+ return iterateProperties(((ObjectSchema) property).getProperties(), refResolver);
+ } else if (property instanceof ArraySchema) {
+ JSONArray jsonArray = new JSONArray();
+ Schema> items = refResolver.resolveSchema(((ArraySchema) property).getItems());
+ jsonArray.put(getPropertyExample(items, refResolver));
+ return jsonArray;
+ } else if (property instanceof IntegerSchema || property instanceof NumberSchema) {
+ return getConfiguredExample(false, ExampleValues::getNumber, java.math.BigDecimal.ZERO);
+ } else if (property instanceof BooleanSchema) {
+ return getConfiguredExample(false, ExampleValues::getBooleanValue, true);
+ } else if (property instanceof DateSchema) {
+ return getConfiguredExample(false, ExampleValues::getDate, new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
+ } else if (property instanceof StringSchema) {
+ return getStringExample((StringSchema) property);
+ }
+ return "";
+ }
+
+ /**
+ * Get example for a String schema, honoring enum values and the date-time format before falling back
+ * to a configured/default string
+ * @param stringProperty String Schema
+ * @return example value
+ */
+ private Object getStringExample(StringSchema stringProperty) {
+ List enums = stringProperty.getEnum();
+ if (enums != null && !enums.isEmpty()) {
+ return enums.get(0);
+ } else if ("date-time".equalsIgnoreCase(stringProperty.getFormat())) {
+ return getConfiguredExample(false, ExampleValues::getDateTime, "");
+ }
+ return getConfiguredExample(false, ExampleValues::getString, "");
+ }
+
+ /**
+ * Look up a custom example value from the request body's "examples" configuration, falling back to
+ * defaultValue if "examples" was not provided, or the requested space/field was not configured
+ * @param wrong true to look up examples.wrong, false to look up examples.successful
+ * @param getter accessor for the desired field on ExampleValues
+ * @param defaultValue value to use if not configured
+ * @return the configured value, or defaultValue
+ */
+ private T getConfiguredExample(boolean wrong, java.util.function.Function getter, T defaultValue) {
+ if (examples == null) {
+ return defaultValue;
+ }
+ ExampleValues values = wrong ? examples.getWrong() : examples.getSuccessful();
+ if (values == null) {
+ return defaultValue;
+ }
+ T configured = getter.apply(values);
+ return configured != null ? configured : defaultValue;
+ }
+
+ /**
+ * Resolve oneOf/anyOf/allOf composition on a schema
+ * allOf is always merged into a single object schema (properties/required of every member), regardless of generateOneOfAnyOf
+ * When generateOneOfAnyOf is true, oneOf/anyOf are resolved to their first candidate schema
+ * Loops to also resolve composition nested inside a merged/chosen candidate, bounded to avoid runaway recursion
+ * @param schema to resolve
+ * @param refResolver instance of RefResolver
+ * @return resolved schema, or the original schema unchanged if it has no applicable composition
+ */
+ @SuppressWarnings("rawtypes")
+ private Schema resolveComposedSchema(Schema schema, RefResolver refResolver) {
+ final int MAX_ITERATIONS = 10;
+ for (int i = 0; i < MAX_ITERATIONS; i++) {
+ Schema resolved = resolveComposedSchemaOnce(schema, refResolver);
+ if (resolved == schema) {
+ return resolved;
}
+ schema = resolved;
}
-
- return example;
+ return schema;
+ }
+
+ /**
+ * Resolve a single level of oneOf/anyOf/allOf composition on a schema
+ * @param schema to resolve
+ * @param refResolver instance of RefResolver
+ * @return resolved schema, or the original schema unchanged if it has no applicable composition
+ */
+ @SuppressWarnings("rawtypes")
+ private Schema resolveComposedSchemaOnce(Schema schema, RefResolver refResolver) {
+ List allOf = schema.getAllOf();
+ if (allOf != null && !allOf.isEmpty()) {
+ return mergeAllOf(allOf, refResolver);
+ } else if (generateOneOfAnyOf && schema.getOneOf() != null && !schema.getOneOf().isEmpty()) {
+ return refResolver.resolveSchema((Schema) schema.getOneOf().get(0));
+ } else if (generateOneOfAnyOf && schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) {
+ return refResolver.resolveSchema((Schema) schema.getAnyOf().get(0));
+ }
+ return schema;
+ }
+
+ /**
+ * Merge every allOf member into a single object schema (properties/required of every member)
+ * @param allOf list of member schemas to merge
+ * @param refResolver instance of RefResolver
+ * @return merged object schema
+ */
+ @SuppressWarnings("rawtypes")
+ private ObjectSchema mergeAllOf(List allOf, RefResolver refResolver) {
+ ObjectSchema merged = new ObjectSchema();
+ Map mergedProperties = new HashMap<>();
+ List mergedRequired = new ArrayList<>();
+ for (Schema member : allOf) {
+ Schema resolvedMember = refResolver.resolveSchema(member);
+ if (resolvedMember.getProperties() != null) {
+ mergedProperties.putAll(resolvedMember.getProperties());
+ }
+ if (resolvedMember.getRequired() != null) {
+ mergedRequired.addAll(resolvedMember.getRequired());
+ }
+ }
+ merged.setProperties(mergedProperties);
+ merged.setRequired(mergedRequired);
+ return merged;
}
/**
@@ -689,24 +905,148 @@ private void setAuthProfile(org.apiaddicts.apitools.openapi2soapui.request.OAuth
*/
private void setTestCases() {
List resources = restService.getAllResources();
- if (resources != null && !resources.isEmpty()) {
- resources.forEach(restResource -> {
- List methods = restResource.getRestMethodList();
- if (methods != null && !methods.isEmpty()) {
- methods.forEach(restMethod -> {
- String method = restMethod.getMethod().name();
- String testSuiteName = restResource.getPath() + "_" + method + "_" + SUITE_SUFFIX;
- WsdlTestSuite testSuite = project.addNewTestSuite(testSuiteName);
- for (String testCaseNameItem : testCaseNames) {
- String testCaseName = testCaseNameItem + "_" + CASE_SUFFIX;
- WsdlTestCase testCase = testSuite.addNewTestCase(testCaseName);
- TestStepConfig ejecutionTestStepConfig = RestRequestStepFactory.createConfig(restMethod.getRequestByName(DEFAULT_REQUEST_NAME), EJECUTION_TEST_STEP + "_" + STEP_SUFFIX);
- testCase.addTestStep(ejecutionTestStepConfig);
- }
- });
- }
- });
+ if (resources == null || resources.isEmpty()) return;
+ resources.forEach(restResource -> {
+ List methods = restResource.getRestMethodList();
+ if (methods != null && !methods.isEmpty()) {
+ methods.forEach(restMethod -> addTestSuiteForMethod(restResource, restMethod));
+ }
+ });
+ }
+
+ /**
+ * Add Test Suite for a single Method
+ * Skipped for non read-only Methods when readOnly is true
+ * Adds a Test Case per configured test case name, plus optional query parameter variant Test Cases
+ * @param restResource instance of Resource owning the Method
+ * @param restMethod instance of Method to generate the Test Suite for
+ */
+ private void addTestSuiteForMethod(RestResource restResource, RestMethod restMethod) {
+ String method = restMethod.getMethod().name();
+ if (readOnly && !"GET".equals(method) && !"OPTIONS".equals(method)) return;
+ String testSuiteName = restResource.getPath() + "_" + method + "_" + SUITE_SUFFIX;
+ WsdlTestSuite testSuite = project.addNewTestSuite(testSuiteName);
+ for (String testCaseNameItem : testCaseNames) {
+ String testCaseName = testCaseNameItem + "_" + CASE_SUFFIX;
+ WsdlTestCase testCase = testSuite.addNewTestCase(testCaseName);
+ TestStepConfig ejecutionTestStepConfig = RestRequestStepFactory.createConfig(restMethod.getRequestByName(DEFAULT_REQUEST_NAME), EJECUTION_TEST_STEP + "_" + STEP_SUFFIX);
+ testCase.addTestStep(ejecutionTestStepConfig);
}
+ if (!minimalEndpoints) {
+ addQueryParamVariantTestCases(restResource, restMethod, testSuite);
+ }
+ }
+
+ /**
+ * Add Query Param Variant Test Cases
+ * For each optional query parameter of the Operation bound to this Method, add a valid and an invalid
+ * test case, each asserting the corresponding HTTP status code (200 or 400)
+ * @param restResource instance of Resource owning the Method
+ * @param restMethod instance of Method to generate variants for
+ * @param testSuite Test Suite to add the variant Test Cases to
+ */
+ private void addQueryParamVariantTestCases(RestResource restResource, RestMethod restMethod, WsdlTestSuite testSuite) {
+ Operation operation = operationByMethodKey.get(methodKey(restResource.getPath(), restMethod.getMethod().name()));
+ if (operation == null) return;
+ List queryParams = getQueryParameters(operation.getParameters());
+ RestRequest defaultRequest = restMethod.getRequestByName(DEFAULT_REQUEST_NAME);
+ getOptionalParameters(queryParams).forEach(param -> {
+ addQueryParamVariantTestCase(restMethod, defaultRequest, testSuite, queryParams, param, false);
+ addQueryParamVariantTestCase(restMethod, defaultRequest, testSuite, queryParams, param, true);
+ });
+ }
+
+ /**
+ * Build a stable key identifying a Method within the SoapUI Project, independent of object identity,
+ * to bridge OpenAPI Operation data between setMethodsRequests and setTestCases
+ * @param path Resource path
+ * @param httpMethod HTTP method name
+ * @return composite key
+ */
+ private String methodKey(String path, String httpMethod) {
+ return path + "#" + httpMethod;
+ }
+
+ /**
+ * Get Query Parameters
+ * Filter OpenAPI Parameters to keep only the ones in the query
+ * @param parameters list of OpenAPI Parameters to filter
+ * @return list of query Parameters
+ */
+ private List getQueryParameters(List parameters) {
+ if (parameters == null) return Collections.emptyList();
+ return parameters.stream()
+ .filter(param -> QUERY.equalsIgnoreCase(param.getIn()))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Get Optional Parameters
+ * Filter Parameters to keep only the ones that are not required
+ * @param parameters list of Parameters to filter
+ * @return list of optional Parameters
+ */
+ private List getOptionalParameters(List parameters) {
+ return parameters.stream()
+ .filter(param -> !Boolean.TRUE.equals(param.getRequired()))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Add Query Param Variant Test Case
+ * Clone the default Request (carrying over its endpoint, auth, media type, body and headers), then
+ * explicitly set every query parameter to a valid value, except the targeted parameter which gets
+ * an invalid value on the "wrong" variant
+ * Add a Test Case with this Request and a "Valid HTTP Status Codes" assertion for the expected status code
+ * @param restMethod instance of Method to add the variant Request to
+ * @param defaultRequest the Method's default Request, cloned as the base for the variant Request
+ * @param testSuite Test Suite to add the variant Test Case to
+ * @param queryParams all query Parameters of the Operation, so every one can be given an explicit valid value
+ * @param targetParam optional query Parameter being varied
+ * @param wrong when true, generates the invalid-value variant (expects 400), otherwise the valid-value variant (expects 200)
+ */
+ private void addQueryParamVariantTestCase(RestMethod restMethod, RestRequest defaultRequest, WsdlTestSuite testSuite,
+ List queryParams, Parameter targetParam, boolean wrong) {
+ String requestName = QUERY_PARAM_VARIANT_PREFIX + targetParam.getName() + (wrong ? QUERY_PARAM_VARIANT_WRONG_SUFFIX : "");
+ RestRequest variantRequest = restMethod.cloneRequest(defaultRequest, requestName);
+
+ queryParams.forEach(param -> {
+ boolean isTarget = param.getName().equals(targetParam.getName());
+ String value = (isTarget && wrong) ? getInvalidQueryParamValue(param) : getValidQueryParamValue(param);
+ variantRequest.setPropertyValue(param.getName(), value);
+ });
+
+ String testCaseName = requestName + "_" + CASE_SUFFIX;
+ WsdlTestCase testCase = testSuite.addNewTestCase(testCaseName);
+ TestStepConfig stepConfig = RestRequestStepFactory.createConfig(variantRequest, EJECUTION_TEST_STEP + "_" + STEP_SUFFIX);
+ WsdlTestStep testStep = testCase.addTestStep(stepConfig);
+
+ ValidHttpStatusCodesAssertion assertion = (ValidHttpStatusCodesAssertion)
+ ((RestTestRequestStep) testStep).addAssertion(VALID_HTTP_STATUS_CODES_ASSERTION);
+ assertion.setCodes(wrong ? WRONG_STATUS_CODE : SUCCESS_STATUS_CODE);
+ }
+
+ /**
+ * Get Valid Query Param Value
+ * Prefer the OpenAPI Parameter example (example/examples/x-example), falling back to a type-aware
+ * generic value (honoring enum values when present) when no example is defined
+ * @param param OpenAPI Parameter to compute a valid value for
+ * @return valid value as String
+ */
+ private String getValidQueryParamValue(Parameter param) {
+ Object example = getParameterExample(param);
+ if (example != null && !example.toString().isBlank()) return example.toString();
+ return QueryParamExampleUtils.validValue(param.getSchema(), examples != null ? examples.getSuccessful() : null);
+ }
+
+ /**
+ * Get Invalid Query Param Value
+ * Compute a type-aware invalid value for the targeted parameter's schema, used for the "wrong" variant
+ * @param param OpenAPI Parameter to compute an invalid value for
+ * @return invalid value as String
+ */
+ private String getInvalidQueryParamValue(Parameter param) {
+ return QueryParamExampleUtils.invalidValue(param.getSchema(), examples != null ? examples.getWrong() : null);
}
/**
diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExampleValues.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExampleValues.java
new file mode 100644
index 0000000..a606077
--- /dev/null
+++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExampleValues.java
@@ -0,0 +1,28 @@
+package org.apiaddicts.apitools.openapi2soapui.request;
+
+import java.math.BigDecimal;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class ExampleValues {
+
+ @JsonProperty("string")
+ private String string;
+
+ @JsonProperty("number")
+ private BigDecimal number;
+
+ @JsonProperty("boolean")
+ private Boolean booleanValue;
+
+ @JsonProperty("date")
+ private String date;
+
+ @JsonProperty("dateTime")
+ private String dateTime;
+}
diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExamplesConfig.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExamplesConfig.java
new file mode 100644
index 0000000..62bdf3c
--- /dev/null
+++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExamplesConfig.java
@@ -0,0 +1,21 @@
+package org.apiaddicts.apitools.openapi2soapui.request;
+
+import javax.validation.Valid;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class ExamplesConfig {
+
+ @Valid
+ @JsonProperty("successful")
+ private ExampleValues successful;
+
+ @Valid
+ @JsonProperty("wrong")
+ private ExampleValues wrong;
+}
diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java
index fd28d8d..d93d055 100644
--- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java
+++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java
@@ -37,4 +37,23 @@ public class SoapUIProjectRequest {
@Valid
@JsonProperty("headers")
private List headers;
+
+ @JsonProperty("readOnly")
+ private Boolean readOnly;
+
+ @JsonProperty("serverPattern")
+ private String serverPattern;
+
+ @JsonProperty("minimalEndpoints")
+ private Boolean minimalEndpoints;
+
+ @JsonProperty("microcksHeaders")
+ private Boolean microcksHeaders;
+
+ @JsonProperty("generateOneOfAnyOf")
+ private Boolean generateOneOfAnyOf;
+
+ @Valid
+ @JsonProperty("examples")
+ private ExamplesConfig examples;
}
diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java
index cd1593a..d86daa6 100644
--- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java
+++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java
@@ -1,16 +1,13 @@
package org.apiaddicts.apitools.openapi2soapui.service;
import java.io.IOException;
-import java.util.List;
-import java.util.Set;
import com.eviware.soapui.support.SoapUIException;
import io.swagger.v3.oas.models.OpenAPI;
import org.apiaddicts.apitools.openapi2soapui.model.SoapUIProject;
-import org.apiaddicts.apitools.openapi2soapui.request.OAuth2Profile;
-import org.apiaddicts.apitools.openapi2soapui.request.Header;
+import org.apiaddicts.apitools.openapi2soapui.request.SoapUIProjectRequest;
import org.apache.xmlbeans.XmlException;
public interface SoapUIProjectService {
- SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List headers, Set testCases) throws IOException, XmlException, SoapUIException;
+ SoapUIProject createSoapUIProject(SoapUIProjectRequest request, OpenAPI openAPI) throws IOException, XmlException, SoapUIException;
}
diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java
index 8ae2397..1e41b88 100644
--- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java
+++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java
@@ -1,8 +1,6 @@
package org.apiaddicts.apitools.openapi2soapui.service;
import java.io.IOException;
-import java.util.List;
-import java.util.Set;
import com.eviware.soapui.support.SoapUIException;
import org.apache.xmlbeans.XmlException;
@@ -10,16 +8,16 @@
import io.swagger.v3.oas.models.OpenAPI;
import org.apiaddicts.apitools.openapi2soapui.model.SoapUIProject;
-import org.apiaddicts.apitools.openapi2soapui.request.OAuth2Profile;
-import org.apiaddicts.apitools.openapi2soapui.request.Header;
+import org.apiaddicts.apitools.openapi2soapui.request.SoapUIProjectRequest;
@Service
public class SoapUIProjectServiceImpl implements SoapUIProjectService {
@Override
- public SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List credentials, List headers,
- Set testCaseNames) throws IOException, XmlException, SoapUIException {
- return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames);
+ public SoapUIProject createSoapUIProject(SoapUIProjectRequest request, OpenAPI openAPI) throws IOException, XmlException, SoapUIException {
+ return new SoapUIProject(request.getApiName(), openAPI, request.getOAuth2Profiles(), request.getHeaders(),
+ request.getTestCaseNames(), request.getReadOnly(), request.getServerPattern(), request.getMinimalEndpoints(),
+ request.getMicrocksHeaders(), request.getGenerateOneOfAnyOf(), request.getExamples());
}
-
+
}
diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java
new file mode 100644
index 0000000..be7a104
--- /dev/null
+++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java
@@ -0,0 +1,137 @@
+package org.apiaddicts.apitools.openapi2soapui.util;
+
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import io.swagger.v3.oas.models.media.ArraySchema;
+import io.swagger.v3.oas.models.media.BooleanSchema;
+import io.swagger.v3.oas.models.media.DateSchema;
+import io.swagger.v3.oas.models.media.IntegerSchema;
+import io.swagger.v3.oas.models.media.NumberSchema;
+import io.swagger.v3.oas.models.media.ObjectSchema;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.media.StringSchema;
+
+import org.apiaddicts.apitools.openapi2soapui.request.ExampleValues;
+
+public final class QueryParamExampleUtils {
+
+ private QueryParamExampleUtils() {
+ // Intentional blank
+ }
+
+ private static final String DATE_TIME_FORMAT = "date-time";
+ private static final String EMAIL_FORMAT = "email";
+ private static final String URI_FORMAT = "uri";
+ private static final String URL_FORMAT = "url";
+ private static final String UUID_FORMAT = "uuid";
+ private static final String HOSTNAME_FORMAT = "hostname";
+ private static final String IPV4_FORMAT = "ipv4";
+ private static final String IPV6_FORMAT = "ipv6";
+ private static final String BYTE_FORMAT = "byte";
+
+ public static String validValue(Schema> schema, ExampleValues successfulExamples) {
+ if (schema == null) return "value";
+ if (schema instanceof DateSchema) return formatDateExample((DateSchema) schema, configuredOrDefault(successfulExamples, ExampleValues::getDate, "2024-01-01"));
+ if (schema.getExample() != null) return schema.getExample().toString();
+ if (schema.getEnum() != null && !schema.getEnum().isEmpty()) return schema.getEnum().get(0).toString();
+ if (schema instanceof StringSchema) return validStringValue((StringSchema) schema, successfulExamples);
+ if (schema instanceof IntegerSchema || schema instanceof NumberSchema) return validNumberValue(successfulExamples);
+ if (schema instanceof BooleanSchema) return validBooleanValue(successfulExamples);
+ if (schema instanceof ArraySchema) return "[]";
+ if (schema instanceof ObjectSchema) return "{}";
+ return "value";
+ }
+
+ private static String validNumberValue(ExampleValues successfulExamples) {
+ if (successfulExamples == null || successfulExamples.getNumber() == null) return "1";
+ return successfulExamples.getNumber().toString();
+ }
+
+ private static String validBooleanValue(ExampleValues successfulExamples) {
+ if (successfulExamples == null || successfulExamples.getBooleanValue() == null) return "true";
+ return successfulExamples.getBooleanValue().toString();
+ }
+
+ public static String invalidValue(Schema> schema, ExampleValues wrongExamples) {
+ if (schema == null) return "badvalue";
+ if (schema instanceof StringSchema) return invalidString((StringSchema) schema, wrongExamples);
+ if (schema instanceof DateSchema) return invalidDate((DateSchema) schema, wrongExamples);
+ if (schema instanceof IntegerSchema || schema instanceof NumberSchema) return invalidNumber(schema, wrongExamples);
+ if (schema instanceof BooleanSchema) return configuredOrDefault(wrongExamples, v -> v.getBooleanValue() != null ? v.getBooleanValue().toString() : null, "badboolean");
+ if (schema instanceof ArraySchema) return "badarray";
+ if (schema instanceof ObjectSchema) return "badobject";
+ return "badvalue";
+ }
+
+ private static String configuredOrDefault(ExampleValues values, java.util.function.Function getter, String defaultValue) {
+ if (values == null) return defaultValue;
+ String configured = getter.apply(values);
+ return configured != null ? configured : defaultValue;
+ }
+
+ private static String validStringValue(StringSchema schema, ExampleValues successfulExamples) {
+ String format = schema.getFormat();
+ if (DATE_TIME_FORMAT.equalsIgnoreCase(format)) return configuredOrDefault(successfulExamples, ExampleValues::getDateTime, "2024-01-01T00:00:00Z");
+ if (EMAIL_FORMAT.equalsIgnoreCase(format)) return "user@example.com";
+ if (URI_FORMAT.equalsIgnoreCase(format) || URL_FORMAT.equalsIgnoreCase(format)) return "https://example.com";
+ if (UUID_FORMAT.equalsIgnoreCase(format)) return "3fa85f64-5717-4562-b3fc-2c963f66afa6";
+ if (HOSTNAME_FORMAT.equalsIgnoreCase(format)) return "example.com";
+ // RFC 5737 / RFC 3849 reserved documentation ranges, safe to use as literal examples
+ if (IPV4_FORMAT.equalsIgnoreCase(format)) return "192.0.2.1";
+ if (IPV6_FORMAT.equalsIgnoreCase(format)) return "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
+ if (BYTE_FORMAT.equalsIgnoreCase(format)) return "SGVsbG8gV29ybGQ=";
+ return configuredOrDefault(successfulExamples, ExampleValues::getString, "string");
+ }
+
+ private static String invalidString(StringSchema schema, ExampleValues wrongExamples) {
+ String format = schema.getFormat();
+ if (DATE_TIME_FORMAT.equalsIgnoreCase(format)) return invalidDateTime(schema, wrongExamples);
+ if (EMAIL_FORMAT.equalsIgnoreCase(format)) return "not-an-email";
+ if (URI_FORMAT.equalsIgnoreCase(format) || URL_FORMAT.equalsIgnoreCase(format)) return "not a uri";
+ if (UUID_FORMAT.equalsIgnoreCase(format)) return "not-a-valid-uuid";
+ if (HOSTNAME_FORMAT.equalsIgnoreCase(format)) return "invalid_hostname!";
+ if (IPV4_FORMAT.equalsIgnoreCase(format)) return "999.999.999.999";
+ if (IPV6_FORMAT.equalsIgnoreCase(format)) return "not:a:valid:ipv6:zzzz";
+ if (BYTE_FORMAT.equalsIgnoreCase(format)) return "not_base64!!!";
+ Integer maxLength = schema.getMaxLength();
+ if (maxLength != null && maxLength > 0) return "z".repeat(maxLength + 1);
+ return configuredOrDefault(wrongExamples, ExampleValues::getString, "badstring");
+ }
+
+ private static String invalidDateTime(StringSchema schema, ExampleValues wrongExamples) {
+ Object example = schema.getExample();
+ if (example == null && wrongExamples != null && wrongExamples.getDateTime() != null) {
+ return wrongExamples.getDateTime();
+ }
+ String base = (example != null) ? example.toString() : "2024-01-01T00:00:00Z";
+ String withInvalidMonth = base.replaceFirst("^(\\d{4})-\\d{2}", "$1-50");
+ return withInvalidMonth.equals(base) ? "baddatetime" : withInvalidMonth;
+ }
+
+ private static String invalidNumber(Schema> schema, ExampleValues wrongExamples) {
+ if (schema.getMaximum() != null) return schema.getMaximum().add(BigDecimal.ONE).toString();
+ if (schema.getMinimum() != null) return schema.getMinimum().subtract(BigDecimal.ONE).toString();
+ return configuredOrDefault(wrongExamples, v -> v.getNumber() != null ? v.getNumber().toString() : null, "badnumber");
+ }
+
+ private static String invalidDate(DateSchema schema, ExampleValues wrongExamples) {
+ if (schema.getExample() == null && wrongExamples != null && wrongExamples.getDate() != null) {
+ return wrongExamples.getDate();
+ }
+ String base = formatDateExample(schema, "2024-01-01");
+ String[] parts = base.split("-");
+ if (parts.length == 3) {
+ parts[1] = "50";
+ return String.join("-", parts);
+ }
+ return "baddate";
+ }
+
+ private static String formatDateExample(DateSchema schema, String fallback) {
+ Object example = schema.getExample();
+ if (example instanceof Date) return new SimpleDateFormat("yyyy-MM-dd").format((Date) example);
+ return (example != null) ? example.toString() : fallback;
+ }
+}
diff --git a/src/main/resources/static/api.yaml b/src/main/resources/static/api.yaml
index 4f6d995..d54041f 100644
--- a/src/main/resources/static/api.yaml
+++ b/src/main/resources/static/api.yaml
@@ -42,6 +42,7 @@ paths:
- Test Suite: {path}\_{httpMethodInUppercase}\_TestSuite
- Test Case (Default): Success\_TestCase
- Test Case: {testCaseName}\_TestCase
+ - Test Case (Query Param Variant): queryString {paramName}\_TestCase / queryString {paramName} wrong\_TestCase
- Test Step: Execution\_TestStep
The variables are obtained from:
@@ -56,7 +57,15 @@ paths:
Additionally, it is possible to add custom headers to each of the requests, defining a list of key-value elements with the name "headers" as part of the request body, where key indicates the name of the header and value the value of the header.
- The response is the content of the SoapUI project in XML format to save as file and import into the SoapUI application.
+ Additionally, when the **readOnly** flag is set to true, only GET and OPTIONS test cases are generated; POST, PUT, PATCH and DELETE operations are excluded.
+
+ Additionally, when the **minimalEndpoints** flag is set to false (the default), for each optional query parameter of an operation, two extra test cases are generated: one with a valid value (asserting HTTP status 200) and one with an invalid value (asserting HTTP status 400). Set to true to only generate the testCaseNames-based test cases.
+
+ Additionally, when the **microcksHeaders** flag is set to true, an X-Microcks-Response-Name header is added to each request, in addition to any custom headers. Its value is the name of the response example defined in the OpenAPI spec (first 2xx response, falling back to the "default" response), or "default" if none is defined.
+
+ Additionally, when the **generateOneOfAnyOf** flag is set to true, oneOf/anyOf schemas in the request body are resolved to their first candidate schema when generating the example body (default: unresolved/omitted). allOf schemas are always merged into a single object regardless of this flag.
+
+ The response is the content of the SoapUI project in XML format to save as file and import into the SoapUI application.
requestBody:
description: |
Information necessary for the generation of the SoapUI Project.
@@ -67,6 +76,10 @@ paths:
- openApiSpec: Base-64 encoded OpenAPI Spec
- testCaseNames: List with the names of the test cases
- headers: List of custom headers
+ - readOnly: Boolean flag to generate only read operations (GET/OPTIONS)
+ - minimalEndpoints: Boolean flag; when false (default), also generates a valid/invalid test case pair per optional query parameter
+ - microcksHeaders: Boolean flag; when true, adds an X-Microcks-Response-Name header to each request, in addition to any custom headers
+ - generateOneOfAnyOf: Boolean flag; when true, resolves oneOf/anyOf schemas to their first candidate when generating example bodies. allOf is always merged regardless of this flag
content:
aplication/json:
schema:
@@ -453,6 +466,56 @@ components:
example: Success
headers:
$ref: '#/components/schemas/Headers'
+ readOnly:
+ type: boolean
+ description: If true, only GET and OPTIONS test cases are generated. POST, PUT, PATCH and DELETE are excluded.
+ example: false
+ minimalEndpoints:
+ type: boolean
+ description: If false (default), generates 2 additional test cases per optional query parameter — one with a valid value (expects 200) and one with an invalid value (expects 400). If true, only testCaseNames-based test cases are generated.
+ example: false
+ microcksHeaders:
+ type: boolean
+ description: If true, adds an X-Microcks-Response-Name header to each request, in addition to any custom headers. Its value is the name of the response example defined in the OpenAPI spec (first 2xx response, falling back to the "default" response), or "default" if none is defined.
+ example: false
+ generateOneOfAnyOf:
+ type: boolean
+ description: If true, resolves oneOf/anyOf schemas to their first candidate schema when generating the example request body. allOf schemas are always merged into a single object regardless of this flag.
+ example: false
+ serverPattern:
+ type: string
+ description: Pattern to select the OpenAPI server used as endpoint. Wrap the substring with % (e.g. %dev%). If no server matches, the first server is used.
+ example: "%dev%"
+ examples:
+ $ref: '#/components/schemas/Examples'
+ Examples:
+ type: object
+ description: Custom example values used when generating request body properties and optional-query-parameter test values. Any field not provided falls back to the tool's internal default. "successful" values are used for valid request bodies and valid query parameter test cases; "wrong" values are used only for the invalid query parameter test cases generated when minimalEndpoints is false.
+ properties:
+ successful:
+ $ref: '#/components/schemas/ExampleValues'
+ wrong:
+ $ref: '#/components/schemas/ExampleValues'
+ ExampleValues:
+ type: object
+ properties:
+ string:
+ type: string
+ example: goodstring
+ number:
+ type: number
+ example: 6
+ boolean:
+ type: boolean
+ example: true
+ date:
+ type: string
+ format: date
+ example: "2020-01-01"
+ dateTime:
+ type: string
+ format: date-time
+ example: "2020-01-01T23:59:59"
Headers:
type: array
description: List of optionals headers. This apply in all resources
diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java b/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java
new file mode 100644
index 0000000..14873b0
--- /dev/null
+++ b/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java
@@ -0,0 +1,286 @@
+package org.apiaddicts.apitools.openapi2soapui.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.BigDecimal;
+
+import org.junit.jupiter.api.Test;
+
+import io.swagger.v3.oas.models.media.ArraySchema;
+import io.swagger.v3.oas.models.media.BooleanSchema;
+import io.swagger.v3.oas.models.media.DateSchema;
+import io.swagger.v3.oas.models.media.IntegerSchema;
+import io.swagger.v3.oas.models.media.NumberSchema;
+import io.swagger.v3.oas.models.media.ObjectSchema;
+import io.swagger.v3.oas.models.media.StringSchema;
+
+import org.apiaddicts.apitools.openapi2soapui.request.ExampleValues;
+
+class QueryParamExampleUtilsTest {
+
+ @Test
+ void invalidValue_returnsGenericValue_whenSchemaIsNull() {
+ assertEquals("badvalue", QueryParamExampleUtils.invalidValue(null, null));
+ }
+
+ @Test
+ void invalidValue_padsToMaxLengthPlusOne_whenStringHasMaxLength() {
+ StringSchema schema = new StringSchema();
+ schema.setMaxLength(5);
+ assertEquals(6, QueryParamExampleUtils.invalidValue(schema, null).length());
+ }
+
+ @Test
+ void invalidValue_returnsGenericString_whenStringHasNoMaxLength() {
+ StringSchema schema = new StringSchema();
+ assertEquals("badstring", QueryParamExampleUtils.invalidValue(schema, null));
+ }
+
+ @Test
+ void invalidValue_exceedsMaximum_whenIntegerHasMaximum() {
+ IntegerSchema schema = new IntegerSchema();
+ schema.setMaximum(BigDecimal.TEN);
+ assertEquals("11", QueryParamExampleUtils.invalidValue(schema, null));
+ }
+
+ @Test
+ void invalidValue_goesBelowMinimum_whenNumberHasOnlyMinimum() {
+ NumberSchema schema = new NumberSchema();
+ schema.setMinimum(BigDecimal.ZERO);
+ assertEquals("-1", QueryParamExampleUtils.invalidValue(schema, null));
+ }
+
+ @Test
+ void invalidValue_returnsGenericNumber_whenNoBoundsDefined() {
+ IntegerSchema schema = new IntegerSchema();
+ assertEquals("badnumber", QueryParamExampleUtils.invalidValue(schema, null));
+ }
+
+ @Test
+ void invalidValue_returnsBadBoolean_forBooleanSchema() {
+ assertEquals("badboolean", QueryParamExampleUtils.invalidValue(new BooleanSchema(), null));
+ }
+
+ @Test
+ void invalidValue_returnsBadArray_forArraySchema() {
+ assertEquals("badarray", QueryParamExampleUtils.invalidValue(new ArraySchema(), null));
+ }
+
+ @Test
+ void invalidValue_returnsBadObject_forObjectSchema() {
+ assertEquals("badobject", QueryParamExampleUtils.invalidValue(new ObjectSchema(), null));
+ }
+
+ @Test
+ void invalidValue_setsInvalidMonth_forDateSchema() {
+ DateSchema schema = new DateSchema();
+ schema.setExample("2024-01-01");
+ String[] parts = QueryParamExampleUtils.invalidValue(schema, null).split("-");
+ assertEquals(3, parts.length);
+ assertEquals("50", parts[1]);
+ }
+
+ @Test
+ void invalidValue_setsInvalidMonth_forDateSchemaWithoutExample() {
+ String[] parts = QueryParamExampleUtils.invalidValue(new DateSchema(), null).split("-");
+ assertEquals(3, parts.length);
+ assertEquals("50", parts[1]);
+ }
+
+ @Test
+ void validValue_returnsGenericValue_whenSchemaIsNull() {
+ assertEquals("value", QueryParamExampleUtils.validValue(null, null));
+ }
+
+ @Test
+ void validValue_returnsExample_whenSchemaHasExample() {
+ StringSchema schema = new StringSchema();
+ schema.setExample("foo");
+ assertEquals("foo", QueryParamExampleUtils.validValue(schema, null));
+ }
+
+ @Test
+ void validValue_returnsTypedDefault_forEachSchemaType() {
+ assertEquals("string", QueryParamExampleUtils.validValue(new StringSchema(), null));
+ assertTrue(QueryParamExampleUtils.validValue(new IntegerSchema(), null).matches("\\d+"));
+ assertEquals("true", QueryParamExampleUtils.validValue(new BooleanSchema(), null));
+ }
+
+ @Test
+ void validValue_returnsFirstEnumValue_whenSchemaHasEnumAndNoExample() {
+ StringSchema schema = new StringSchema();
+ schema.setEnum(java.util.List.of("asc", "desc"));
+ assertEquals("asc", QueryParamExampleUtils.validValue(schema, null));
+ }
+
+ @Test
+ void validValue_prefersExampleOverEnum() {
+ StringSchema schema = new StringSchema();
+ schema.setEnum(java.util.List.of("asc", "desc"));
+ schema.setExample("desc");
+ assertEquals("desc", QueryParamExampleUtils.validValue(schema, null));
+ }
+
+ @Test
+ void validValue_returnsIsoDateTime_forDateTimeFormattedString() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("date-time");
+ assertEquals("2024-01-01T00:00:00Z", QueryParamExampleUtils.validValue(schema, null));
+ }
+
+ @Test
+ void invalidValue_setsInvalidMonth_forDateTimeFormattedString() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("date-time");
+ assertEquals("2024-50-01T00:00:00Z", QueryParamExampleUtils.invalidValue(schema, null));
+ }
+
+ @Test
+ void invalidValue_setsInvalidMonth_forDateTimeFormattedStringWithExample() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("date-time");
+ schema.setExample("2025-06-15T10:30:00Z");
+ assertEquals("2025-50-15T10:30:00Z", QueryParamExampleUtils.invalidValue(schema, null));
+ }
+
+ @Test
+ void validValue_returnsRealisticValue_forEmailFormat() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("email");
+ assertEquals("user@example.com", QueryParamExampleUtils.validValue(schema, null));
+ }
+
+ @Test
+ void invalidValue_hasNoAtSign_forEmailFormat() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("email");
+ assertTrue(!QueryParamExampleUtils.invalidValue(schema, null).contains("@"));
+ }
+
+ @Test
+ void validValue_returnsRealisticValue_forUriFormat() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("uri");
+ assertEquals("https://example.com", QueryParamExampleUtils.validValue(schema, null));
+ }
+
+ @Test
+ void validValue_returnsRealisticValue_forUuidFormat() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("uuid");
+ assertEquals("3fa85f64-5717-4562-b3fc-2c963f66afa6", QueryParamExampleUtils.validValue(schema, null));
+ }
+
+ @Test
+ void invalidValue_isNotUuidShaped_forUuidFormat() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("uuid");
+ assertEquals("not-a-valid-uuid", QueryParamExampleUtils.invalidValue(schema, null));
+ }
+
+ @Test
+ void validValue_returnsRealisticValue_forIpv4Format() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("ipv4");
+ assertEquals("192.0.2.1", QueryParamExampleUtils.validValue(schema, null));
+ }
+
+ @Test
+ void invalidValue_hasOutOfRangeOctets_forIpv4Format() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("ipv4");
+ assertEquals("999.999.999.999", QueryParamExampleUtils.invalidValue(schema, null));
+ }
+
+ @Test
+ void validValue_returnsRealisticValue_forByteFormat() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("byte");
+ assertEquals("SGVsbG8gV29ybGQ=", QueryParamExampleUtils.validValue(schema, null));
+ }
+
+ @Test
+ void formatAwareness_stillYieldsToExplicitExample() {
+ StringSchema schema = new StringSchema();
+ schema.setFormat("email");
+ schema.setExample("custom@example.org");
+ assertEquals("custom@example.org", QueryParamExampleUtils.validValue(schema, null));
+ }
+
+ @Test
+ void validValue_usesConfiguredSuccessfulExample_whenGenericStringHasNoFormat() {
+ ExampleValues successful = new ExampleValues();
+ successful.setString("goodstring");
+ assertEquals("goodstring", QueryParamExampleUtils.validValue(new StringSchema(), successful));
+ }
+
+ @Test
+ void invalidValue_usesConfiguredWrongExample_whenGenericStringHasNoMaxLength() {
+ ExampleValues wrong = new ExampleValues();
+ wrong.setString("badstring-configured");
+ assertEquals("badstring-configured", QueryParamExampleUtils.invalidValue(new StringSchema(), wrong));
+ }
+
+ @Test
+ void invalidValue_ignoresConfiguredWrongExample_whenMaxLengthDefined() {
+ StringSchema schema = new StringSchema();
+ schema.setMaxLength(3);
+ ExampleValues wrong = new ExampleValues();
+ wrong.setString("badstring-configured");
+ assertEquals(4, QueryParamExampleUtils.invalidValue(schema, wrong).length());
+ }
+
+ @Test
+ void validValue_usesConfiguredSuccessfulNumber() {
+ ExampleValues successful = new ExampleValues();
+ successful.setNumber(BigDecimal.valueOf(6));
+ assertEquals("6", QueryParamExampleUtils.validValue(new IntegerSchema(), successful));
+ }
+
+ @Test
+ void invalidValue_usesConfiguredWrongNumber_whenNoBoundsDefined() {
+ ExampleValues wrong = new ExampleValues();
+ wrong.setNumber(BigDecimal.valueOf(-6));
+ assertEquals("-6", QueryParamExampleUtils.invalidValue(new IntegerSchema(), wrong));
+ }
+
+ @Test
+ void validValue_usesConfiguredSuccessfulBoolean() {
+ ExampleValues successful = new ExampleValues();
+ successful.setBooleanValue(Boolean.FALSE);
+ assertEquals("false", QueryParamExampleUtils.validValue(new BooleanSchema(), successful));
+ }
+
+ @Test
+ void validValue_usesConfiguredSuccessfulDate() {
+ ExampleValues successful = new ExampleValues();
+ successful.setDate("2020-01-01");
+ assertEquals("2020-01-01", QueryParamExampleUtils.validValue(new DateSchema(), successful));
+ }
+
+ @Test
+ void invalidValue_usesConfiguredWrongDateDirectly_whenNoSchemaExample() {
+ ExampleValues wrong = new ExampleValues();
+ wrong.setDate("2020-40-40");
+ assertEquals("2020-40-40", QueryParamExampleUtils.invalidValue(new DateSchema(), wrong));
+ }
+
+ @Test
+ void validValue_usesConfiguredSuccessfulDateTime() {
+ ExampleValues successful = new ExampleValues();
+ successful.setDateTime("2020-01-01T23:59:59");
+ StringSchema schema = new StringSchema();
+ schema.setFormat("date-time");
+ assertEquals("2020-01-01T23:59:59", QueryParamExampleUtils.validValue(schema, successful));
+ }
+
+ @Test
+ void invalidValue_usesConfiguredWrongDateTimeDirectly_whenNoSchemaExample() {
+ ExampleValues wrong = new ExampleValues();
+ wrong.setDateTime("2020-40-40T00:00:00");
+ StringSchema schema = new StringSchema();
+ schema.setFormat("date-time");
+ assertEquals("2020-40-40T00:00:00", QueryParamExampleUtils.invalidValue(schema, wrong));
+ }
+}