From ad32807a4f19711422feb20128cb012e1c7978c0 Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Tue, 30 Jun 2026 13:53:08 -0500 Subject: [PATCH 1/7] feat: add readOnly parameter to filter write operations from test cases --- CHANGELOG.md | 12 ++++++++++++ .../controller/SoapUIProjectController.java | 5 +++-- .../openapi2soapui/model/SoapUIProject.java | 14 +++++++++++--- .../request/SoapUIProjectRequest.java | 3 +++ .../service/SoapUIProjectService.java | 2 +- .../service/SoapUIProjectServiceImpl.java | 4 ++-- src/main/resources/static/api.yaml | 9 ++++++++- 7 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..109cdba --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# 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] - 2026-06-30 + +### Added +- Added `readOnly` parameter: when set to `true`, only GET and OPTIONS test cases are generated; POST, PUT, PATCH and DELETE operations are excluded. 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..89af74a 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,9 @@ 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.getApiName(), openAPI, + newSoapUIProject.getOAuth2Profiles(), newSoapUIProject.getHeaders(), newSoapUIProject.getTestCaseNames(), + newSoapUIProject.getReadOnly()); 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..ea15814 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java @@ -126,6 +126,10 @@ 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; /** * SoapUIProject constructor @@ -143,22 +147,25 @@ 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 * @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) throws IOException, XmlException, SoapUIException { this.apiName = apiName; this.openAPI = openAPI; this.headers = headers; - + 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); createTempFile(); @@ -695,6 +702,7 @@ private void setTestCases() { if (methods != null && !methods.isEmpty()) { methods.forEach(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) { 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..1104ccb 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,7 @@ public class SoapUIProjectRequest { @Valid @JsonProperty("headers") private List
headers; + + @JsonProperty("readOnly") + private Boolean readOnly; } 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..fff265c 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java @@ -12,5 +12,5 @@ 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(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCases, Boolean readOnly) 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..49971d6 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java @@ -18,8 +18,8 @@ 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); + Set testCaseNames, Boolean readOnly) throws IOException, XmlException, SoapUIException { + return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly); } } diff --git a/src/main/resources/static/api.yaml b/src/main/resources/static/api.yaml index 4f6d995..08d19d8 100644 --- a/src/main/resources/static/api.yaml +++ b/src/main/resources/static/api.yaml @@ -56,7 +56,9 @@ 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. + + 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 +69,7 @@ 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) content: aplication/json: schema: @@ -453,6 +456,10 @@ 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 Headers: type: array description: List of optionals headers. This apply in all resources From 89386c866566c93beee5a87dcd542e075e8e0a0c Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Tue, 30 Jun 2026 17:19:24 -0500 Subject: [PATCH 2/7] feat: add serverPattern parameter to filter server endpoint --- CHANGELOG.md | 5 +++++ .../controller/SoapUIProjectController.java | 2 +- .../openapi2soapui/model/SoapUIProject.java | 21 +++++++++++++++---- .../request/SoapUIProjectRequest.java | 3 +++ .../service/SoapUIProjectService.java | 2 +- .../service/SoapUIProjectServiceImpl.java | 4 ++-- src/main/resources/static/api.yaml | 4 ++++ 7 files changed, 33 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 109cdba..6c090af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ 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.2.0] - 2026-06-30 + +### Added +- Added `serverPattern` parameter: optional string (e.g. `%dev%`) that selects the OpenAPI server whose URL contains the given substring (after stripping `%`). If no server matches, the first server in the list is used as fallback. When omitted, all servers are added as endpoints (existing behavior preserved). + ## [1.1.0] - 2026-06-30 ### Added 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 89af74a..7dafa27 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java @@ -36,7 +36,7 @@ public String newSoapUIProject(@Valid @RequestBody SoapUIProjectRequest newSoapU } SoapUIProject soapUIProject = soapUIProjectService.createSoapUIProject(newSoapUIProject.getApiName(), openAPI, newSoapUIProject.getOAuth2Profiles(), newSoapUIProject.getHeaders(), newSoapUIProject.getTestCaseNames(), - newSoapUIProject.getReadOnly()); + newSoapUIProject.getReadOnly(), newSoapUIProject.getServerPattern()); 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 ea15814..0d2be9a 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java @@ -20,10 +20,12 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; 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; @@ -152,7 +154,7 @@ public class SoapUIProject { * @throws XmlException * @throws SoapUIException */ - public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly) throws IOException, XmlException, SoapUIException { + public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly, String serverPattern) throws IOException, XmlException, SoapUIException { this.apiName = apiName; this.openAPI = openAPI; this.headers = headers; @@ -179,7 +181,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); 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 1104ccb..c16687e 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java @@ -40,4 +40,7 @@ public class SoapUIProjectRequest { @JsonProperty("readOnly") private Boolean readOnly; + + @JsonProperty("serverPattern") + private String serverPattern; } 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 fff265c..cd856e8 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java @@ -12,5 +12,5 @@ import org.apache.xmlbeans.XmlException; public interface SoapUIProjectService { - SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCases, Boolean readOnly) throws IOException, XmlException, SoapUIException; + SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCases, Boolean readOnly, String serverPattern) 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 49971d6..7e3d46c 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java @@ -18,8 +18,8 @@ public class SoapUIProjectServiceImpl implements SoapUIProjectService { @Override public SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List credentials, List
headers, - Set testCaseNames, Boolean readOnly) throws IOException, XmlException, SoapUIException { - return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly); + Set testCaseNames, Boolean readOnly, String serverPattern) throws IOException, XmlException, SoapUIException { + return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly, serverPattern); } } diff --git a/src/main/resources/static/api.yaml b/src/main/resources/static/api.yaml index 08d19d8..a978d80 100644 --- a/src/main/resources/static/api.yaml +++ b/src/main/resources/static/api.yaml @@ -460,6 +460,10 @@ components: type: boolean description: If true, only GET and OPTIONS test cases are generated. POST, PUT, PATCH and DELETE are excluded. 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%" Headers: type: array description: List of optionals headers. This apply in all resources From 59516e2b3d43e4a8d1a69bb05e344c6eca38c074 Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Wed, 1 Jul 2026 10:23:15 -0500 Subject: [PATCH 3/7] feat: add minimalEndpoints parameter to generate query parameter test variants --- CHANGELOG.md | 7 +- .../openapi2soapui/constants/Constants.java | 6 + .../controller/SoapUIProjectController.java | 2 +- .../openapi2soapui/model/SoapUIProject.java | 134 ++++++++++- .../request/SoapUIProjectRequest.java | 3 + .../service/SoapUIProjectService.java | 2 +- .../service/SoapUIProjectServiceImpl.java | 4 +- .../util/QueryParamExampleUtils.java | 111 ++++++++++ src/main/resources/static/api.yaml | 8 + .../util/QueryParamExampleUtilsTest.java | 208 ++++++++++++++++++ 10 files changed, 473 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java create mode 100644 src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c090af..d2f0536 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,9 @@ 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.2.0] - 2026-06-30 - -### Added -- Added `serverPattern` parameter: optional string (e.g. `%dev%`) that selects the OpenAPI server whose URL contains the given substring (after stripping `%`). If no server matches, the first server in the list is used as fallback. When omitted, all servers are added as endpoints (existing behavior preserved). - ## [1.1.0] - 2026-06-30 ### Added - Added `readOnly` parameter: when set to `true`, only GET and OPTIONS test cases are generated; POST, PUT, PATCH and DELETE operations are excluded. +- Added `serverPattern` parameter: optional string (e.g. `%dev%`) that selects the OpenAPI server whose URL contains the given substring (after stripping `%`). If no server matches, the first server in the list is used as fallback. When omitted, all servers are added as endpoints (existing behavior preserved). +- Added `minimalEndpoints` parameter: when set to `false` (default), two additional test cases are generated per optional query parameter of each operation — one with a valid value asserting HTTP status 200, and one with an invalid value asserting HTTP status 400. When `true`, only the `testCaseNames`-based test cases are generated. 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..d012ce1 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,10 @@ 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"; } 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 7dafa27..6d64936 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java @@ -36,7 +36,7 @@ public String newSoapUIProject(@Valid @RequestBody SoapUIProjectRequest newSoapU } SoapUIProject soapUIProject = soapUIProjectService.createSoapUIProject(newSoapUIProject.getApiName(), openAPI, newSoapUIProject.getOAuth2Profiles(), newSoapUIProject.getHeaders(), newSoapUIProject.getTestCaseNames(), - newSoapUIProject.getReadOnly(), newSoapUIProject.getServerPattern()); + newSoapUIProject.getReadOnly(), newSoapUIProject.getServerPattern(), newSoapUIProject.getMinimalEndpoints()); 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 0d2be9a..feda1f6 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,11 @@ 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 java.io.File; import java.io.IOException; @@ -22,6 +27,7 @@ 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; @@ -59,7 +65,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; @@ -88,6 +97,7 @@ import lombok.Getter; import org.apiaddicts.apitools.openapi2soapui.request.GrantType; import org.apiaddicts.apitools.openapi2soapui.request.Header; +import org.apiaddicts.apitools.openapi2soapui.util.QueryParamExampleUtils; import org.apiaddicts.apitools.openapi2soapui.util.RefResolver; /** @@ -132,7 +142,17 @@ public class SoapUIProject { * 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; + /** + * 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 @@ -150,11 +170,12 @@ public class SoapUIProject { * @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 * @throws IOException * @throws XmlException * @throws SoapUIException */ - public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly, String serverPattern) throws IOException, XmlException, SoapUIException { + public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints) throws IOException, XmlException, SoapUIException { this.apiName = apiName; this.openAPI = openAPI; this.headers = headers; @@ -168,7 +189,8 @@ public SoapUIProject(String apiName, OpenAPI openAPI, List { 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(); @@ -724,12 +747,117 @@ private void setTestCases() { 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) ? QueryParamExampleUtils.invalidValue(param.getSchema()) : 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()); + } + /** * Get content of SoapUI Project File (XML) * @return SoapUI Project file content 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 c16687e..76fc105 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java @@ -43,4 +43,7 @@ public class SoapUIProjectRequest { @JsonProperty("serverPattern") private String serverPattern; + + @JsonProperty("minimalEndpoints") + private Boolean minimalEndpoints; } 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 cd856e8..649098b 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java @@ -12,5 +12,5 @@ import org.apache.xmlbeans.XmlException; public interface SoapUIProjectService { - SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCases, Boolean readOnly, String serverPattern) throws IOException, XmlException, SoapUIException; + SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCases, Boolean readOnly, String serverPattern, Boolean minimalEndpoints) 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 7e3d46c..6239396 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java @@ -18,8 +18,8 @@ public class SoapUIProjectServiceImpl implements SoapUIProjectService { @Override public SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List credentials, List
headers, - Set testCaseNames, Boolean readOnly, String serverPattern) throws IOException, XmlException, SoapUIException { - return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly, serverPattern); + Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints) throws IOException, XmlException, SoapUIException { + return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints); } } 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..c1e3fe9 --- /dev/null +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java @@ -0,0 +1,111 @@ +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; + +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) { + if (schema == null) return "value"; + if (schema instanceof DateSchema) return formatDateExample((DateSchema) schema, "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); + if (schema instanceof IntegerSchema || schema instanceof NumberSchema) return "1"; + if (schema instanceof BooleanSchema) return "true"; + if (schema instanceof ArraySchema) return "[]"; + if (schema instanceof ObjectSchema) return "{}"; + return "value"; + } + + public static String invalidValue(Schema schema) { + if (schema == null) return "badvalue"; + if (schema instanceof StringSchema) return invalidString((StringSchema) schema); + if (schema instanceof DateSchema) return invalidDate((DateSchema) schema); + if (schema instanceof IntegerSchema || schema instanceof NumberSchema) return invalidNumber(schema); + if (schema instanceof BooleanSchema) return "badboolean"; + if (schema instanceof ArraySchema) return "badarray"; + if (schema instanceof ObjectSchema) return "badobject"; + return "badvalue"; + } + + private static String validStringValue(StringSchema schema) { + String format = schema.getFormat(); + if (DATE_TIME_FORMAT.equalsIgnoreCase(format)) return "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"; + if (IPV4_FORMAT.equalsIgnoreCase(format)) return "192.168.0.1"; + if (IPV6_FORMAT.equalsIgnoreCase(format)) return "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + if (BYTE_FORMAT.equalsIgnoreCase(format)) return "SGVsbG8gV29ybGQ="; + return "string"; + } + + private static String invalidString(StringSchema schema) { + String format = schema.getFormat(); + if (DATE_TIME_FORMAT.equalsIgnoreCase(format)) return invalidDateTime(schema); + 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(); + return (maxLength != null && maxLength > 0) ? "z".repeat(maxLength + 1) : "badstring"; + } + + private static String invalidDateTime(StringSchema schema) { + Object example = schema.getExample(); + 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) { + if (schema.getMaximum() != null) return schema.getMaximum().add(BigDecimal.ONE).toString(); + if (schema.getMinimum() != null) return schema.getMinimum().subtract(BigDecimal.ONE).toString(); + return "badnumber"; + } + + private static String invalidDate(DateSchema schema) { + 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 a978d80..d101006 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: @@ -58,6 +59,8 @@ paths: 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. + The response is the content of the SoapUI project in XML format to save as file and import into the SoapUI application. requestBody: description: | @@ -70,6 +73,7 @@ paths: - 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 content: aplication/json: schema: @@ -460,6 +464,10 @@ components: 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 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. 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..4c5ae6c --- /dev/null +++ b/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java @@ -0,0 +1,208 @@ +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; + +class QueryParamExampleUtilsTest { + + @Test + void invalidValue_returnsGenericValue_whenSchemaIsNull() { + assertEquals("badvalue", QueryParamExampleUtils.invalidValue(null)); + } + + @Test + void invalidValue_padsToMaxLengthPlusOne_whenStringHasMaxLength() { + StringSchema schema = new StringSchema(); + schema.setMaxLength(5); + assertEquals(6, QueryParamExampleUtils.invalidValue(schema).length()); + } + + @Test + void invalidValue_returnsGenericString_whenStringHasNoMaxLength() { + StringSchema schema = new StringSchema(); + assertEquals("badstring", QueryParamExampleUtils.invalidValue(schema)); + } + + @Test + void invalidValue_exceedsMaximum_whenIntegerHasMaximum() { + IntegerSchema schema = new IntegerSchema(); + schema.setMaximum(BigDecimal.TEN); + assertEquals("11", QueryParamExampleUtils.invalidValue(schema)); + } + + @Test + void invalidValue_goesBelowMinimum_whenNumberHasOnlyMinimum() { + NumberSchema schema = new NumberSchema(); + schema.setMinimum(BigDecimal.ZERO); + assertEquals("-1", QueryParamExampleUtils.invalidValue(schema)); + } + + @Test + void invalidValue_returnsGenericNumber_whenNoBoundsDefined() { + IntegerSchema schema = new IntegerSchema(); + assertEquals("badnumber", QueryParamExampleUtils.invalidValue(schema)); + } + + @Test + void invalidValue_returnsBadBoolean_forBooleanSchema() { + assertEquals("badboolean", QueryParamExampleUtils.invalidValue(new BooleanSchema())); + } + + @Test + void invalidValue_returnsBadArray_forArraySchema() { + assertEquals("badarray", QueryParamExampleUtils.invalidValue(new ArraySchema())); + } + + @Test + void invalidValue_returnsBadObject_forObjectSchema() { + assertEquals("badobject", QueryParamExampleUtils.invalidValue(new ObjectSchema())); + } + + @Test + void invalidValue_setsInvalidMonth_forDateSchema() { + DateSchema schema = new DateSchema(); + schema.setExample("2024-01-01"); + String[] parts = QueryParamExampleUtils.invalidValue(schema).split("-"); + assertEquals(3, parts.length); + assertEquals("50", parts[1]); + } + + @Test + void invalidValue_setsInvalidMonth_forDateSchemaWithoutExample() { + String[] parts = QueryParamExampleUtils.invalidValue(new DateSchema()).split("-"); + assertEquals(3, parts.length); + assertEquals("50", parts[1]); + } + + @Test + void validValue_returnsGenericValue_whenSchemaIsNull() { + assertEquals("value", QueryParamExampleUtils.validValue(null)); + } + + @Test + void validValue_returnsExample_whenSchemaHasExample() { + StringSchema schema = new StringSchema(); + schema.setExample("foo"); + assertEquals("foo", QueryParamExampleUtils.validValue(schema)); + } + + @Test + void validValue_returnsTypedDefault_forEachSchemaType() { + assertEquals("string", QueryParamExampleUtils.validValue(new StringSchema())); + assertTrue(QueryParamExampleUtils.validValue(new IntegerSchema()).matches("\\d+")); + assertEquals("true", QueryParamExampleUtils.validValue(new BooleanSchema())); + } + + @Test + void validValue_returnsFirstEnumValue_whenSchemaHasEnumAndNoExample() { + StringSchema schema = new StringSchema(); + schema.setEnum(java.util.List.of("asc", "desc")); + assertEquals("asc", QueryParamExampleUtils.validValue(schema)); + } + + @Test + void validValue_prefersExampleOverEnum() { + StringSchema schema = new StringSchema(); + schema.setEnum(java.util.List.of("asc", "desc")); + schema.setExample("desc"); + assertEquals("desc", QueryParamExampleUtils.validValue(schema)); + } + + @Test + void validValue_returnsIsoDateTime_forDateTimeFormattedString() { + StringSchema schema = new StringSchema(); + schema.setFormat("date-time"); + assertEquals("2024-01-01T00:00:00Z", QueryParamExampleUtils.validValue(schema)); + } + + @Test + void invalidValue_setsInvalidMonth_forDateTimeFormattedString() { + StringSchema schema = new StringSchema(); + schema.setFormat("date-time"); + assertEquals("2024-50-01T00:00:00Z", QueryParamExampleUtils.invalidValue(schema)); + } + + @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)); + } + + @Test + void validValue_returnsRealisticValue_forEmailFormat() { + StringSchema schema = new StringSchema(); + schema.setFormat("email"); + assertEquals("user@example.com", QueryParamExampleUtils.validValue(schema)); + } + + @Test + void invalidValue_hasNoAtSign_forEmailFormat() { + StringSchema schema = new StringSchema(); + schema.setFormat("email"); + assertTrue(!QueryParamExampleUtils.invalidValue(schema).contains("@")); + } + + @Test + void validValue_returnsRealisticValue_forUriFormat() { + StringSchema schema = new StringSchema(); + schema.setFormat("uri"); + assertEquals("https://example.com", QueryParamExampleUtils.validValue(schema)); + } + + @Test + void validValue_returnsRealisticValue_forUuidFormat() { + StringSchema schema = new StringSchema(); + schema.setFormat("uuid"); + assertEquals("3fa85f64-5717-4562-b3fc-2c963f66afa6", QueryParamExampleUtils.validValue(schema)); + } + + @Test + void invalidValue_isNotUuidShaped_forUuidFormat() { + StringSchema schema = new StringSchema(); + schema.setFormat("uuid"); + assertEquals("not-a-valid-uuid", QueryParamExampleUtils.invalidValue(schema)); + } + + @Test + void validValue_returnsRealisticValue_forIpv4Format() { + StringSchema schema = new StringSchema(); + schema.setFormat("ipv4"); + assertEquals("192.168.0.1", QueryParamExampleUtils.validValue(schema)); + } + + @Test + void invalidValue_hasOutOfRangeOctets_forIpv4Format() { + StringSchema schema = new StringSchema(); + schema.setFormat("ipv4"); + assertEquals("999.999.999.999", QueryParamExampleUtils.invalidValue(schema)); + } + + @Test + void validValue_returnsRealisticValue_forByteFormat() { + StringSchema schema = new StringSchema(); + schema.setFormat("byte"); + assertEquals("SGVsbG8gV29ybGQ=", QueryParamExampleUtils.validValue(schema)); + } + + @Test + void formatAwareness_stillYieldsToExplicitExample() { + StringSchema schema = new StringSchema(); + schema.setFormat("email"); + schema.setExample("custom@example.org"); + assertEquals("custom@example.org", QueryParamExampleUtils.validValue(schema)); + } +} From dc8a4369a9d529c5b558c96e9dd749c627f835b5 Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Wed, 1 Jul 2026 13:11:49 -0500 Subject: [PATCH 4/7] feat: add microcksHeaders parameter to add Microcks response-name header --- CHANGELOG.md | 1 + .../openapi2soapui/constants/Constants.java | 2 + .../controller/SoapUIProjectController.java | 3 +- .../openapi2soapui/model/SoapUIProject.java | 63 +++++++++++++++++-- .../request/SoapUIProjectRequest.java | 3 + .../service/SoapUIProjectService.java | 2 +- .../service/SoapUIProjectServiceImpl.java | 4 +- src/main/resources/static/api.yaml | 7 +++ 8 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f0536..42addc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,3 +12,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `readOnly` parameter: when set to `true`, only GET and OPTIONS test cases are generated; POST, PUT, PATCH and DELETE operations are excluded. - Added `serverPattern` parameter: optional string (e.g. `%dev%`) that selects the OpenAPI server whose URL contains the given substring (after stripping `%`). If no server matches, the first server in the list is used as fallback. When omitted, all servers are added as endpoints (existing behavior preserved). - Added `minimalEndpoints` parameter: when set to `false` (default), two additional test cases are generated per optional query parameter of each operation — one with a valid value asserting HTTP status 200, and one with an invalid value asserting HTTP status 400. When `true`, only the `testCaseNames`-based test cases are generated. +- Added `microcksHeaders` parameter: when set to `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. 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 d012ce1..3d3a90a 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java @@ -36,4 +36,6 @@ private Constants() { 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 6d64936..da50880 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java @@ -36,7 +36,8 @@ public String newSoapUIProject(@Valid @RequestBody SoapUIProjectRequest newSoapU } SoapUIProject soapUIProject = soapUIProjectService.createSoapUIProject(newSoapUIProject.getApiName(), openAPI, newSoapUIProject.getOAuth2Profiles(), newSoapUIProject.getHeaders(), newSoapUIProject.getTestCaseNames(), - newSoapUIProject.getReadOnly(), newSoapUIProject.getServerPattern(), newSoapUIProject.getMinimalEndpoints()); + newSoapUIProject.getReadOnly(), newSoapUIProject.getServerPattern(), newSoapUIProject.getMinimalEndpoints(), + newSoapUIProject.getMicrocksHeaders()); 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 feda1f6..2602203 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java @@ -18,6 +18,7 @@ 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; @@ -146,6 +147,10 @@ public class SoapUIProject { * 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; /** * 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 @@ -171,11 +176,12 @@ public class SoapUIProject { * @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 * @throws IOException * @throws XmlException * @throws SoapUIException */ - public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints) throws IOException, XmlException, SoapUIException { + public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders) throws IOException, XmlException, SoapUIException { this.apiName = apiName; this.openAPI = openAPI; this.headers = headers; @@ -190,6 +196,7 @@ public SoapUIProject(String apiName, OpenAPI openAPI, List 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 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 76fc105..13172a6 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java @@ -46,4 +46,7 @@ public class SoapUIProjectRequest { @JsonProperty("minimalEndpoints") private Boolean minimalEndpoints; + + @JsonProperty("microcksHeaders") + private Boolean microcksHeaders; } 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 649098b..c25408c 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java @@ -12,5 +12,5 @@ import org.apache.xmlbeans.XmlException; public interface SoapUIProjectService { - SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCases, Boolean readOnly, String serverPattern, Boolean minimalEndpoints) throws IOException, XmlException, SoapUIException; + SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCases, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders) 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 6239396..60e2b97 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java @@ -18,8 +18,8 @@ public class SoapUIProjectServiceImpl implements SoapUIProjectService { @Override public SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List credentials, List
headers, - Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints) throws IOException, XmlException, SoapUIException { - return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints); + Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders) throws IOException, XmlException, SoapUIException { + return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints, microcksHeaders); } } diff --git a/src/main/resources/static/api.yaml b/src/main/resources/static/api.yaml index d101006..f490932 100644 --- a/src/main/resources/static/api.yaml +++ b/src/main/resources/static/api.yaml @@ -61,6 +61,8 @@ paths: 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. + The response is the content of the SoapUI project in XML format to save as file and import into the SoapUI application. requestBody: description: | @@ -74,6 +76,7 @@ paths: - 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 content: aplication/json: schema: @@ -468,6 +471,10 @@ components: 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 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. From 5d2e5d0c6f5f99c891ac21ed4e1148b3090af3e1 Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Wed, 1 Jul 2026 16:48:12 -0500 Subject: [PATCH 5/7] feat: add generateOneOfAnyOf parameter to resolve oneOf/anyOf/allOf schemas in example bodies --- CHANGELOG.md | 1 + .../controller/SoapUIProjectController.java | 2 +- .../openapi2soapui/model/SoapUIProject.java | 58 ++++++++++++++++++- .../request/SoapUIProjectRequest.java | 3 + .../service/SoapUIProjectService.java | 2 +- .../service/SoapUIProjectServiceImpl.java | 4 +- src/main/resources/static/api.yaml | 7 +++ 7 files changed, 70 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42addc4..7ff54fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,3 +13,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `serverPattern` parameter: optional string (e.g. `%dev%`) that selects the OpenAPI server whose URL contains the given substring (after stripping `%`). If no server matches, the first server in the list is used as fallback. When omitted, all servers are added as endpoints (existing behavior preserved). - Added `minimalEndpoints` parameter: when set to `false` (default), two additional test cases are generated per optional query parameter of each operation — one with a valid value asserting HTTP status 200, and one with an invalid value asserting HTTP status 400. When `true`, only the `testCaseNames`-based test cases are generated. - Added `microcksHeaders` parameter: when set to `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. +- Added `generateOneOfAnyOf` parameter: when set to `true`, `oneOf`/`anyOf` schemas are resolved to their first candidate schema when generating the example request body (default: `false`, left unresolved). `allOf` schemas are always merged into a single object, regardless of this flag. 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 da50880..7fa9e5c 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java @@ -37,7 +37,7 @@ public String newSoapUIProject(@Valid @RequestBody SoapUIProjectRequest newSoapU SoapUIProject soapUIProject = soapUIProjectService.createSoapUIProject(newSoapUIProject.getApiName(), openAPI, newSoapUIProject.getOAuth2Profiles(), newSoapUIProject.getHeaders(), newSoapUIProject.getTestCaseNames(), newSoapUIProject.getReadOnly(), newSoapUIProject.getServerPattern(), newSoapUIProject.getMinimalEndpoints(), - newSoapUIProject.getMicrocksHeaders()); + newSoapUIProject.getMicrocksHeaders(), newSoapUIProject.getGenerateOneOfAnyOf()); 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 2602203..a306d4c 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java @@ -151,6 +151,11 @@ public class SoapUIProject { * 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; /** * 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 @@ -177,11 +182,12 @@ public class SoapUIProject { * @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 * @throws IOException * @throws XmlException * @throws SoapUIException */ - public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders) 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) throws IOException, XmlException, SoapUIException { this.apiName = apiName; this.openAPI = openAPI; this.headers = headers; @@ -197,6 +203,7 @@ public SoapUIProject(String apiName, OpenAPI openAPI, List 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()) { @@ -624,8 +631,9 @@ private JSONObject iterateProperties(Map properties, RefResolver @SuppressWarnings("rawtypes") private Object getPropertyExample(Schema property, RefResolver refResolver) throws JSONException { Object example = property.getExample(); - + if (example == null) { + property = resolveComposedSchema(property, refResolver); if (property instanceof ObjectSchema) { example = iterateProperties(((ObjectSchema) property).getProperties(), refResolver); } else if (property instanceof ArraySchema) { @@ -657,6 +665,50 @@ private Object getPropertyExample(Schema property, RefResolver refResolver) thro return example; } + /** + * 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 = schema; + List allOf = schema.getAllOf(); + if (allOf != null && !allOf.isEmpty()) { + 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); + resolved = merged; + } else if (generateOneOfAnyOf && schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { + resolved = refResolver.resolveSchema((Schema) schema.getOneOf().get(0)); + } else if (generateOneOfAnyOf && schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) { + resolved = refResolver.resolveSchema((Schema) schema.getAnyOf().get(0)); + } + if (resolved == schema) { + return resolved; + } + schema = resolved; + } + return schema; + } + /** * Convert Object or JSONObject to JSON String * @param object to convert 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 13172a6..34ed927 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java @@ -49,4 +49,7 @@ public class SoapUIProjectRequest { @JsonProperty("microcksHeaders") private Boolean microcksHeaders; + + @JsonProperty("generateOneOfAnyOf") + private Boolean generateOneOfAnyOf; } 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 c25408c..26a2778 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java @@ -12,5 +12,5 @@ import org.apache.xmlbeans.XmlException; public interface SoapUIProjectService { - SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCases, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders) throws IOException, XmlException, SoapUIException; + SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCases, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf) 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 60e2b97..e77a642 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java @@ -18,8 +18,8 @@ public class SoapUIProjectServiceImpl implements SoapUIProjectService { @Override public SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List credentials, List
headers, - Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders) throws IOException, XmlException, SoapUIException { - return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints, microcksHeaders); + Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf) throws IOException, XmlException, SoapUIException { + return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints, microcksHeaders, generateOneOfAnyOf); } } diff --git a/src/main/resources/static/api.yaml b/src/main/resources/static/api.yaml index f490932..b5f7e6b 100644 --- a/src/main/resources/static/api.yaml +++ b/src/main/resources/static/api.yaml @@ -63,6 +63,8 @@ paths: 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: | @@ -77,6 +79,7 @@ paths: - 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: @@ -475,6 +478,10 @@ components: 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. From 4f7d091797c3cf50909e28c03435400daa0d9a1e Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Thu, 2 Jul 2026 08:50:37 -0500 Subject: [PATCH 6/7] feat: add examples parameter for custom sample values --- CHANGELOG.md | 13 +- pom.xml | 5 +- .../controller/SoapUIProjectController.java | 2 +- .../openapi2soapui/model/SoapUIProject.java | 46 +++++- .../openapi2soapui/request/ExampleValues.java | 28 ++++ .../request/ExamplesConfig.java | 21 +++ .../request/SoapUIProjectRequest.java | 4 + .../service/SoapUIProjectService.java | 3 +- .../service/SoapUIProjectServiceImpl.java | 5 +- .../util/QueryParamExampleUtils.java | 55 ++++--- src/main/resources/static/api.yaml | 30 ++++ .../util/QueryParamExampleUtilsTest.java | 138 ++++++++++++++---- 12 files changed, 280 insertions(+), 70 deletions(-) create mode 100644 src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExampleValues.java create mode 100644 src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExamplesConfig.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ff54fd..30f5741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,12 @@ 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] - 2026-06-30 +## [1.1.0-beta-1] - 2026-07-02 ### Added -- Added `readOnly` parameter: when set to `true`, only GET and OPTIONS test cases are generated; POST, PUT, PATCH and DELETE operations are excluded. -- Added `serverPattern` parameter: optional string (e.g. `%dev%`) that selects the OpenAPI server whose URL contains the given substring (after stripping `%`). If no server matches, the first server in the list is used as fallback. When omitted, all servers are added as endpoints (existing behavior preserved). -- Added `minimalEndpoints` parameter: when set to `false` (default), two additional test cases are generated per optional query parameter of each operation — one with a valid value asserting HTTP status 200, and one with an invalid value asserting HTTP status 400. When `true`, only the `testCaseNames`-based test cases are generated. -- Added `microcksHeaders` parameter: when set to `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. -- Added `generateOneOfAnyOf` parameter: when set to `true`, `oneOf`/`anyOf` schemas are resolved to their first candidate schema when generating the example request body (default: `false`, left unresolved). `allOf` schemas are always merged into a single object, regardless of this flag. +- `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/controller/SoapUIProjectController.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java index 7fa9e5c..fa2086a 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java @@ -37,7 +37,7 @@ public String newSoapUIProject(@Valid @RequestBody SoapUIProjectRequest newSoapU SoapUIProject soapUIProject = soapUIProjectService.createSoapUIProject(newSoapUIProject.getApiName(), openAPI, newSoapUIProject.getOAuth2Profiles(), newSoapUIProject.getHeaders(), newSoapUIProject.getTestCaseNames(), newSoapUIProject.getReadOnly(), newSoapUIProject.getServerPattern(), newSoapUIProject.getMinimalEndpoints(), - newSoapUIProject.getMicrocksHeaders(), newSoapUIProject.getGenerateOneOfAnyOf()); + newSoapUIProject.getMicrocksHeaders(), newSoapUIProject.getGenerateOneOfAnyOf(), newSoapUIProject.getExamples()); 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 a306d4c..55086fa 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java @@ -98,6 +98,8 @@ 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; @@ -156,6 +158,10 @@ public class SoapUIProject { * 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 @@ -183,14 +189,16 @@ public class SoapUIProject { * @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, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf) 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(); @@ -642,20 +650,22 @@ private Object getPropertyExample(Schema property, RefResolver refResolver) thro jsonArray.put(getPropertyExample(items, refResolver)); example = jsonArray; } else if (property instanceof IntegerSchema) { - example = 0; + example = getConfiguredExample(false, ExampleValues::getNumber, java.math.BigDecimal.ZERO); } else if (property instanceof NumberSchema) { - example = 0; + example = getConfiguredExample(false, ExampleValues::getNumber, java.math.BigDecimal.ZERO); } else if (property instanceof BooleanSchema) { - example = true; + example = getConfiguredExample(false, ExampleValues::getBooleanValue, true); } else if (property instanceof DateSchema) { - example = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + example = getConfiguredExample(false, ExampleValues::getDate, 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 if ("date-time".equalsIgnoreCase(stringProperty.getFormat())) { + example = getConfiguredExample(false, ExampleValues::getDateTime, ""); } else { - example = ""; + example = getConfiguredExample(false, ExampleValues::getString, ""); } } else { example = ""; @@ -665,6 +675,26 @@ private Object getPropertyExample(Schema property, RefResolver refResolver) thro return example; } + /** + * 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 @@ -938,7 +968,7 @@ private void addQueryParamVariantTestCase(RestMethod restMethod, RestRequest def queryParams.forEach(param -> { boolean isTarget = param.getName().equals(targetParam.getName()); - String value = (isTarget && wrong) ? QueryParamExampleUtils.invalidValue(param.getSchema()) : getValidQueryParamValue(param); + String value = (isTarget && wrong) ? QueryParamExampleUtils.invalidValue(param.getSchema(), examples != null ? examples.getWrong() : null) : getValidQueryParamValue(param); variantRequest.setPropertyValue(param.getName(), value); }); @@ -962,7 +992,7 @@ private void addQueryParamVariantTestCase(RestMethod restMethod, RestRequest def private String getValidQueryParamValue(Parameter param) { Object example = getParameterExample(param); if (example != null && !example.toString().isBlank()) return example.toString(); - return QueryParamExampleUtils.validValue(param.getSchema()); + return QueryParamExampleUtils.validValue(param.getSchema(), examples != null ? examples.getSuccessful() : 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 34ed927..d93d055 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java @@ -52,4 +52,8 @@ public class SoapUIProjectRequest { @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 26a2778..b616478 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java @@ -9,8 +9,9 @@ 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.ExamplesConfig; import org.apache.xmlbeans.XmlException; public interface SoapUIProjectService { - SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCases, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf) throws IOException, XmlException, SoapUIException; + SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCases, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf, ExamplesConfig examples) 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 e77a642..f8f4534 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java @@ -12,14 +12,15 @@ 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.ExamplesConfig; @Service public class SoapUIProjectServiceImpl implements SoapUIProjectService { @Override public SoapUIProject createSoapUIProject(String apiName, OpenAPI openAPI, List credentials, List
headers, - Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf) throws IOException, XmlException, SoapUIException { - return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints, microcksHeaders, generateOneOfAnyOf); + Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf, ExamplesConfig examples) throws IOException, XmlException, SoapUIException { + return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints, microcksHeaders, generateOneOfAnyOf, examples); } } diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java b/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java index c1e3fe9..150dc95 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java @@ -13,6 +13,8 @@ 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() { @@ -29,33 +31,39 @@ private QueryParamExampleUtils() { private static final String IPV6_FORMAT = "ipv6"; private static final String BYTE_FORMAT = "byte"; - public static String validValue(Schema schema) { + public static String validValue(Schema schema, ExampleValues successfulExamples) { if (schema == null) return "value"; - if (schema instanceof DateSchema) return formatDateExample((DateSchema) schema, "2024-01-01"); + 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); - if (schema instanceof IntegerSchema || schema instanceof NumberSchema) return "1"; - if (schema instanceof BooleanSchema) return "true"; + if (schema instanceof StringSchema) return validStringValue((StringSchema) schema, successfulExamples); + if (schema instanceof IntegerSchema || schema instanceof NumberSchema) return configuredOrDefault(successfulExamples, v -> v.getNumber() != null ? v.getNumber().toString() : null, "1"); + if (schema instanceof BooleanSchema) return configuredOrDefault(successfulExamples, v -> v.getBooleanValue() != null ? v.getBooleanValue().toString() : null, "true"); if (schema instanceof ArraySchema) return "[]"; if (schema instanceof ObjectSchema) return "{}"; return "value"; } - public static String invalidValue(Schema schema) { + public static String invalidValue(Schema schema, ExampleValues wrongExamples) { if (schema == null) return "badvalue"; - if (schema instanceof StringSchema) return invalidString((StringSchema) schema); - if (schema instanceof DateSchema) return invalidDate((DateSchema) schema); - if (schema instanceof IntegerSchema || schema instanceof NumberSchema) return invalidNumber(schema); - if (schema instanceof BooleanSchema) return "badboolean"; + 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 validStringValue(StringSchema schema) { + 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 "2024-01-01T00:00:00Z"; + 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"; @@ -63,12 +71,12 @@ private static String validStringValue(StringSchema schema) { if (IPV4_FORMAT.equalsIgnoreCase(format)) return "192.168.0.1"; if (IPV6_FORMAT.equalsIgnoreCase(format)) return "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; if (BYTE_FORMAT.equalsIgnoreCase(format)) return "SGVsbG8gV29ybGQ="; - return "string"; + return configuredOrDefault(successfulExamples, ExampleValues::getString, "string"); } - private static String invalidString(StringSchema schema) { + private static String invalidString(StringSchema schema, ExampleValues wrongExamples) { String format = schema.getFormat(); - if (DATE_TIME_FORMAT.equalsIgnoreCase(format)) return invalidDateTime(schema); + 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"; @@ -77,23 +85,30 @@ private static String invalidString(StringSchema schema) { if (IPV6_FORMAT.equalsIgnoreCase(format)) return "not:a:valid:ipv6:zzzz"; if (BYTE_FORMAT.equalsIgnoreCase(format)) return "not_base64!!!"; Integer maxLength = schema.getMaxLength(); - return (maxLength != null && maxLength > 0) ? "z".repeat(maxLength + 1) : "badstring"; + if (maxLength != null && maxLength > 0) return "z".repeat(maxLength + 1); + return configuredOrDefault(wrongExamples, ExampleValues::getString, "badstring"); } - private static String invalidDateTime(StringSchema schema) { + 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) { + 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 "badnumber"; + return configuredOrDefault(wrongExamples, v -> v.getNumber() != null ? v.getNumber().toString() : null, "badnumber"); } - private static String invalidDate(DateSchema schema) { + 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) { diff --git a/src/main/resources/static/api.yaml b/src/main/resources/static/api.yaml index b5f7e6b..d54041f 100644 --- a/src/main/resources/static/api.yaml +++ b/src/main/resources/static/api.yaml @@ -486,6 +486,36 @@ components: 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 index 4c5ae6c..c6a671d 100644 --- a/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java +++ b/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java @@ -15,101 +15,103 @@ 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)); + assertEquals("badvalue", QueryParamExampleUtils.invalidValue(null, null)); } @Test void invalidValue_padsToMaxLengthPlusOne_whenStringHasMaxLength() { StringSchema schema = new StringSchema(); schema.setMaxLength(5); - assertEquals(6, QueryParamExampleUtils.invalidValue(schema).length()); + assertEquals(6, QueryParamExampleUtils.invalidValue(schema, null).length()); } @Test void invalidValue_returnsGenericString_whenStringHasNoMaxLength() { StringSchema schema = new StringSchema(); - assertEquals("badstring", QueryParamExampleUtils.invalidValue(schema)); + assertEquals("badstring", QueryParamExampleUtils.invalidValue(schema, null)); } @Test void invalidValue_exceedsMaximum_whenIntegerHasMaximum() { IntegerSchema schema = new IntegerSchema(); schema.setMaximum(BigDecimal.TEN); - assertEquals("11", QueryParamExampleUtils.invalidValue(schema)); + assertEquals("11", QueryParamExampleUtils.invalidValue(schema, null)); } @Test void invalidValue_goesBelowMinimum_whenNumberHasOnlyMinimum() { NumberSchema schema = new NumberSchema(); schema.setMinimum(BigDecimal.ZERO); - assertEquals("-1", QueryParamExampleUtils.invalidValue(schema)); + assertEquals("-1", QueryParamExampleUtils.invalidValue(schema, null)); } @Test void invalidValue_returnsGenericNumber_whenNoBoundsDefined() { IntegerSchema schema = new IntegerSchema(); - assertEquals("badnumber", QueryParamExampleUtils.invalidValue(schema)); + assertEquals("badnumber", QueryParamExampleUtils.invalidValue(schema, null)); } @Test void invalidValue_returnsBadBoolean_forBooleanSchema() { - assertEquals("badboolean", QueryParamExampleUtils.invalidValue(new BooleanSchema())); + assertEquals("badboolean", QueryParamExampleUtils.invalidValue(new BooleanSchema(), null)); } @Test void invalidValue_returnsBadArray_forArraySchema() { - assertEquals("badarray", QueryParamExampleUtils.invalidValue(new ArraySchema())); + assertEquals("badarray", QueryParamExampleUtils.invalidValue(new ArraySchema(), null)); } @Test void invalidValue_returnsBadObject_forObjectSchema() { - assertEquals("badobject", QueryParamExampleUtils.invalidValue(new ObjectSchema())); + 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).split("-"); + 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()).split("-"); + 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)); + assertEquals("value", QueryParamExampleUtils.validValue(null, null)); } @Test void validValue_returnsExample_whenSchemaHasExample() { StringSchema schema = new StringSchema(); schema.setExample("foo"); - assertEquals("foo", QueryParamExampleUtils.validValue(schema)); + assertEquals("foo", QueryParamExampleUtils.validValue(schema, null)); } @Test void validValue_returnsTypedDefault_forEachSchemaType() { - assertEquals("string", QueryParamExampleUtils.validValue(new StringSchema())); - assertTrue(QueryParamExampleUtils.validValue(new IntegerSchema()).matches("\\d+")); - assertEquals("true", QueryParamExampleUtils.validValue(new BooleanSchema())); + 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)); + assertEquals("asc", QueryParamExampleUtils.validValue(schema, null)); } @Test @@ -117,21 +119,21 @@ void validValue_prefersExampleOverEnum() { StringSchema schema = new StringSchema(); schema.setEnum(java.util.List.of("asc", "desc")); schema.setExample("desc"); - assertEquals("desc", QueryParamExampleUtils.validValue(schema)); + 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)); + 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)); + assertEquals("2024-50-01T00:00:00Z", QueryParamExampleUtils.invalidValue(schema, null)); } @Test @@ -139,63 +141,63 @@ 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)); + 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)); + assertEquals("user@example.com", QueryParamExampleUtils.validValue(schema, null)); } @Test void invalidValue_hasNoAtSign_forEmailFormat() { StringSchema schema = new StringSchema(); schema.setFormat("email"); - assertTrue(!QueryParamExampleUtils.invalidValue(schema).contains("@")); + 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)); + 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)); + 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)); + assertEquals("not-a-valid-uuid", QueryParamExampleUtils.invalidValue(schema, null)); } @Test void validValue_returnsRealisticValue_forIpv4Format() { StringSchema schema = new StringSchema(); schema.setFormat("ipv4"); - assertEquals("192.168.0.1", QueryParamExampleUtils.validValue(schema)); + assertEquals("192.168.0.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)); + 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)); + assertEquals("SGVsbG8gV29ybGQ=", QueryParamExampleUtils.validValue(schema, null)); } @Test @@ -203,6 +205,82 @@ void formatAwareness_stillYieldsToExplicitExample() { StringSchema schema = new StringSchema(); schema.setFormat("email"); schema.setExample("custom@example.org"); - assertEquals("custom@example.org", QueryParamExampleUtils.validValue(schema)); + 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)); } } From 1a5b92fea7656978372cff1fa59e6b47319eec6f Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Thu, 2 Jul 2026 10:00:55 -0500 Subject: [PATCH 7/7] fix: sonar issues --- .../controller/SoapUIProjectController.java | 5 +- .../openapi2soapui/model/SoapUIProject.java | 210 +++++++++++------- .../service/SoapUIProjectService.java | 8 +- .../service/SoapUIProjectServiceImpl.java | 15 +- .../util/QueryParamExampleUtils.java | 17 +- .../util/QueryParamExampleUtilsTest.java | 2 +- 6 files changed, 156 insertions(+), 101 deletions(-) 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 fa2086a..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,10 +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(), - newSoapUIProject.getReadOnly(), newSoapUIProject.getServerPattern(), newSoapUIProject.getMinimalEndpoints(), - newSoapUIProject.getMicrocksHeaders(), newSoapUIProject.getGenerateOneOfAnyOf(), newSoapUIProject.getExamples()); + 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 55086fa..c508a37 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java @@ -637,42 +637,56 @@ 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) { + return example; + } + return getExampleForResolvedType(resolveComposedSchema(property, refResolver), refResolver); + } - if (example == null) { - property = resolveComposedSchema(property, refResolver); - 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 = getConfiguredExample(false, ExampleValues::getNumber, java.math.BigDecimal.ZERO); - } else if (property instanceof NumberSchema) { - example = getConfiguredExample(false, ExampleValues::getNumber, java.math.BigDecimal.ZERO); - } else if (property instanceof BooleanSchema) { - example = getConfiguredExample(false, ExampleValues::getBooleanValue, true); - } else if (property instanceof DateSchema) { - example = getConfiguredExample(false, ExampleValues::getDate, 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 if ("date-time".equalsIgnoreCase(stringProperty.getFormat())) { - example = getConfiguredExample(false, ExampleValues::getDateTime, ""); - } else { - example = getConfiguredExample(false, ExampleValues::getString, ""); - } - } else { - example = ""; - } + /** + * 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 example; + 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, ""); } /** @@ -708,29 +722,7 @@ private T getConfiguredExample(boolean wrong, java.util.function.Function allOf = schema.getAllOf(); - if (allOf != null && !allOf.isEmpty()) { - 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); - resolved = merged; - } else if (generateOneOfAnyOf && schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { - resolved = refResolver.resolveSchema((Schema) schema.getOneOf().get(0)); - } else if (generateOneOfAnyOf && schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) { - resolved = refResolver.resolveSchema((Schema) schema.getAnyOf().get(0)); - } + Schema resolved = resolveComposedSchemaOnce(schema, refResolver); if (resolved == schema) { return resolved; } @@ -739,6 +731,50 @@ private Schema resolveComposedSchema(Schema schema, RefResolver refResolver) { 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; + } + /** * Convert Object or JSONObject to JSON String * @param object to convert @@ -869,27 +905,35 @@ 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(); - 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); - } - }); - } - }); + 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); } } @@ -968,7 +1012,7 @@ private void addQueryParamVariantTestCase(RestMethod restMethod, RestRequest def queryParams.forEach(param -> { boolean isTarget = param.getName().equals(targetParam.getName()); - String value = (isTarget && wrong) ? QueryParamExampleUtils.invalidValue(param.getSchema(), examples != null ? examples.getWrong() : null) : getValidQueryParamValue(param); + String value = (isTarget && wrong) ? getInvalidQueryParamValue(param) : getValidQueryParamValue(param); variantRequest.setPropertyValue(param.getName(), value); }); @@ -995,6 +1039,16 @@ private String getValidQueryParamValue(Parameter param) { 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); + } + /** * Get content of SoapUI Project File (XML) * @return SoapUI Project file content 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 b616478..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,17 +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.ExamplesConfig; +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, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf, ExamplesConfig examples) 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 f8f4534..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,17 +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.ExamplesConfig; +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, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf, ExamplesConfig examples) throws IOException, XmlException, SoapUIException { - return new SoapUIProject(apiName, openAPI, credentials, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints, microcksHeaders, generateOneOfAnyOf, examples); + 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 index 150dc95..be7a104 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java +++ b/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java @@ -37,13 +37,23 @@ public static String validValue(Schema schema, ExampleValues successfulExampl 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 configuredOrDefault(successfulExamples, v -> v.getNumber() != null ? v.getNumber().toString() : null, "1"); - if (schema instanceof BooleanSchema) return configuredOrDefault(successfulExamples, v -> v.getBooleanValue() != null ? v.getBooleanValue().toString() : null, "true"); + 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); @@ -68,7 +78,8 @@ private static String validStringValue(StringSchema schema, ExampleValues succes 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"; - if (IPV4_FORMAT.equalsIgnoreCase(format)) return "192.168.0.1"; + // 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"); diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java b/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java index c6a671d..14873b0 100644 --- a/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java +++ b/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java @@ -183,7 +183,7 @@ void invalidValue_isNotUuidShaped_forUuidFormat() { void validValue_returnsRealisticValue_forIpv4Format() { StringSchema schema = new StringSchema(); schema.setFormat("ipv4"); - assertEquals("192.168.0.1", QueryParamExampleUtils.validValue(schema, null)); + assertEquals("192.0.2.1", QueryParamExampleUtils.validValue(schema, null)); } @Test