From 6e6e6ac787cead958d92f2a7c4a91de9285a606a Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Wed, 26 Aug 2026 16:46:44 -0500 Subject: [PATCH 1/6] feat: create new openapi2soapui cli --- .../skills/generar-proyecto-soapui/SKILL.md | 48 +- CHANGELOG.md | 21 + README.md | 145 ++++-- docker-compose.yml | 2 +- openapi2soapui-cli/pom.xml | 94 ++++ .../apitools/openapi2soapui/cli/CliArgs.java | 281 +++++++++++ .../openapi2soapui/cli/CliValidator.java | 47 ++ .../openapi2soapui/cli/Openapi2SoapUICli.java | 140 ++++++ .../openapi2soapui/cli/SoapUILogging.java | 53 +++ .../src/main/resources/cli.properties | 1 + .../src/main/resources/logback.xml | 15 + .../src/main/resources/soapui-cli-log4j.xml | 23 + .../cli/Openapi2SoapUICliTest.java | 140 ++++++ .../src/test/resources/petstore.yaml | 79 ++++ .../src/test/resources/request.json | 5 + openapi2soapui-core/pom.xml | 342 ++++++++++++++ .../openapi2soapui/constants/Constants.java | 13 +- .../APIVersionNotFoundException.java | 5 +- .../CreateEnumInstanceException.java | 8 +- .../exceptions/DecodeBase64Exception.java | 3 - .../exceptions/ParseOpenAPIException.java | 3 - .../SwaggerContentEmptyException.java | 5 +- .../SwaggerInvalidContentException.java | 5 +- .../validators/AuthenticationConditional.java | 3 - .../AuthenticationConditionalValidator.java | 39 +- .../error/validators/Conditionals.java | 0 .../openapi2soapui/model/SoapUIProject.java | 446 ++---------------- .../request/AccessTokenPosition.java | 12 +- .../request/CustomAuthorizationRequest.java | 0 .../openapi2soapui/request/ExampleValues.java | 0 .../request/ExamplesConfig.java | 0 .../openapi2soapui/request/GrantType.java | 12 +- .../openapi2soapui/request/Header.java | 4 +- .../openapi2soapui/request/OAuth2Profile.java | 24 +- .../request/SoapUIProjectRequest.java | 4 +- .../util/QueryParamExampleUtils.java | 1 - .../openapi2soapui/util/RefResolver.java | 6 +- .../util/SerializedDataUtils.java | 18 +- .../util/SwaggerContentDeserializer.java | 4 - .../src}/main/resources/messages.properties | 0 .../model/ApplicationTokenTest.java | 0 .../model/CustomAuthorizationsFileTest.java | 0 .../openapi2soapui/model/HasScopesTest.java | 5 - .../openapi2soapui/model/IsInlineTest.java | 2 - .../model/MicrocksHeadersStatusTest.java | 4 - .../model/MinimalEndpointsTest.java | 5 - .../model/SchemaIsInlineTest.java | 0 .../model/SchemaPrettyPrintTest.java | 0 .../model/ServerPatternTest.java | 4 - .../ServiceApiConventionCompositionTest.java | 0 .../model/ServiceApiConventionTest.java | 38 +- .../model/Swagger2SpecTest.java | 101 ++++ .../model/ValidateSchemaScriptTest.java | 5 - .../util/QueryParamExampleUtilsTest.java | 0 .../src/test/resources/soapui-test-log4j.xml | 22 + openapi2soapui-rest/pom.xml | 99 ++++ .../Openapi2SoapUIApplication.java | 0 .../config/MessageSourceConfig.java | 13 +- .../controller/SoapUIProjectController.java | 0 .../apitools/openapi2soapui/error/Error.java | 0 .../error/ObjectErrorTypes.java | 0 .../apitools/openapi2soapui/error/Result.java | 3 - .../openapi2soapui/error/ValidationError.java | 4 +- .../error/WebControllerAdvice.java | 22 +- .../service/SoapUIProjectService.java | 0 .../service/SoapUIProjectServiceImpl.java | 0 .../util/LowerCaseClassNameResolver.java | 0 .../src/main/resources/application.properties | 4 + .../src}/main/resources/banner.txt | 0 .../src}/main/resources/log4j.properties | 0 .../src}/main/resources/static/api.yaml | 0 .../Openapi2soapuiApplicationTests.java | 0 ...ProjectControllerApplicationTokenTest.java | 0 ...ontrollerCustomAuthorizationsFileTest.java | 0 .../SoapUIProjectControllerHasScopesTest.java | 0 pom.xml | 390 ++------------- src/main/resources/application.properties | 8 - 77 files changed, 1781 insertions(+), 999 deletions(-) create mode 100644 openapi2soapui-cli/pom.xml create mode 100644 openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java create mode 100644 openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliValidator.java create mode 100644 openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java create mode 100644 openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java create mode 100644 openapi2soapui-cli/src/main/resources/cli.properties create mode 100644 openapi2soapui-cli/src/main/resources/logback.xml create mode 100644 openapi2soapui-cli/src/main/resources/soapui-cli-log4j.xml create mode 100644 openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java create mode 100644 openapi2soapui-cli/src/test/resources/petstore.yaml create mode 100644 openapi2soapui-cli/src/test/resources/request.json create mode 100644 openapi2soapui-core/pom.xml rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java (97%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/APIVersionNotFoundException.java (94%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/CreateEnumInstanceException.java (97%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/DecodeBase64Exception.java (95%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/ParseOpenAPIException.java (95%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerContentEmptyException.java (94%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerInvalidContentException.java (94%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditional.java (88%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditionalValidator.java (81%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/Conditionals.java (100%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java (78%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/request/AccessTokenPosition.java (98%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/request/CustomAuthorizationRequest.java (100%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExampleValues.java (100%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExamplesConfig.java (100%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/request/GrantType.java (98%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/request/Header.java (99%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/request/OAuth2Profile.java (98%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java (99%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java (98%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/util/RefResolver.java (89%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java (87%) rename {src => openapi2soapui-core/src}/main/java/org/apiaddicts/apitools/openapi2soapui/util/SwaggerContentDeserializer.java (93%) rename {src => openapi2soapui-core/src}/main/resources/messages.properties (100%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/ApplicationTokenTest.java (100%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/CustomAuthorizationsFileTest.java (100%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/HasScopesTest.java (98%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/IsInlineTest.java (97%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/MicrocksHeadersStatusTest.java (96%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/MinimalEndpointsTest.java (94%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/SchemaIsInlineTest.java (100%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/SchemaPrettyPrintTest.java (100%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServerPatternTest.java (94%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionCompositionTest.java (100%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionTest.java (92%) create mode 100644 openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/Swagger2SpecTest.java rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/model/ValidateSchemaScriptTest.java (95%) rename {src => openapi2soapui-core/src}/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java (100%) create mode 100644 openapi2soapui-core/src/test/resources/soapui-test-log4j.xml create mode 100644 openapi2soapui-rest/pom.xml rename {src => openapi2soapui-rest/src}/main/java/org/apiaddicts/apitools/openapi2soapui/Openapi2SoapUIApplication.java (100%) rename {src => openapi2soapui-rest/src}/main/java/org/apiaddicts/apitools/openapi2soapui/config/MessageSourceConfig.java (81%) rename {src => openapi2soapui-rest/src}/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java (100%) rename {src => openapi2soapui-rest/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/Error.java (100%) rename {src => openapi2soapui-rest/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/ObjectErrorTypes.java (100%) rename {src => openapi2soapui-rest/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/Result.java (97%) rename {src => openapi2soapui-rest/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/ValidationError.java (99%) rename {src => openapi2soapui-rest/src}/main/java/org/apiaddicts/apitools/openapi2soapui/error/WebControllerAdvice.java (93%) rename {src => openapi2soapui-rest/src}/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java (100%) rename {src => openapi2soapui-rest/src}/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java (100%) rename {src => openapi2soapui-rest/src}/main/java/org/apiaddicts/apitools/openapi2soapui/util/LowerCaseClassNameResolver.java (100%) create mode 100644 openapi2soapui-rest/src/main/resources/application.properties rename {src => openapi2soapui-rest/src}/main/resources/banner.txt (100%) rename {src => openapi2soapui-rest/src}/main/resources/log4j.properties (100%) rename {src => openapi2soapui-rest/src}/main/resources/static/api.yaml (100%) rename {src => openapi2soapui-rest/src}/test/java/org/apiaddicts/apitools/openapi2soapui/Openapi2soapuiApplicationTests.java (100%) rename {src => openapi2soapui-rest/src}/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerApplicationTokenTest.java (100%) rename {src => openapi2soapui-rest/src}/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerCustomAuthorizationsFileTest.java (100%) rename {src => openapi2soapui-rest/src}/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerHasScopesTest.java (100%) delete mode 100644 src/main/resources/application.properties diff --git a/.claude/skills/generar-proyecto-soapui/SKILL.md b/.claude/skills/generar-proyecto-soapui/SKILL.md index 9c66133..9b801e8 100644 --- a/.claude/skills/generar-proyecto-soapui/SKILL.md +++ b/.claude/skills/generar-proyecto-soapui/SKILL.md @@ -1,13 +1,51 @@ --- name: generar-proyecto-soapui -description: Enseña cómo llamar la API propia de este repo (openapi2soapui) para generar un proyecto SoapUI en XML a partir de un spec OpenAPI, incluyendo el contrato completo del request (parámetros, defaults, validaciones). Úsala cuando el usuario pida generar el proyecto/colección SoapUI con la API, llamar al endpoint de openapi2soapui, crear la colección vía API, o necesite saber qué parámetros/configuración acepta la generación (oAuth2Profiles, headers, customAuthorizationsFile, testCaseNames, flags como readOnly/hasScopes/validateSchema, etc.), incluso si no menciona el nombre exacto del endpoint. NO cubre ejecutar las pruebas generadas con SoapUI TestRunner ni levantar el servicio con Docker — para eso no uses esta skill. +description: Enseña cómo generar un proyecto SoapUI en XML a partir de un spec OpenAPI con este repo (openapi2soapui), por sus dos vías — la API HTTP propia y el CLI (`openapi2soapui-cli.jar`) — incluyendo el contrato completo del request (parámetros, defaults, validaciones) y su equivalencia en flags. Úsala cuando el usuario pida generar el proyecto/colección SoapUI con la API o por línea de comandos, llamar al endpoint de openapi2soapui, crear la colección vía API/CLI, o necesite saber qué parámetros/configuración acepta la generación (oAuth2Profiles, headers, customAuthorizationsFile, testCaseNames, flags como readOnly/hasScopes/validateSchema, etc.), incluso si no menciona el nombre exacto del endpoint. NO cubre ejecutar las pruebas generadas con SoapUI TestRunner ni levantar el servicio con Docker — para eso no uses esta skill. --- -# Generar proyecto SoapUI vía API de openapi2soapui +# Generar proyecto SoapUI con openapi2soapui -Esta skill cubre **solo** cómo llamar el endpoint de este repo que genera un proyecto SoapUI a partir de un spec OpenAPI, y qué configuración acepta. No cubre ejecutar el proyecto generado (SoapUI TestRunner) ni levantar el servicio (Docker/Maven) — si el usuario pide eso, es trabajo aparte. +Esta skill cubre **solo** cómo generar un proyecto SoapUI a partir de un spec OpenAPI con este repo, y qué configuración acepta. No cubre ejecutar el proyecto generado (SoapUI TestRunner) ni levantar el servicio (Docker/Maven) — si el usuario pide eso, es trabajo aparte. -## Paso 0 — obtener la URL base (obligatorio) +## Elegir la vía: CLI o API + +Hay dos front ends sobre el mismo motor. Generan **XML idéntico** con los mismos defaults y las mismas validaciones, así que la tabla de parámetros de más abajo aplica a ambos. + +| | CLI (`openapi2soapui-cli.jar`) | API HTTP | +|---|---|---| +| Requisitos | El jar y un JRE 21. Nada corriendo | Servicio levantado + URL base confirmada | +| Spec | Fichero plano JSON/YAML (`-f`) | Base64 dentro del JSON body | +| Salida | Fichero `.xml` en disco | Body de la respuesta | + +**Si el servicio no está levantado y confirmado, prefiere el CLI**: evita el paso 0, el base64 y el manejo de la respuesta HTTP. Usa la API cuando el usuario ya la tiene corriendo o pide explícitamente el endpoint. + +### Vía CLI + +Construir el jar (si no existe): `mvn clean package -DskipTests` → `openapi2soapui-cli/target/openapi2soapui-cli.jar`. + +```bash +# spec plano + flags, salida en ./output/{apiName}_{apiVersion}-soapui-project.xml +java -jar openapi2soapui-cli.jar -f archivo.yaml -n MiApi -o ./output + +# configuración completa: el MISMO JSON body que la API (openApiSpec en base64) +java -jar openapi2soapui-cli.jar -c request.json -o proyecto-soapui.xml + +# config para lo anidado + spec como fichero plano +java -jar openapi2soapui-cli.jar -c request.json -f archivo.yaml +``` + +Equivalencias con la tabla de parámetros: + +- `apiName` → `-n/--api-name` (si no se da, el CLI lo deriva del `info.title` del spec; la API sí lo exige). +- `openApiSpec` → `-f/--file` (texto plano, sin base64). +- `headers` → `-H/--header "clave:valor"`, repetible. `testCaseNames` → `--test-case-names a,b`. `serverPattern` → `--server-pattern`. `numberOfScopes` → `--number-of-scopes`. +- Flags booleanos: `--read-only`, `--minimal-endpoints`, `--microcks-headers`, `--generate-one-of-any-of`, `--schema-is-inline`, `--is-inline`, `--has-scopes`, `--application-token`. Los dos que vienen activados por defecto se apagan con `--no-validate-schema` y `--no-schema-pretty-print`. +- `oAuth2Profiles`, `customAuthorizationsFile` y `examples` son objetos anidados: **solo por `-c`**, con la misma forma documentada abajo. +- Exit codes: `0` ok, `1` error de generación/validación, `2` error de uso. Los errores salen por stderr con los mismos códigos de la tabla de diagnóstico (`-v` para la traza completa). + +Para la vía API, sigue con el paso 0. + +## Paso 0 — obtener la URL base (obligatorio, solo vía API) La URL base del servicio (host:puerto, ej. `http://localhost:8080`) **nunca se asume**. Si no está ya confirmada en la conversación actual, pregúntala al usuario antes de construir cualquier request. No uses `localhost:8080` por defecto sin que el usuario lo confirme — puede estar corriendo en otro puerto, en Docker con otro mapeo, o en un host remoto. @@ -165,7 +203,7 @@ Cada entrada define un request de bootstrap de autenticación (ej. un fetch de t ## Nota sobre el spec propio publicado -El propio `api.yaml` del servicio (`src/main/resources/static/api.yaml`) tiene un typo conocido en el discriminator `oneOf` de `OAuth2ProfileToGetToken`: el mapping entre `IMPLICIT` y `RESOURCE_OWNER_PASSWORD_CREDENTIALS` está invertido. No afecta la validación real (que corre en Java vía `AuthenticationConditionalValidator`), solo es ruido en esa documentación — no te confundas si lo comparás contra ese YAML. +El propio `api.yaml` del servicio (`openapi2soapui-rest/src/main/resources/static/api.yaml`) tiene un typo conocido en el discriminator `oneOf` de `OAuth2ProfileToGetToken`: el mapping entre `IMPLICIT` y `RESOURCE_OWNER_PASSWORD_CREDENTIALS` está invertido. No afecta la validación real (que corre en Java vía `AuthenticationConditionalValidator`), solo es ruido en esa documentación — no te confundas si lo comparás contra ese YAML. ## Fuera de alcance diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc55de..5f6b5b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ 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). +## [2.1.0] - 2026-08-26 + +### Fixed +- **OpenAPI/Swagger 2.0 specs can be read again.** Since the Java 21 / Spring Boot 3.5.16 upgrade in 2.0.0, every v2 spec failed (HTTP 500 from the endpoint) with `NoSuchMethodError`, even though v2 has always been advertised as supported. swagger-parser's v2 converter delegates to the Swagger 1.x stack, and SoapUI drags in an older copy of it (`swagger-inflector` 1.0.19 → `swagger-parser` 1.0.54, `swagger-core` 1.6.2) which Maven's nearest-wins resolution preferred; those releases are built against snakeyaml 1.x, removed in the snakeyaml 2.4 that Spring Boot manages. The parent pom now pins that stack to the versions `swagger-compat-spec-parser` 1.0.76 declares (`swagger-parser` 1.0.76, `swagger-core`/`swagger-models`/`swagger-annotations` 1.6.16), which target snakeyaml 2.4. A v2 spec and its v3 equivalent now generate byte for byte the same project; `Swagger2SpecTest` guards both facts. Generation from v3 specs is unaffected. + +### Added +- **Command line interface.** `openapi2soapui-cli.jar` generates a SoapUI project from an OpenAPI spec without starting the service: `java -jar openapi2soapui-cli.jar -f petstore.yaml -o ./output`. It reuses the same engine, the same request model and the same bean validation constraints as the HTTP endpoint, so both produce identical projects (verified against `demo/petstore-ok-only-run`). + - `-f`/`--file` takes the spec as plain JSON or YAML; `-c`/`--config` takes the very same JSON body the REST API accepts, `openApiSpec` base64 encoded included, so existing request files work unchanged. When both are given, `-f` provides the spec. + - Every scalar parameter has a flag (`--read-only`, `--minimal-endpoints`, `--microcks-headers`, `--generate-one-of-any-of`, `--schema-is-inline`, `--is-inline`, `--has-scopes`, `--application-token`, `--number-of-scopes`, `--server-pattern`, `--test-case-names`, `-H`/`--header`, `--no-validate-schema`, `--no-schema-pretty-print`) and overrides the config file. `oAuth2Profiles`, `customAuthorizationsFile` and `examples` are nested objects, reachable only through `-c`. + - Defaults are identical to the HTTP API. The only difference is `apiName`: required by the API, derived from the spec title (or the spec file name) by the CLI when neither `-n` nor the config provide one. + - Output defaults to `./output/{apiName}_{apiVersion}-soapui-project.xml`; an `-o` value ending in `.xml` is taken as the exact file. Exit codes: `0` success, `1` generation or validation error, `2` usage error. Errors and SoapUI logs go to stderr, leaving stdout with just the result line. + - SoapUI's bundled log4j2 configuration is replaced at runtime (via the `soapui.log4j.config` property), so a run no longer prints DEBUG on stdout nor writes `soapui.log`, `soapui-errors.log` and `global-groovy.log` under `${user.home}/.soapuios/logs`. + +### Changed +- **Split into a Maven multi module build**: `openapi2soapui-core` (conversion engine and request model, free of Spring and of any web dependency), `openapi2soapui-rest` (the HTTP service) and `openapi2soapui-cli`. Java packages are unchanged, so no import in the existing code was touched. + - The service artifact is still `openapi2soapui.war` (or `openapi2soapui.jar` with `-Pjar`), now under `openapi2soapui-rest/target/`. The `war`, `jar`, `INTE`, `TEST` and `PROD` profiles behave as before. **Its Maven coordinates change from `net.cloudappi:openapi2soapui` to `net.cloudappi:openapi2soapui-rest`**, and `docker-compose.yml` now points `JAR_FILE` at the new path. + - Running the service from the reactor now needs the module: `mvn -pl openapi2soapui-rest -am spring-boot:run`. + - `messages.properties` moved to the core module so the HTTP service and the CLI report the same validation messages and codes. + - `AuthenticationConditionalValidator` no longer uses Spring's `ObjectUtils.isEmpty`, keeping the core module free of Spring. Behaviour is unchanged, blank but non empty values are still not considered empty. + - The engine tests now run with a quiet SoapUI log4j2 configuration instead of flooding the build log with DEBUG. + ## [2.0.0] - 2026-08-25 ### Added diff --git a/README.md b/README.md index 9caeeeb..0ccf5eb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # 🛠️ OpenAPI2SoapUI ![Release](https://img.shields.io/badge/release-0.1.0-purple) ![Swagger](https://img.shields.io/badge/-soap-%23Clojure?style=flat&logo=swagger&logoColor=white) ![Java](https://img.shields.io/badge/java-%23ED8B00.svg?style=flat&logo=openjdk&logoColor=white) [![License: LGPL v3](https://img.shields.io/badge/license-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) -[API](./src/main/resources/static/api.yaml) to generate a SoapUI project from an OpenAPI Specification (fka Swagger Specification) +[API](./openapi2soapui-rest/src/main/resources/static/api.yaml) to generate a SoapUI project from an OpenAPI Specification (fka Swagger Specification) Given an OpenAPI Specification, either v2 or v3, a SoapUI project is generated with the _requests_ for each resource operation and a _test suite_. The response is the content of the SoapUI project in XML format to save as file and import into the SoapUI application. @@ -31,7 +31,7 @@ Feel free to drop by and greet us on our GitHub discussion or Discord chat. You # ⚙️ Functionalities -[Here](./src/main/resources/static/api.yaml) you can check the definition of the API Swagger to SoapUI +[Here](./openapi2soapui-rest/src/main/resources/static/api.yaml) you can check the definition of the API Swagger to SoapUI - Base64 Decoding of Open API Specification Content - Parse Open API Specification Content into swagger-core representation as Java POJO @@ -146,9 +146,12 @@ Alternatively you can use the [Spring Boot Maven plugin](https://docs.spring.io/ * To build and start the server type ```shell -$ mvn spring-boot:run +$ mvn -pl openapi2soapui-rest -am spring-boot:run ``` +The HTTP service lives in the `openapi2soapui-rest` module, hence the `-pl`; `-am` also builds the +`openapi2soapui-core` module it depends on. + * URL to access: **http://localhost:8080/api-openapi-to-soapui/v1/soap-ui-projects** ### Running the application in Docker @@ -184,65 +187,125 @@ $ mvn clean package $CATALINA_HOME/webapps/openapi2soapui-.war ``` +The war is produced at `openapi2soapui-rest/target/openapi2soapui.war`. + * Restart Tomcat Server * URL to access: **http://localhost:8080/openapi2soapui/api-openapi-to-soapui/v1/soap-ui-projects** + +## 🖥️ Command line interface (CLI) + +The same generation is available as a standalone command, with no server involved: an OpenAPI spec in JSON or +YAML goes in, the SoapUI project XML comes out. It reuses the exact same engine and the same request model as +the HTTP service, so both produce identical projects. + +* To build the CLI jar + +```shell +$ mvn clean package -DskipTests +``` + +* The jar is produced at `openapi2soapui-cli/target/openapi2soapui-cli.jar` + +```shell +# generate from a spec file into ./output +$ java -jar openapi2soapui-cli.jar -f petstore.yaml + +# name the API and pick the exact output file +$ java -jar openapi2soapui-cli.jar -f petstore.yaml -n Petstore -o ./petstore-project.xml + +# only GET and OPTIONS test cases, with a custom header +$ java -jar openapi2soapui-cli.jar -f petstore.yaml --read-only -H "X-Api-Key:secret" + +# full configuration: the very same JSON body the REST API takes, openApiSpec included as base64 +$ java -jar openapi2soapui-cli.jar -c request.json + +# config file for everything else, spec as a plain file +$ java -jar openapi2soapui-cli.jar -c request.json -f petstore.yaml + +$ java -jar openapi2soapui-cli.jar --help +``` + +Notes: + +* `-f` takes the spec as plain text; `-c` takes the REST API body, where `openApiSpec` is base64 encoded, so + an existing request such as [demo/petstore-ok-only-run/request.json](demo/petstore-ok-only-run/request.json) + works as is. When both are given, `-f` provides the spec. +* `oAuth2Profiles`, `customAuthorizationsFile` and `examples` are nested objects and are only reachable + through `-c`. Every other parameter has a flag, listed by `--help`. +* Defaults match the HTTP API exactly, including `validateSchema` and `schemaPrettyPrint` being enabled unless + turned off with `--no-validate-schema` / `--no-schema-pretty-print`. `apiName` is the only difference: the + API requires it, while the CLI derives it from the spec title when neither `-n` nor the config provide one. +* Output defaults to `./output/{apiName}_{apiVersion}-soapui-project.xml`. An `-o` value ending in `.xml` is + taken as the exact file, anything else as a folder. +* Exit codes: `0` success, `1` generation or validation error, `2` usage error. Errors go to stderr, so stdout + only ever carries the result line. Add `-v` for stack traces and SoapUI logs. + ## Files and Directories Structure The project directory has a particular directory structure. A representative project is shown below: ### Project Structure +The build is a Maven multi module project: the conversion engine is shared by the HTTP service and the CLI. + ```text . -├── src -│ └── main -│ └── java -│ ├── org.apiaddicts.apitools.openapi2soapui -│ │ -│ ├── org.apiaddicts.apitools.openapi2soapui.config -│ │ -│ ├── org.apiaddicts.apitools.openapi2soapui.constants -│ │ -│ ├── org.apiaddicts.apitools.openapi2soapui.controller -│ │ -│ ├── org.apiaddicts.apitools.openapi2soapui.error -│ ├── org.apiaddicts.apitools.openapi2soapui.error.exceptions -│ ├── org.apiaddicts.apitools.openapi2soapui.error.validators -│ │ -│ ├── org.apiaddicts.apitools.openapi2soapui.model -│ │ -│ ├── org.apiaddicts.apitools.openapi2soapui.request -│ │ -│ ├── org.apiaddicts.apitools.openapi2soapui.service -│ │ -│ └── org.apiaddicts.apitools.openapi2soapui.util -├── src -│ └── main +├── pom.xml parent: packaging pom, shared properties and profiles +├── openapi2soapui-core the conversion engine, no Spring and no web +│ └── src/main +│ ├── java/org.apiaddicts.apitools.openapi2soapui +│ │ ├── .constants +│ │ ├── .error.exceptions +│ │ ├── .error.validators +│ │ ├── .model SoapUIProject, the engine itself +│ │ ├── .request request model shared by both front ends +│ │ └── .util │ └── resources -│ ├── static -│ │ └── api.yaml -│ │ -│ ├── application.properties -│ ├── log4j.properties │ └── messages.properties -├── JRE System Library -├── Maven Dependencies -├── src -├── target -│ └──openapi2soapui-1.0.2 -├── .gitlab-ci.yaml +├── openapi2soapui-rest the HTTP service (war by default, jar with -Pjar) +│ └── src/main +│ ├── java/org.apiaddicts.apitools.openapi2soapui +│ │ ├── (Openapi2SoapUIApplication) +│ │ ├── .config +│ │ ├── .controller +│ │ ├── .error HTTP error payload and controller advice +│ │ ├── .service +│ │ └── .util +│ └── resources +│ ├── static/api.yaml +│ ├── application.properties +│ ├── banner.txt +│ └── log4j.properties +├── openapi2soapui-cli the standalone command line jar +│ └── src/main +│ ├── java/org.apiaddicts.apitools.openapi2soapui.cli +│ └── resources +│ ├── cli.properties version reported by the version flag +│ ├── logback.xml +│ └── soapui-cli-log4j.xml keeps SoapUI from flooding stdout +├── demo sample specs, requests and generated projects +├── Dockerfile +├── docker-compose.yml ├── lombok.config ├── mvnw ├── mvnw.cmd -├── pom.xml └── README.md ``` +### Modules + +* `openapi2soapui-core` - the conversion engine and the request model. Deliberately free of Spring and of any + web dependency, so the CLI can embed it without booting an application context; +* `openapi2soapui-rest` - the HTTP service. Produces `openapi2soapui.war` by default and + `openapi2soapui.jar` with `-Pjar`; +* `openapi2soapui-cli` - the command line front end. Produces `openapi2soapui-cli.jar`; + ### Packages * `config` - app configurations; * `constants` - app contants; * `controller` - listen to the client; +* `cli` - command line front end; * `error` - manage errors; * `exceptions` - custom exception handling; * `validators` - custom validations; @@ -258,7 +321,7 @@ The project directory has a particular directory structure. A representative pro * `resources/static/api.yaml` - contains Open API Specification. * `resources/application.properties` - contains application-wide properties. Spring reads the properties defined in this file to configure your application. You can define server’s default port, server’s context path, database URLs etc, in this file. * `resources/log4j.properties` - contains contains the entire runtime configuration used by log4j. This file will contain log4j appenders information, log level information and output file names for file appenders. -* `resources/messages.properties` - contains the error messages used in the application. +* `resources/messages.properties` - contains the error messages used in the application. It lives in the core module because both the HTTP service and the CLI report the same validation messages from it. * mvnw / mvnw.cmd - This allows you to run the Maven project without having Maven installed and present in the path. Download the correct version of Maven if it can't be found (as far as I know by default in your user home directory). The mvnw file is for Linux (bash) and mvnw.cmd is for the Windows environment. * `pom.xml` - contains all the project dependencies @@ -282,7 +345,7 @@ $CATALINA_HOME/webapps/openapi2soapui.war ## Documentation - [cURL Example](example.sh) -- [Open API Specification](./src/main/resources/static/api.yaml) +- [Open API Specification](./openapi2soapui-rest/src/main/resources/static/api.yaml) - [Swagger UI](http://localhost:8080/swagger-ui.html) - `http://localhost:8080/swagger-ui.html` - Find Java Doc in **javadoc** folder - Java Doc is generated in ./target/site/apidocs` folder using the Maven command diff --git a/docker-compose.yml b/docker-compose.yml index aa13fcc..8ab823b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,6 @@ services: build: context: . args: - JAR_FILE: "target/openapi2soapui.jar" + JAR_FILE: "openapi2soapui-rest/target/openapi2soapui.jar" ports: - "8080:8080" \ No newline at end of file diff --git a/openapi2soapui-cli/pom.xml b/openapi2soapui-cli/pom.xml new file mode 100644 index 0000000..3c6b05b --- /dev/null +++ b/openapi2soapui-cli/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + net.cloudappi + openapi2soapui + 2.1.0 + ../pom.xml + + + openapi2soapui-cli + jar + + openapi2soapui-cli + Openapi to SoapUI Project - command line interface + + + openapi2soapui-cli + + + + + ${basedir}/src/main/resources + true + + cli.properties + + + + ${basedir}/src/main/resources + false + + cli.properties + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + org.apiaddicts.apitools.openapi2soapui.cli.Openapi2SoapUICli + + + + + + + + + net.cloudappi + openapi2soapui-core + ${project.version} + + + + + ch.qos.logback + logback-classic + + + + + org.apache.tomcat.embed + tomcat-embed-el + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.vaadin.external.google + android-json + + + + org.apache.logging.log4j + log4j-to-slf4j + + + + + + + diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java new file mode 100644 index 0000000..f2f57db --- /dev/null +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java @@ -0,0 +1,281 @@ +package org.apiaddicts.apitools.openapi2soapui.cli; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apiaddicts.apitools.openapi2soapui.request.Header; +import org.apiaddicts.apitools.openapi2soapui.request.SoapUIProjectRequest; + +final class CliArgs { + + static final String DEFAULT_OUTPUT = "./output"; + + private static final String OUTPUT_FILE_SUFFIX = "-soapui-project.xml"; + + private static final Pattern UNSAFE_NAME_CHARS = Pattern.compile("[^A-Za-z0-9._-]+"); + + private String specFile; + private String configFile; + private String output; + private String apiName; + private String serverPattern; + private Set testCaseNames; + private final List
headers = new ArrayList<>(); + private Boolean readOnly; + private Boolean minimalEndpoints; + private Boolean microcksHeaders; + private Boolean generateOneOfAnyOf; + private Boolean validateSchema; + private Boolean schemaIsInline; + private Boolean schemaPrettyPrint; + private Boolean isInline; + private Boolean hasScopes; + private Boolean applicationToken; + private Integer numberOfScopes; + private boolean verbose; + private boolean help; + private boolean version; + + private CliArgs() { + } + + static CliArgs parse(String[] args) { + CliArgs parsed = new CliArgs(); + List tokens = normalize(args); + for (int i = 0; i < tokens.size(); i++) { + String option = tokens.get(i); + switch (option) { + case "-h", "--help" -> parsed.help = true; + case "-V", "--version" -> parsed.version = true; + case "-v", "--verbose" -> parsed.verbose = true; + case "-f", "--file" -> parsed.specFile = value(tokens, ++i, option); + case "-c", "--config" -> parsed.configFile = value(tokens, ++i, option); + case "-o", "--output" -> parsed.output = value(tokens, ++i, option); + case "-n", "--api-name" -> parsed.apiName = value(tokens, ++i, option); + case "-H", "--header" -> parsed.headers.add(header(value(tokens, ++i, option))); + case "--server-pattern" -> parsed.serverPattern = value(tokens, ++i, option); + case "--test-case-names" -> parsed.testCaseNames = testCaseNames(value(tokens, ++i, option)); + case "--number-of-scopes" -> parsed.numberOfScopes = integer(value(tokens, ++i, option), option); + case "--read-only" -> parsed.readOnly = Boolean.TRUE; + case "--minimal-endpoints" -> parsed.minimalEndpoints = Boolean.TRUE; + case "--microcks-headers" -> parsed.microcksHeaders = Boolean.TRUE; + case "--generate-one-of-any-of" -> parsed.generateOneOfAnyOf = Boolean.TRUE; + case "--schema-is-inline" -> parsed.schemaIsInline = Boolean.TRUE; + case "--is-inline" -> parsed.isInline = Boolean.TRUE; + case "--has-scopes" -> parsed.hasScopes = Boolean.TRUE; + case "--application-token" -> parsed.applicationToken = Boolean.TRUE; + case "--no-validate-schema" -> parsed.validateSchema = Boolean.FALSE; + case "--no-schema-pretty-print" -> parsed.schemaPrettyPrint = Boolean.FALSE; + default -> throw new UsageException("unknown option: " + option); + } + } + return parsed; + } + + private static List normalize(String[] args) { + List tokens = new ArrayList<>(args.length); + for (String arg : args) { + int equals = arg.startsWith("--") ? arg.indexOf('=') : -1; + if (equals > 0) { + tokens.add(arg.substring(0, equals)); + tokens.add(arg.substring(equals + 1)); + } else { + tokens.add(arg); + } + } + return tokens; + } + + private static String value(List tokens, int index, String option) { + if (index >= tokens.size()) throw new UsageException("option " + option + " requires a value"); + String value = tokens.get(index); + if (value.length() > 1 && value.startsWith("-")) { + throw new UsageException("option " + option + " requires a value, found " + value); + } + return value; + } + + private static Header header(String value) { + int separator = value.indexOf(':'); + if (separator < 1 || separator == value.length() - 1) { + throw new UsageException("header must be given as key:value, found " + value); + } + Header header = new Header(); + header.setKey(value.substring(0, separator).trim()); + header.setValue(value.substring(separator + 1).trim()); + return header; + } + + private static Set testCaseNames(String value) { + Set names = new LinkedHashSet<>(); + Arrays.stream(value.split(",")).map(String::trim).filter(name -> !name.isEmpty()).forEach(names::add); + if (names.isEmpty()) throw new UsageException("option --test-case-names requires at least one name"); + return names; + } + + private static Integer integer(String value, String option) { + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + throw new UsageException("option " + option + " requires a number, found " + value); + } + } + + SoapUIProjectRequest toRequest() throws IOException { + SoapUIProjectRequest request = (configFile != null) ? readConfig(configFile) : new SoapUIProjectRequest(); + + if (specFile != null) request.setOpenAPIContent(readSpec(specFile)); + + if (apiName != null) request.setApiName(apiName); + if (serverPattern != null) request.setServerPattern(serverPattern); + if (testCaseNames != null) request.setTestCaseNames(testCaseNames); + if (!headers.isEmpty()) request.setHeaders(mergeHeaders(request.getHeaders())); + if (readOnly != null) request.setReadOnly(readOnly); + if (minimalEndpoints != null) request.setMinimalEndpoints(minimalEndpoints); + if (microcksHeaders != null) request.setMicrocksHeaders(microcksHeaders); + if (generateOneOfAnyOf != null) request.setGenerateOneOfAnyOf(generateOneOfAnyOf); + if (validateSchema != null) request.setValidateSchema(validateSchema); + if (schemaIsInline != null) request.setSchemaIsInline(schemaIsInline); + if (schemaPrettyPrint != null) request.setSchemaPrettyPrint(schemaPrettyPrint); + if (isInline != null) request.setIsInline(isInline); + if (hasScopes != null) request.setHasScopes(hasScopes); + if (applicationToken != null) request.setApplicationToken(applicationToken); + if (numberOfScopes != null) request.setNumberOfScopes(numberOfScopes); + + return request; + } + + private static SoapUIProjectRequest readConfig(String path) throws IOException { + ObjectMapper mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + return mapper.readValue(requireFile(path, "config"), SoapUIProjectRequest.class); + } + + private static String readSpec(String path) throws IOException { + return Files.readString(requireFile(path, "OpenAPI").toPath()); + } + + private static File requireFile(String path, String description) { + File file = new File(path); + if (!file.isFile()) throw new IllegalArgumentException(description + " file not found: " + path); + return file; + } + + private List
mergeHeaders(List
configured) { + List
merged = new ArrayList<>(); + if (configured != null) merged.addAll(configured); + merged.addAll(headers); + return merged; + } + + Path resolveOutput(String apiName, String apiVersion) { + String target = (output != null) ? output : DEFAULT_OUTPUT; + if (target.toLowerCase().endsWith(".xml")) return Path.of(target); + return Path.of(target).resolve(sanitize(apiName) + "_" + sanitize(apiVersion) + OUTPUT_FILE_SUFFIX); + } + + private static String sanitize(String value) { + if (value == null || value.isBlank()) return "project"; + String safe = UNSAFE_NAME_CHARS.matcher(value).replaceAll("-").replaceAll("^-+|-+$", ""); + return safe.isEmpty() ? "project" : safe; + } + + String defaultApiName(String title) { + String fromTitle = UNSAFE_NAME_CHARS.matcher(title == null ? "" : title).replaceAll(""); + if (!fromTitle.isEmpty()) return fromTitle; + if (specFile != null) { + String fileName = Path.of(specFile).getFileName().toString().replaceFirst("\\.[^.]+$", ""); + String fromFile = UNSAFE_NAME_CHARS.matcher(fileName).replaceAll(""); + if (!fromFile.isEmpty()) return fromFile; + } + return "api"; + } + + String getSpecFile() { + return specFile; + } + + String getConfigFile() { + return configFile; + } + + boolean isVerbose() { + return verbose; + } + + boolean isHelp() { + return help; + } + + boolean isVersion() { + return version; + } + + static String usage() { + return """ + openapi2soapui - generates a SoapUI project (XML) from an OpenAPI specification + + Usage: java -jar openapi2soapui-cli.jar [options] + + Input (at least one of -f, -c is required): + -f, --file OpenAPI spec, JSON or YAML, as plain text (not base64) + -c, --config JSON file with the same body as the REST API, where + openApiSpec is base64 encoded. It is the only way to pass + oAuth2Profiles, customAuthorizationsFile and examples. + When -f is also given, -f provides the spec. + + Output: + -o, --output Folder, or a path ending in .xml for an exact file name + (default: ./output) + + Generation options, they override the config file: + -n, --api-name apiName (default: the spec title, or the spec file name) + -H, --header Request header, repeatable + --server-pattern Pick the spec server whose URL contains this text + --test-case-names Extra test cases, comma separated + --number-of-scopes numberOfScopes, only relevant with --has-scopes + --read-only Generate only GET and OPTIONS test cases + --minimal-endpoints Collapse the ErrorRequired test cases into one + --microcks-headers Add the X-Microcks-Response-Name header to every request + --generate-one-of-any-of Resolve oneOf/anyOf using their first candidate + --schema-is-inline Embed the response schema instead of using a project property + --is-inline Embed body example values instead of project properties + --has-scopes One extra test case per oAuth2Profiles entry + --application-token Extra test case per CLIENT_CREDENTIALS profile + --no-validate-schema Do not add the schema assertion (on by default) + --no-schema-pretty-print Serialize the schema compactly (pretty by default) + + Other: + -v, --verbose Print stack traces and SoapUI logs on stderr + -h, --help Show this help + -V, --version Show the version + + Exit codes: 0 success, 1 generation or validation error, 2 usage error + + Examples: + java -jar openapi2soapui-cli.jar -f petstore.yaml -o ./output + java -jar openapi2soapui-cli.jar -f petstore.yaml -n Petstore --read-only + java -jar openapi2soapui-cli.jar -c request.json -o petstore-project.xml + java -jar openapi2soapui-cli.jar -c request.json -f petstore.yaml + """; + } + + static final class UsageException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + UsageException(String message) { + super(message); + } + } +} diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliValidator.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliValidator.java new file mode 100644 index 0000000..be51f9d --- /dev/null +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliValidator.java @@ -0,0 +1,47 @@ +package org.apiaddicts.apitools.openapi2soapui.cli; + +import java.util.List; +import java.util.Map; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.ValidatorFactory; + +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator; +import org.hibernate.validator.spi.resourceloading.ResourceBundleLocator; +import org.hibernate.validator.resourceloading.PlatformResourceBundleLocator; + +import org.apiaddicts.apitools.openapi2soapui.request.SoapUIProjectRequest; + +final class CliValidator { + + private static final String MESSAGE_BUNDLE = "messages"; + + private static final Map JSON_NAMES = Map.of("openAPIContent", "openApiSpec"); + + private CliValidator() { + } + + static List validate(SoapUIProjectRequest request) { + ResourceBundleLocator bundleLocator = new PlatformResourceBundleLocator(MESSAGE_BUNDLE); + try (ValidatorFactory factory = Validation.byProvider(HibernateValidator.class) + .configure() + .messageInterpolator(new ResourceBundleMessageInterpolator(bundleLocator)) + .buildValidatorFactory()) { + return factory.getValidator().validate(request).stream() + .map(CliValidator::describe) + .sorted() + .toList(); + } + } + + private static String describe(ConstraintViolation violation) { + String property = violation.getPropertyPath().toString(); + property = JSON_NAMES.getOrDefault(property, property); + String message = violation.getMessage(); + String[] parts = message.split("\\|", 2); + String text = (parts.length > 1) ? "[" + parts[0] + "] " + parts[1] : message; + return property.isEmpty() ? text : property + ": " + text; + } +} diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java new file mode 100644 index 0000000..057a684 --- /dev/null +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java @@ -0,0 +1,140 @@ +package org.apiaddicts.apitools.openapi2soapui.cli; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Properties; + +import io.swagger.v3.oas.models.OpenAPI; + +import org.apiaddicts.apitools.openapi2soapui.model.SoapUIProject; +import org.apiaddicts.apitools.openapi2soapui.request.SoapUIProjectRequest; +import org.apiaddicts.apitools.openapi2soapui.util.SerializedDataUtils; + +public final class Openapi2SoapUICli { + + static final int EXIT_OK = 0; + + static final int EXIT_ERROR = 1; + + static final int EXIT_USAGE = 2; + + private static final String VERSION_RESOURCE = "/cli.properties"; + + private static final String UNKNOWN_VERSION = "unknown"; + + public static void main(String[] args) { + System.exit(new Openapi2SoapUICli().run(args)); + } + + public int run(String[] args) { + CliArgs cli; + try { + cli = CliArgs.parse(args); + } catch (CliArgs.UsageException e) { + return usageError(e.getMessage()); + } + + if (cli.isHelp()) { + System.out.println(CliArgs.usage()); + return EXIT_OK; + } + if (cli.isVersion()) { + System.out.println("openapi2soapui " + version()); + return EXIT_OK; + } + if (cli.getSpecFile() == null && cli.getConfigFile() == null) { + return usageError("no input given, pass -f and/or -c "); + } + + SoapUILogging.install(cli.isVerbose()); + + try { + return generate(cli); + } catch (Throwable t) { + if (cli.isVerbose()) t.printStackTrace(); + return error(describe(t)); + } + } + + private int generate(CliArgs cli) throws Exception { + SoapUIProjectRequest request = cli.toRequest(); + + String spec = request.getOpenAPIContent(); + if (spec == null || spec.isBlank()) { + return error("no OpenAPI spec given, pass -f or set openApiSpec in the config file"); + } + + OpenAPI openAPI = SerializedDataUtils.parseOpenAPIContent(spec); + if (openAPI.getInfo() == null || openAPI.getInfo().getVersion() == null) { + return error("Version not found in OpenAPI"); + } + + if (request.getApiName() == null || request.getApiName().isBlank()) { + request.setApiName(cli.defaultApiName(openAPI.getInfo().getTitle())); + } + + List violations = CliValidator.validate(request); + if (!violations.isEmpty()) { + System.err.println("error: invalid request"); + violations.forEach(violation -> System.err.println(" " + violation)); + return EXIT_ERROR; + } + + SoapUIProject project = SoapUILogging.withoutStdout(!cli.isVerbose(), + () -> new SoapUIProject(request.getApiName(), openAPI, request.getOAuth2Profiles(), + request.getHeaders(), request.getTestCaseNames(), request.getReadOnly(), + request.getServerPattern(), request.getMinimalEndpoints(), request.getMicrocksHeaders(), + request.getGenerateOneOfAnyOf(), request.getValidateSchema(), request.getSchemaIsInline(), + request.getIsInline(), request.getSchemaPrettyPrint(), request.getHasScopes(), + request.getApplicationToken(), request.getNumberOfScopes(), request.getExamples(), + request.getCustomAuthorizationsFile())); + try { + Path target = write(project.getFileContent(), + cli.resolveOutput(request.getApiName(), openAPI.getInfo().getVersion())); + System.out.println("SoapUI project generated successfully in " + target); + return EXIT_OK; + } finally { + project.deleteTemporaryFile(); + } + } + + private static Path write(String xml, Path target) throws IOException { + Path absolute = target.toAbsolutePath().normalize(); + Path parent = absolute.getParent(); + if (parent != null) Files.createDirectories(parent); + Files.writeString(absolute, xml); + return absolute; + } + + private static int usageError(String message) { + System.err.println("error: " + message); + System.err.println(); + System.err.println(CliArgs.usage()); + return EXIT_USAGE; + } + + private static int error(String message) { + System.err.println("error: " + message); + return EXIT_ERROR; + } + + private static String describe(Throwable failure) { + String message = failure.getMessage(); + if (message == null || message.isBlank()) return failure.getClass().getSimpleName(); + return (failure instanceof Error) ? failure.getClass().getSimpleName() + ": " + message : message; + } + + private static String version() { + try (InputStream stream = Openapi2SoapUICli.class.getResourceAsStream(VERSION_RESOURCE)) { + if (stream == null) return UNKNOWN_VERSION; + Properties properties = new Properties(); + properties.load(stream); + return properties.getProperty("version", UNKNOWN_VERSION); + } catch (IOException e) { + return UNKNOWN_VERSION; + } + } +} diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java new file mode 100644 index 0000000..0ecdcfa --- /dev/null +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java @@ -0,0 +1,53 @@ +package org.apiaddicts.apitools.openapi2soapui.cli; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.Callable; + +final class SoapUILogging { + + private static final String LOG4J_CONFIG_PROPERTY = "soapui.log4j.config"; + + static final String LOG_LEVEL_PROPERTY = "openapi2soapui.cli.logLevel"; + + static final String PARSER_LOG_LEVEL_PROPERTY = "openapi2soapui.cli.parserLogLevel"; + + private static final String CONFIG_RESOURCE = "/soapui-cli-log4j.xml"; + + private SoapUILogging() { + } + + static void install(boolean verbose) { + System.setProperty(LOG_LEVEL_PROPERTY, verbose ? "DEBUG" : "WARN"); + System.setProperty(PARSER_LOG_LEVEL_PROPERTY, verbose ? "DEBUG" : "ERROR"); + + if (System.getProperty(LOG4J_CONFIG_PROPERTY) != null) return; + + try (InputStream config = SoapUILogging.class.getResourceAsStream(CONFIG_RESOURCE)) { + if (config == null) return; + Path target = Files.createTempFile("soapui-cli-log4j", ".xml"); + target.toFile().deleteOnExit(); + Files.copy(config, target, StandardCopyOption.REPLACE_EXISTING); + System.setProperty(LOG4J_CONFIG_PROPERTY, target.toAbsolutePath().toString()); + } catch (IOException e) { + System.err.println("warning: could not install the SoapUI logging configuration: " + e.getMessage()); + } + } + + static T withoutStdout(boolean mute, Callable action) throws Exception { + if (!mute) return action.call(); + PrintStream original = System.out; + System.setOut(new PrintStream(OutputStream.nullOutputStream(), true, StandardCharsets.UTF_8)); + try { + return action.call(); + } finally { + System.setOut(original); + } + } +} diff --git a/openapi2soapui-cli/src/main/resources/cli.properties b/openapi2soapui-cli/src/main/resources/cli.properties new file mode 100644 index 0000000..a9ca5c5 --- /dev/null +++ b/openapi2soapui-cli/src/main/resources/cli.properties @@ -0,0 +1 @@ +version=@project.version@ diff --git a/openapi2soapui-cli/src/main/resources/logback.xml b/openapi2soapui-cli/src/main/resources/logback.xml new file mode 100644 index 0000000..9afe4c0 --- /dev/null +++ b/openapi2soapui-cli/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + System.err + + %-5level [%logger{0}] %msg%n + + + + + + + + + diff --git a/openapi2soapui-cli/src/main/resources/soapui-cli-log4j.xml b/openapi2soapui-cli/src/main/resources/soapui-cli-log4j.xml new file mode 100644 index 0000000..b6935e6 --- /dev/null +++ b/openapi2soapui-cli/src/main/resources/soapui-cli-log4j.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java b/openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java new file mode 100644 index 0000000..7f6a9d1 --- /dev/null +++ b/openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java @@ -0,0 +1,140 @@ +package org.apiaddicts.apitools.openapi2soapui.cli; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class Openapi2SoapUICliTest { + + private static final String SPEC = "petstore.yaml"; + private static final String CONFIG = "request.json"; + + @TempDir + Path outputDir; + + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + private final ByteArrayOutputStream err = new ByteArrayOutputStream(); + private PrintStream originalOut; + private PrintStream originalErr; + + @BeforeEach + void captureConsole() { + originalOut = System.out; + originalErr = System.err; + System.setOut(new PrintStream(out, true, StandardCharsets.UTF_8)); + System.setErr(new PrintStream(err, true, StandardCharsets.UTF_8)); + } + + @AfterEach + void restoreConsole() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Test + void generatesProjectFromSpecFile() throws Exception { + int exitCode = run("-f", resource(SPEC), "-n", "Petstore", "-o", outputDir.toString()); + + assertEquals(Openapi2SoapUICli.EXIT_OK, exitCode, stderr()); + Path project = outputDir.resolve("Petstore_1.0.0-soapui-project.xml"); + assertTrue(Files.exists(project), "expected the project at " + project); + String xml = Files.readString(project); + assertTrue(xml.contains("name=\"Petstore_1.0.0\""), "project name missing from " + project); + assertTrue(xml.contains("/pet/findByStatus_Petstore_1.0.0-GET-Suite"), "test suite missing"); + assertTrue(xml.contains("GET_CaseOkAllProperties"), "test case missing"); + assertEquals(1, stdout().lines().count(), "unexpected stdout: " + stdout()); + } + + @Test + void generatesProjectFromConfigFileWithBase64Spec() throws Exception { + int exitCode = run("-c", resource(CONFIG), "-o", outputDir.resolve("project.xml").toString()); + + assertEquals(Openapi2SoapUICli.EXIT_OK, exitCode, stderr()); + String xml = Files.readString(outputDir.resolve("project.xml")); + assertTrue(xml.contains("name=\"Petstore_1.0.0\""), "project name missing"); + } + + @Test + void derivesApiNameFromSpecTitleWhenNotGiven() throws Exception { + int exitCode = run("-f", resource(SPEC), "-o", outputDir.toString()); + + assertEquals(Openapi2SoapUICli.EXIT_OK, exitCode, stderr()); + assertTrue(Files.exists(outputDir.resolve("SwaggerPetstore_1.0.0-soapui-project.xml")), + "expected the derived name, found " + Arrays.toString(outputDir.toFile().list())); + } + + @Test + void appliesFlagsOverConfigFile() throws Exception { + int exitCode = run("-f", resource(SPEC), "-n", "Flagged", "--read-only", "--no-validate-schema", + "-H", "X-Api-Key:secret", "-o", outputDir.toString()); + + assertEquals(Openapi2SoapUICli.EXIT_OK, exitCode, stderr()); + String xml = Files.readString(outputDir.resolve("Flagged_1.0.0-soapui-project.xml")); + assertTrue(xml.contains("X-Api-Key"), "the header passed with -H is missing"); + assertTrue(xml.contains("Valid HTTP Status Codes"), "the status code assertion is always expected"); + assertFalse(xml.contains("GroovyScriptAssertion"), "no-validate-schema should drop the schema assertion"); + } + + @Test + void missingSpecFileIsAnError() { + int exitCode = run("-f", outputDir.resolve("nope.yaml").toString(), "-o", outputDir.toString()); + + assertEquals(Openapi2SoapUICli.EXIT_ERROR, exitCode); + assertTrue(stderr().contains("OpenAPI file not found"), "unexpected stderr: " + stderr()); + } + + @Test + void unknownOptionIsAUsageError() { + int exitCode = run("--nope"); + + assertEquals(Openapi2SoapUICli.EXIT_USAGE, exitCode); + assertTrue(stderr().contains("unknown option: --nope"), "unexpected stderr: " + stderr()); + assertTrue(stderr().contains("Usage:"), "the usage text should follow the error"); + } + + @Test + void noInputIsAUsageError() { + int exitCode = run("-o", outputDir.toString()); + + assertEquals(Openapi2SoapUICli.EXIT_USAGE, exitCode); + assertTrue(stderr().contains("no input given"), "unexpected stderr: " + stderr()); + } + + @Test + void helpAndVersionSucceed() { + assertEquals(Openapi2SoapUICli.EXIT_OK, run("--help")); + assertTrue(stdout().contains("Usage:")); + + out.reset(); + assertEquals(Openapi2SoapUICli.EXIT_OK, run("--version")); + assertTrue(stdout().startsWith("openapi2soapui "), "unexpected version output: " + stdout()); + } + + private int run(String... args) { + return new Openapi2SoapUICli().run(args); + } + + private String resource(String name) throws Exception { + return Path.of(getClass().getClassLoader().getResource(name).toURI()).toString(); + } + + private String stdout() { + return out.toString(StandardCharsets.UTF_8); + } + + private String stderr() { + return err.toString(StandardCharsets.UTF_8); + } +} diff --git a/openapi2soapui-cli/src/test/resources/petstore.yaml b/openapi2soapui-cli/src/test/resources/petstore.yaml new file mode 100644 index 0000000..5b1d51e --- /dev/null +++ b/openapi2soapui-cli/src/test/resources/petstore.yaml @@ -0,0 +1,79 @@ +openapi: 3.0.0 +info: + title: Swagger Petstore + description: Simplified Petstore API with only GET /pet/findByStatus, tuned so generated test cases only assert the 200 case + version: 1.0.0 +servers: + - url: https://petstore.swagger.io/v2 +paths: + /pet/findByStatus: + get: + summary: Finds Pets by status + operationId: findPetsByStatus + tags: + - pet + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: false + schema: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' +components: + schemas: + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + Pet: + type: object + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + items: + type: string + tags: + type: array + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold diff --git a/openapi2soapui-cli/src/test/resources/request.json b/openapi2soapui-cli/src/test/resources/request.json new file mode 100644 index 0000000..9d41cf2 --- /dev/null +++ b/openapi2soapui-cli/src/test/resources/request.json @@ -0,0 +1,5 @@ +{ + "apiName": "Petstore", + "openApiSpec": "b3BlbmFwaTogMy4wLjAKaW5mbzoKICB0aXRsZTogU3dhZ2dlciBQZXRzdG9yZQogIGRlc2NyaXB0aW9uOiBTaW1wbGlmaWVkIFBldHN0b3JlIEFQSSB3aXRoIG9ubHkgR0VUIC9wZXQvZmluZEJ5U3RhdHVzLCB0dW5lZCBzbyBnZW5lcmF0ZWQgdGVzdCBjYXNlcyBvbmx5IGFzc2VydCB0aGUgMjAwIGNhc2UKICB2ZXJzaW9uOiAxLjAuMApzZXJ2ZXJzOgogIC0gdXJsOiBodHRwczovL3BldHN0b3JlLnN3YWdnZXIuaW8vdjIKcGF0aHM6CiAgL3BldC9maW5kQnlTdGF0dXM6CiAgICBnZXQ6CiAgICAgIHN1bW1hcnk6IEZpbmRzIFBldHMgYnkgc3RhdHVzCiAgICAgIG9wZXJhdGlvbklkOiBmaW5kUGV0c0J5U3RhdHVzCiAgICAgIHRhZ3M6CiAgICAgICAgLSBwZXQKICAgICAgcGFyYW1ldGVyczoKICAgICAgICAtIG5hbWU6IHN0YXR1cwogICAgICAgICAgaW46IHF1ZXJ5CiAgICAgICAgICBkZXNjcmlwdGlvbjogU3RhdHVzIHZhbHVlcyB0aGF0IG5lZWQgdG8gYmUgY29uc2lkZXJlZCBmb3IgZmlsdGVyCiAgICAgICAgICByZXF1aXJlZDogZmFsc2UKICAgICAgICAgIHNjaGVtYToKICAgICAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgICAgICAgIGVudW06CiAgICAgICAgICAgICAgLSBhdmFpbGFibGUKICAgICAgICAgICAgICAtIHBlbmRpbmcKICAgICAgICAgICAgICAtIHNvbGQKICAgICAgICAgICAgZGVmYXVsdDogYXZhaWxhYmxlCiAgICAgIHJlc3BvbnNlczoKICAgICAgICAnMjAwJzoKICAgICAgICAgIGRlc2NyaXB0aW9uOiBzdWNjZXNzZnVsIG9wZXJhdGlvbgogICAgICAgICAgY29udGVudDoKICAgICAgICAgICAgYXBwbGljYXRpb24vanNvbjoKICAgICAgICAgICAgICBzY2hlbWE6CiAgICAgICAgICAgICAgICB0eXBlOiBhcnJheQogICAgICAgICAgICAgICAgaXRlbXM6CiAgICAgICAgICAgICAgICAgICRyZWY6ICcjL2NvbXBvbmVudHMvc2NoZW1hcy9QZXQnCmNvbXBvbmVudHM6CiAgc2NoZW1hczoKICAgIENhdGVnb3J5OgogICAgICB0eXBlOiBvYmplY3QKICAgICAgcHJvcGVydGllczoKICAgICAgICBpZDoKICAgICAgICAgIHR5cGU6IGludGVnZXIKICAgICAgICAgIGZvcm1hdDogaW50NjQKICAgICAgICBuYW1lOgogICAgICAgICAgdHlwZTogc3RyaW5nCiAgICBUYWc6CiAgICAgIHR5cGU6IG9iamVjdAogICAgICBwcm9wZXJ0aWVzOgogICAgICAgIGlkOgogICAgICAgICAgdHlwZTogaW50ZWdlcgogICAgICAgICAgZm9ybWF0OiBpbnQ2NAogICAgICAgIG5hbWU6CiAgICAgICAgICB0eXBlOiBzdHJpbmcKICAgIFBldDoKICAgICAgdHlwZTogb2JqZWN0CiAgICAgIHByb3BlcnRpZXM6CiAgICAgICAgaWQ6CiAgICAgICAgICB0eXBlOiBpbnRlZ2VyCiAgICAgICAgICBmb3JtYXQ6IGludDY0CiAgICAgICAgY2F0ZWdvcnk6CiAgICAgICAgICAkcmVmOiAnIy9jb21wb25lbnRzL3NjaGVtYXMvQ2F0ZWdvcnknCiAgICAgICAgbmFtZToKICAgICAgICAgIHR5cGU6IHN0cmluZwogICAgICAgICAgZXhhbXBsZTogZG9nZ2llCiAgICAgICAgcGhvdG9VcmxzOgogICAgICAgICAgdHlwZTogYXJyYXkKICAgICAgICAgIGl0ZW1zOgogICAgICAgICAgICB0eXBlOiBzdHJpbmcKICAgICAgICB0YWdzOgogICAgICAgICAgdHlwZTogYXJyYXkKICAgICAgICAgIGl0ZW1zOgogICAgICAgICAgICAkcmVmOiAnIy9jb21wb25lbnRzL3NjaGVtYXMvVGFnJwogICAgICAgIHN0YXR1czoKICAgICAgICAgIHR5cGU6IHN0cmluZwogICAgICAgICAgZGVzY3JpcHRpb246IHBldCBzdGF0dXMgaW4gdGhlIHN0b3JlCiAgICAgICAgICBlbnVtOgogICAgICAgICAgICAtIGF2YWlsYWJsZQogICAgICAgICAgICAtIHBlbmRpbmcKICAgICAgICAgICAgLSBzb2xkCg==", + "headers": [] +} diff --git a/openapi2soapui-core/pom.xml b/openapi2soapui-core/pom.xml new file mode 100644 index 0000000..ff31198 --- /dev/null +++ b/openapi2soapui-core/pom.xml @@ -0,0 +1,342 @@ + + + 4.0.0 + + net.cloudappi + openapi2soapui + 2.1.0 + ../pom.xml + + + openapi2soapui-core + jar + + openapi2soapui-core + OpenAPI to SoapUI project conversion engine + + + + + com.smartbear.soapui + soapui + ${soapui.version} + + + javax.xml.bind + jsr173_api + + + com.smartbear.utils.analytics + analytics-core + + + com.smartbear.utils.analytics + out-app-analytics-provider + + + com.graphql-java + graphql-java + + + commons-collections + commons-collections + + + commons-collections + commons-lang + + + commons-collections + commons-io + + + org.slf4j + slf4j-log4j12 + + + com.jgoodies + forms + + + com.jgoodies + looks + + + com.jgoodies + binding + + + org.openjfx + javafx-base + + + org.openjfx + javafx-controls + + + org.openjfx + javafx-graphics + + + org.openjfx + javafx-media + + + org.openjfx + javafx-web + + + org.openjfx + javafx-swing + + + jetty + jetty + + + jetty + jetty-util + + + jetty + servlet-api + + + org.apache.ws.security + wss4j + + + net.sourceforge.htmlunit + htmlunit + + + org.w3c.css + sac + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpmime + + + org.apache.httpcomponents + httpclient-cache + + + org.apache.httpcomponents + httpcore + + + org.apache.httpcomponents + httpcore-nio + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.client + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.httpclient4 + + + com.google.oauth-client + google-oauth-client + + + org.apache.maven + maven-plugin-api + + + javax.activation + activation + + + javax.mail + mail + + + com.narupley + not-going-to-be-commons-ssl + + + swingx + swingx + + + rhino + js + + + bouncycastle + bcprov + + + jtidy + jtidy + + + hermesjms + hermes + + + net.sourceforge.cssparser + cssparser + + + net.sourceforge.nekohtml + nekohtml + + + org.samba.jcifs + jcifs + + + l2fprod + l2fprod-common-directorychooser + + + l2fprod + l2fprod-common-fontchooser + + + org.apache.ws.commons.util + ws-commons-util + + + commons-httpclient + commons-httpclient + + + org.sonatype.install4j + i4jruntime + + + org.codehaus.mojo + animal-sniffer-annotations + + + ezmorph + ezmorph + + + xom + xom + + + commons-codec + commons-codec + + + saxon + saxon + + + saxon + saxon-dom + + + xmlunit + xmlunit + + + javax.jms + jms + + + thoughtworks + xstream + + + org.apache.santuario + xmlsec + + + org.apache.xerces + xml-apis + + + + + + io.swagger.parser.v3 + swagger-parser + ${swagger-parser.version} + + + + org.yaml + snakeyaml + + + + + org.json + json + ${json.version} + + + + com.fasterxml.jackson.core + jackson-databind + + + + jakarta.validation + jakarta.validation-api + + + + org.hibernate.validator + hibernate-validator + + + + org.slf4j + slf4j-api + + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.vaadin.external.google + android-json + + + org.apache.logging.log4j + log4j-to-slf4j + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${project.basedir}/src/test/resources/soapui-test-log4j.xml + + + + + + + diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java similarity index 97% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java index e81ed7d..d44ac61 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/constants/Constants.java @@ -1,28 +1,28 @@ package org.apiaddicts.apitools.openapi2soapui.constants; public class Constants { - + private Constants() { // Intentional blank } - + public static final String SOAP_UI_PROJECT_FILE_NAME = "soapui-project"; public static final String SOAP_UI_PROJECT_FILE_EXTENSION = ".xml"; - + public static final String STEP_SUFFIX = "TestStep"; public static final String AUTHORIZATIONS_TEST_SUITE_NAME = "authorizations"; - + public static final String EJECUTION_TEST_STEP = "Execution"; public static final String HEADER = "header"; public static final String QUERY = "query"; public static final String PATH = "path"; - + public static final String DEFAULT = "default"; public static final String JSON = "json"; - + public static final String DEFAULT_REQUEST_NAME = "Request 1"; public static final String HEADERS_KEY = "headers"; @@ -41,7 +41,6 @@ private Constants() { public static final String SELECT_QUERY_PARAM = "$select"; public static final String EXCLUDE_QUERY_PARAM = "$exclude"; - // Test Suite/Test Case naming convention public static final String SERVICE_API_SUITE_SUFFIX = "Suite"; public static final String SERVICE_API_CASE_OK_ALL_PROPERTIES = "OkAllProperties"; public static final String SERVICE_API_CASE_OK_REQUIRED_PROPERTIES = "OkRequiredProperties"; diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/APIVersionNotFoundException.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/APIVersionNotFoundException.java similarity index 94% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/APIVersionNotFoundException.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/APIVersionNotFoundException.java index bf1c12c..dffeac4 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/APIVersionNotFoundException.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/APIVersionNotFoundException.java @@ -2,13 +2,10 @@ public class APIVersionNotFoundException extends RuntimeException { - /** - * - */ private static final long serialVersionUID = 6439384446777840383L; public APIVersionNotFoundException(String message) { super(message); } - + } diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/CreateEnumInstanceException.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/CreateEnumInstanceException.java similarity index 97% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/CreateEnumInstanceException.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/CreateEnumInstanceException.java index e413cf0..e1aafd4 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/CreateEnumInstanceException.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/CreateEnumInstanceException.java @@ -4,13 +4,11 @@ @Getter public class CreateEnumInstanceException extends RuntimeException { - /** - * - */ + private static final long serialVersionUID = -1954631250809380389L; - + public enum ErrorType {NOT_NULL, NOT_BLANK, INVALID} - + private final ErrorType errorType; private final String param; private final String value; diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/DecodeBase64Exception.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/DecodeBase64Exception.java similarity index 95% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/DecodeBase64Exception.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/DecodeBase64Exception.java index 6c3d77a..957e57c 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/DecodeBase64Exception.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/DecodeBase64Exception.java @@ -2,9 +2,6 @@ public class DecodeBase64Exception extends RuntimeException { - /** - * - */ private static final long serialVersionUID = 4099336966617245334L; public DecodeBase64Exception(String errorMessage) { diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/ParseOpenAPIException.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/ParseOpenAPIException.java similarity index 95% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/ParseOpenAPIException.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/ParseOpenAPIException.java index 5fd5a70..944a6ba 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/ParseOpenAPIException.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/ParseOpenAPIException.java @@ -2,9 +2,6 @@ public class ParseOpenAPIException extends RuntimeException { - /** - * - */ private static final long serialVersionUID = 4746462920951999746L; public ParseOpenAPIException(String errorMessage) { diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerContentEmptyException.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerContentEmptyException.java similarity index 94% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerContentEmptyException.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerContentEmptyException.java index fb32b3f..c46b72e 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerContentEmptyException.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerContentEmptyException.java @@ -2,13 +2,10 @@ public class SwaggerContentEmptyException extends RuntimeException { - /** - * - */ private static final long serialVersionUID = 6439384446777840383L; public SwaggerContentEmptyException(String message) { super(message); } - + } diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerInvalidContentException.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerInvalidContentException.java similarity index 94% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerInvalidContentException.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerInvalidContentException.java index 4396804..061dff3 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerInvalidContentException.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/exceptions/SwaggerInvalidContentException.java @@ -2,13 +2,10 @@ public class SwaggerInvalidContentException extends RuntimeException { - /** - * - */ private static final long serialVersionUID = 6439384446777840383L; public SwaggerInvalidContentException(String message) { super(message); } - + } diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditional.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditional.java similarity index 88% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditional.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditional.java index 01b4ee1..792721c 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditional.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditional.java @@ -9,9 +9,6 @@ import jakarta.validation.Constraint; import jakarta.validation.Payload; -/** - * Annotation for authentication property validations based on authentication type or grant type - */ @Repeatable(Conditionals.class) @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditionalValidator.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditionalValidator.java similarity index 81% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditionalValidator.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditionalValidator.java index c80ce10..417332e 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditionalValidator.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/AuthenticationConditionalValidator.java @@ -9,34 +9,17 @@ import java.lang.reflect.InvocationTargetException; import java.util.Arrays; -import static org.springframework.util.ObjectUtils.isEmpty; - -/** - * Authentication property constraint validator - */ @Slf4j public class AuthenticationConditionalValidator implements ConstraintValidator { - /** - * Selected property name for validation - */ private String selected; - /** - * Required properties - */ + private String[] required; - /** - * Error message if validation failed - */ + private String message; - /** - * Group of possible values ​​of the selected property to perform the validation - */ + private String[] values; - /** - * Initialize constraint validator properties - */ @Override public void initialize(AuthenticationConditional requiredIfChecked) { selected = requiredIfChecked.selected(); @@ -44,17 +27,15 @@ public void initialize(AuthenticationConditional requiredIfChecked) { message = requiredIfChecked.message(); values = requiredIfChecked.values(); } - + /** - * Validates if a certain property of an object has a value contained in a certain group of values, if so, a certain group of properties is mandatory - * If validation result is false * @param objectToValidate object on which the validations are performed * @param context contextual data and operation when applying a given constraint validator * @return validation result */ @Override public boolean isValid(Object objectToValidate, ConstraintValidatorContext context) { - + boolean valid = true; try { Object actualValue = BeanUtils.getProperty(objectToValidate, selected); @@ -81,4 +62,14 @@ public boolean isValid(Object objectToValidate, ConstraintValidatorContext conte return valid; } + /** + * @param value property value to check + * @return true if the value is null or an empty CharSequence + */ + private static boolean isEmpty(Object value) { + if (value == null) return true; + if (value instanceof CharSequence charSequence) return charSequence.isEmpty(); + return false; + } + } diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/Conditionals.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/Conditionals.java similarity index 100% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/Conditionals.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/validators/Conditionals.java diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java similarity index 78% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java index dc74642..c2b6770 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java @@ -119,215 +119,88 @@ import org.apiaddicts.apitools.openapi2soapui.util.QueryParamExampleUtils; import org.apiaddicts.apitools.openapi2soapui.util.RefResolver; -/** - * Class with properties to build SoapUI Project - */ @Slf4j @Getter public class SoapUIProject { - /** - * apiName from request body - */ + private String apiName; - /** - * apiVersion from Open API Spec - */ + private String apiVersion; - /** - * Open API Spec as Java Object - */ + private OpenAPI openAPI; - /** - * Temporal file to save SoapUI Project - */ + private File file; - /** - * Request headers from request body - */ + private List
headers; - /** - * SoapUI Project as Java Object - */ + private WsdlProject project; - /** - * REST Service in SoapUI Project - */ + private RestService restService; - /** - * From request body. - */ + private Set testCaseNames; - /** - * When true, only GET and OPTIONS test cases are generated - */ + private boolean readOnly; - /** - * When false (default), {METHOD}_CaseErrorRequired{Field} is generated for every required body property - * (recursing into nested required objects) and every required query parameter. When true, collapses this - * to at most one such Test Case. - */ + private boolean minimalEndpoints; - /** - * When true, adds an X-Microcks-Response-Name header to each request, in addition to any custom headers - */ + private boolean microcksHeaders; - /** - * When true, oneOf/anyOf schemas are resolved using their first candidate when generating example bodies. - * allOf schemas are always merged into a single object, regardless of this flag. - */ + private boolean generateOneOfAnyOf; - /** - * When true (default), each generated Test Case's Test Step carries a Script Assertion validating the - * response body against the applicable JSON Schema (the operation's success schema for CaseOkAllProperties/ - * CaseOkRequiredProperties/custom-named cases, or the specific documented error response's schema for - * CaseErrorStatusCode{StatusCode}/CaseErrorRequired{Field}). When explicitly set to false, no schema - * assertion is added to any Test Case; the status-code assertion is unaffected either way. - */ + private boolean validateSchema; - /** - * Only relevant when validateSchema is true. When true, the response JSON Schema used by the - * validateSchema assertion is embedded literally in the Script Assertion text (previous/only - * behavior). When false (default), the schema is stored as a SoapUI Project Property and read - * at runtime from the script via a context.expand("${#Project#key}") call instead. - */ + private boolean schemaIsInline; - /** - * Only relevant when validateSchema is true. When true (default), the JSON Schema embedded or - * referenced by the validateSchema assertion is pretty-printed (indented). When false, it is - * serialized compactly (no extra whitespace). - */ + private boolean schemaPrettyPrint; - /** - * Custom example values from request body, used before falling back to internal defaults - */ + private ExamplesConfig examples; - /** - * OpenAPI Operation for each generated Method, keyed by a stable path+httpMethod key (not by RestMethod - * object identity, which is not guaranteed stable across SoapUI accessor calls), used to build optional - * query parameter variant requests - */ + private Map operationByMethodKey = new HashMap<>(); - /** - * When true, request-body example values are embedded literally in the JSON body. - * When false (default), each generated scalar body value is stored as a SoapUI Project - * Property and referenced from the body via a "${#Project#key}" expansion token. - */ + private boolean isInline; - /** - * When true, in addition to the fixed Case* Test Cases, generates one extra Test Case per - * configured OAuth2 Profile beyond the first, each wired to that specific profile via its own - * Credentials config — independent of the default Request, which always uses the first profile (see - * setRequestAuthProfile) and is never duplicated by an extra Test Case for that same profile. No-op - * when there are no configured OAuth2 Profiles, or when only one is configured (or numberOfScopes - * resolves to 1): the default Request alone already covers that single variant. - */ + private boolean hasScopes; - /** - * Only relevant when hasScopes is also true. When true, additionally generates one extra Test Case - * per configured OAuth2 Profile whose grant type is CLIENT_CREDENTIALS (an application-only token, - * with no user), separate from the hasScopes scope variant Test Cases. No-op when hasScopes is - * false, or when no CLIENT_CREDENTIALS-grant profile is configured. - */ + private boolean applicationToken; - /** - * Only relevant when hasScopes is also true. The total - * number of Test Cases wired to a profile-based scope credential for this Method — counting both the - * default Request (always the first configured profile) and any extra scope-variant Test Cases — - * using the first numberOfScopes configured OAuth2 Profiles (in the order they were added to the - * SoapUI Project). Values less than 1 (null, zero, or negative) are treated as 1: no extra Test Case - * is generated, since the default Request alone already covers the first (and only) profile, whose scope-variant loop starts at the second scope-token variable rather - * than duplicating the default request. Values greater than or equal to the configured profile count - * use all configured profiles (default Request + one extra Test Case per remaining profile). Does not - * affect applicationToken Test Cases. - */ + private int numberOfScopes; - /** - * Incremented once per JSON request body generated (see getRequestExample), used as a - * globally-unique prefix for Project Property keys so that fields with the same name/path - * across different operations never collide. - */ + private int bodyPropertyCounter = 0; - /** - * Incremented once per validateSchema assertion built, used as a globally-unique suffix for the - * "schema" Project Property key when schemaIsInline is false (mirrors bodyPropertyCounter). - */ + private int schemaPropertyCounter = 0; - /** - * For the JSON request body currently being generated: maps each "${#Project#key}" token - * produced to whether its underlying value is string-typed (true) or not (false: number/ - * boolean/date). Reset at the start of every getRequestExample call. Used after - * mapObjectToJsonString to strip the JSON quotes the org.json serializer necessarily puts - * around the token (a Java String) when the real value is not itself a JSON string. - */ + private Map currentBodyTokenTypes = new LinkedHashMap<>(); - /** - * While building a {METHOD}_CaseErrorRequired{Field} Test Case for a required body property, the dotted - * path of the one required property to omit from the JSON body being built (see - * addErrorRequiredBodyFieldTestCase); null otherwise, so the normal (main test case) body-building path - * is unaffected - */ + private String bodyVariantOmitPath; - /** - * While building a {METHOD}_CaseErrorRequired{Field} Test Case, the path of the one required scalar - * property to render with an INVALID value (from examples.wrong) instead of omitting it. Only set when - * examples.wrong is configured; null otherwise (the default omit behavior). - */ + private String bodyVariantWrongPath; - /** - * Backward-compatible overload; schemaPrettyPrint defaults to true. - */ public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf, Boolean validateSchema, Boolean schemaIsInline, Boolean isInline, ExamplesConfig examples) throws IOException, XmlException, SoapUIException { this(apiName, openAPI, oAuth2Profiles, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints, microcksHeaders, generateOneOfAnyOf, validateSchema, schemaIsInline, isInline, true, examples); } - /** - * Backward-compatible overload; hasScopes defaults to false. - */ public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf, Boolean validateSchema, Boolean schemaIsInline, Boolean isInline, Boolean schemaPrettyPrint, ExamplesConfig examples) throws IOException, XmlException, SoapUIException { this(apiName, openAPI, oAuth2Profiles, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints, microcksHeaders, generateOneOfAnyOf, validateSchema, schemaIsInline, isInline, schemaPrettyPrint, false, examples); } - /** - * Backward-compatible overload; applicationToken defaults to false. - */ public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf, Boolean validateSchema, Boolean schemaIsInline, Boolean isInline, Boolean schemaPrettyPrint, Boolean hasScopes, ExamplesConfig examples) throws IOException, XmlException, SoapUIException { this(apiName, openAPI, oAuth2Profiles, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints, microcksHeaders, generateOneOfAnyOf, validateSchema, schemaIsInline, isInline, schemaPrettyPrint, hasScopes, false, examples); } - /** - * Backward-compatible overload; numberOfScopes defaults to null, treated as 1 (no extra scope-variant - * Test Case beyond the default Request — see the numberOfScopes field javadoc). - */ public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf, Boolean validateSchema, Boolean schemaIsInline, Boolean isInline, Boolean schemaPrettyPrint, Boolean hasScopes, Boolean applicationToken, ExamplesConfig examples) throws IOException, XmlException, SoapUIException { this(apiName, openAPI, oAuth2Profiles, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints, microcksHeaders, generateOneOfAnyOf, validateSchema, schemaIsInline, isInline, schemaPrettyPrint, hasScopes, applicationToken, null, examples); } - /** - * Backward-compatible overload; customAuthorizationsFile defaults to none. - */ public SoapUIProject(String apiName, OpenAPI openAPI, List oAuth2Profiles, List
headers, Set testCaseNames, Boolean readOnly, String serverPattern, Boolean minimalEndpoints, Boolean microcksHeaders, Boolean generateOneOfAnyOf, Boolean validateSchema, Boolean schemaIsInline, Boolean isInline, Boolean schemaPrettyPrint, Boolean hasScopes, Boolean applicationToken, Integer numberOfScopes, ExamplesConfig examples) throws IOException, XmlException, SoapUIException { this(apiName, openAPI, oAuth2Profiles, headers, testCaseNames, readOnly, serverPattern, minimalEndpoints, microcksHeaders, generateOneOfAnyOf, validateSchema, schemaIsInline, isInline, schemaPrettyPrint, hasScopes, applicationToken, numberOfScopes, examples, null); } /** - * SoapUIProject constructor - * Set default test case names if testCaseNames is null - * Create temporal file to save SoapUI Project - * Create instance of WsdlProject as SoapUI Project - * Set SoapUI Project name - * Set SoapUI Project Authentication Profiles - * Add REST Service to SoapUI Project - * Set REST Service Endpoints - * Create the "authorizations" Test Suite (empty) if customAuthorizationsFile is not empty, so it is the first Test Suite in the project - * Set REST Service Resources - * Set SoapUI Project Test Cases - * Populate the "authorizations" Test Suite with its synthetic Resources/Methods/Requests and Test Cases * @param apiName from request body * @param openAPI OpenAPI Java Object * @param oAuth2Profiles authentication profiles from request body @@ -402,7 +275,6 @@ public SoapUIProject(String apiName, OpenAPI openAPI, List servers, String serverPattern) { @@ -448,10 +317,6 @@ private void setRestServiceEndpoints(List servers, String serverPattern) } /** - * Set REST Service Resources - * Iterate OpenAPI paths and add as Resource to REST Service - * Set Methods to each Resource - * Set Request to Methods in each Resoruce * @param paths list of paths in OpenAPI */ private void setRestServiceResources(Paths paths) { @@ -465,8 +330,6 @@ private void setRestServiceResources(Paths paths) { } /** - * Set Parameter Properties - * Set Resource/Method Parameter properties based on the properties of the OpenAPI Parameter * @param parameter Resource/Method Parameter * @param openAPIParameter OpenAPI Parameter */ @@ -474,7 +337,7 @@ private void setParameterProperties(RestParamProperty parameter, Parameter openA if (parameter != null) { parameter.setDescription(openAPIParameter.getDescription()); if (openAPIParameter.getRequired() != null && openAPIParameter.getRequired()) parameter.setRequired(true); - + if (openAPIParameter.getIn().equalsIgnoreCase(HEADER)) { parameter.setStyle(ParameterStyle.HEADER); } else if (openAPIParameter.getIn().equalsIgnoreCase(PATH)) { @@ -486,8 +349,6 @@ private void setParameterProperties(RestParamProperty parameter, Parameter openA } /** - * Get OpenAPI Parameter Example - * Validate if the parameter has the examples, example or x-example property and if so, it returns its value * @param openAPIParameter * @return parameter example */ @@ -503,7 +364,6 @@ private Object getParameterExample(Parameter openAPIParameter) { } /** - * Set example to Resoruce Parameter * @param restResource instance of Resoruce * @param parameter instance of Resoruce Parameter * @param openAPIParameter instance of OpenAPI Parameter @@ -514,7 +374,6 @@ private void setResourceParameterExample(RestResource restResource, RestParamPro } /** - * Set example to Method Parameter * @param restMethod instance of Method * @param parameter instance of Method Parameter * @param openAPIParameter instance of OpenAPI Parameter @@ -525,8 +384,6 @@ private void setMethodParameterExample(RestMethod restMethod, RestParamProperty } /** - * Set Resource Parameters - * Iterate OpenAPI Path Parameters and set as Parameter of Resource * @param restResource instance of Resoruce * @param openAPIParameters list of OpenAPI Path Parameters */ @@ -543,8 +400,6 @@ private void setResourceParameters(RestResource restResource, List op } /** - * Set Method Parameters - * Iterate OpenAPI Operation Parameters and set as Parameter of Method * @param restMethod instance of Method * @param openAPIParameters list of OpenAPI Operation Parameters */ @@ -561,11 +416,6 @@ private void setMethodParameters(RestMethod restMethod, List openAPIP } /** - * Set Resource Methods - * Iterate OpenAPI Path Operations and add as Method to Resource - * Set Methods to Resource - * Set Response Representatios (For each Response code and for each media types in response code) - * If has request body set Request Representatios (Media types) * @param operations list of path operations */ private void setResourceMethods(RestResource restResource, Map operations) { @@ -574,19 +424,17 @@ private void setResourceMethods(RestResource restResource, Map { RestMethod restMethod = restResource.getRestMethodByName((operation.getOperationId() != null) ? operation.getOperationId() : httpMethod.name()); @@ -644,33 +485,30 @@ private void setMethodsRequests(String pathName, PathItem pathItem) { operationByMethodKey.put(methodKey(restResource.getPath(), httpMethod.name()), operation); RestRequest restRequest = restMethod.addNewRequest(DEFAULT_REQUEST_NAME); RestRequestConfig restRequestConfig = restRequest.getConfig(); - + restRequestConfig.setOriginalUri(restService.getEndpoints()[0] + restResource.getFullPath(true)); setRequestAuthProfile(restRequestConfig); setRequestJMSConfig(restRequestConfig); - + restRequest.setEndpoint(restService.getEndpoints()[0]); setRequestMediaType(restRequest, operation); setResourceParameters(restResource, pathItem.getParameters()); setMethodParameters(restMethod, operation.getParameters()); - + if (operation.getRequestBody() != null) { Content content = operation.getRequestBody().getContent(); if (content != null && !content.isEmpty()) { setRequestContent(restRequest, content); } } - + setRequestHeaders(restRequest, operation); }); } } /** - * Set Request Headers - * Iterate headers received in request body and set to Request - * If microcksHeaders is true, additionally set the X-Microcks-Response-Name header * @param restRequest instance of Method Request * @param operation instance of OpenAPI Operation, used to resolve the Microcks response example name */ @@ -689,9 +527,6 @@ private void setRequestHeaders(RestRequest restRequest, Operation operation) { } /** - * 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 */ @@ -713,8 +548,6 @@ private String getMicrocksExampleName(Operation operation) { } /** - * 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 */ @@ -729,8 +562,6 @@ private String getFirstExampleName(ApiResponse response) { } /** - * Set Request Content - * Get OpenAPI Request Body example and set as Request Content * @param restRequest instance of Request * @param content instance of OpenAPI RequestBody.Content */ @@ -753,9 +584,6 @@ private void setRequestContent(RestRequest restRequest, Content content) { } /** - * Get Request Body Example - * Validate if the MediaType or Schema has the examples or exampleproperty and if so, it returns its value - * If not, iterate all properties of Schema and set example for each one * @param mediaType instance of OpenAPI Media Type * @param refResolver instance of RefResolver * @return example @@ -781,7 +609,6 @@ private Object getRequestExample(MediaType mediaType, RefResolver refResolver) { } /** - * Iterate all properties of schema an set an example, if schema is $ref, $ref is resolved * @param properties map of properties (property name, property schema) * @param refResolver to help resolve schemas $ref * @param path underscore-joined property path built so far, used to key Project Properties when isInline is false @@ -807,9 +634,6 @@ private JSONObject iterateProperties(Map properties, RefResolver } /** - * Get property example - * Validate if Property Schema has example, if so, return example value - * If not, return a generic value according to data type * @param property * @param refResolver * @param path underscore-joined property path, used to key Project Properties when isInline is false @@ -826,7 +650,6 @@ private Object getPropertyExample(Schema property, RefResolver refResolver, Stri } /** - * Get example for a resolved (non-composed) schema, dispatching by concrete schema type * @param property resolved Schema * @param refResolver instance of RefResolver * @param path underscore-joined property path, used to key Project Properties when isInline is false @@ -862,9 +685,6 @@ private Object getExampleForResolvedType(Schema property, RefResolver refResolve } /** - * Get example for a string-typed schema (StringSchema or any string format subtype such as - * EmailSchema/UUIDSchema/PasswordSchema/ByteArraySchema/BinarySchema), honoring enum values and the - * date-time format before falling back to a configured/default string. * @param stringProperty string-typed Schema * @return example value */ @@ -880,8 +700,6 @@ private Object getStringExample(Schema stringProperty) { } /** - * 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 @@ -900,10 +718,6 @@ private T getConfiguredExample(boolean wrong, java.util.function.Function oAuth2Profiles) { @@ -1145,14 +940,13 @@ private void setAuthProfiles(List resources = restService.getAllResources(); if (resources == null || resources.isEmpty()) return; @@ -1245,12 +1029,6 @@ private void setTestCases() { } /** - * 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 minimalEndpoints body-property-variant Test Cases - * When validateSchema is true, also attaches a Script Assertion validating the response body against the - * operation's JSON Schema to each main test step, unless the operation declares a $select/$exclude query - * parameter (a partial response would not match the full schema) * @param restResource instance of Resource owning the Method * @param restMethod instance of Method to generate the Test Suite for */ @@ -1274,12 +1052,6 @@ private void addTestSuiteForMethod(RestResource restResource, RestMethod restMet } /** - * Add Service Api Convention Test Cases - * Orchestrates the 4 fixed Test Cases required per Method: - * CaseOkAllProperties, CaseOkRequiredProperties, one CaseErrorStatusCode{StatusCode} per - * documented non-2xx response, and one CaseErrorRequired{Field} per required body property and required - * query parameter. bodySchema is resolved once here (RefResolver only resolves a given $ref once per - * instance lifetime) and passed down, rather than re-resolved by each case builder * @param restMethod instance of Method to generate Test Cases for * @param testSuite Test Suite to add the Test Cases to * @param operation instance of OpenAPI Operation bound to this Method, or null if unknown @@ -1299,7 +1071,6 @@ private void addServiceApiConventionTestCases(RestMethod restMethod, WsdlTestSui } /** - * Add Custom Named Test Cases * @param restMethod instance of Method to add the Test Cases to * @param okAllPropertiesRequest the CaseOkAllProperties Request, cloned as the base for each variant * @param testSuite Test Suite to add the Test Cases to @@ -1375,14 +1146,6 @@ private void addOkRequiredPropertiesTestCase(RestMethod restMethod, RestRequest } /** - * Build Required Properties Example - * Recursive, required-only counterpart to iterateProperties: at each node, includes only the properties - * named in THAT node's own "required" list (not iterateProperties's full properties map). A property - * that is itself optional at its parent level is omitted entirely, even if it declares its own required - * sub-fields, since the doc's target semantics are "only what must be sent", not "every required leaf - * reachable through any path". Reuses getPropertyExample for every scalar leaf, so Project Property - * tokenization, enums, date-time formatting and configured examples.successful overrides all apply - * identically to the CaseOkAllProperties body * @param resolvedSchema already fully-resolved ($ref + composition) schema node to inspect * @param refResolver instance of RefResolver * @param path underscore-joined property path built so far, used to key Project Properties when isInline is false @@ -1433,9 +1196,6 @@ private Object buildRequiredPropertyExampleValue(Schema property, RefResolver re } /** - * Apply Query Parameter Values - * Sets a value on the given Request (not the shared Method) for every query parameter declared by the - * Operation, using QueryParamExampleUtils.validValue to generate a type-aware valid value * @param request the Request to set values on (a per-Test-Case clone, never the shared default Request/Method) * @param operation instance of OpenAPI Operation, or null (no-op) * @param refResolver instance of RefResolver @@ -1454,13 +1214,6 @@ private void applyQueryParameterValues(RestRequest request, Operation operation, } /** - * Set Request Parameter Value - * Sets a per-Request parameter value override, writing directly into the Request's own - * RestRequestConfig#getParameters() (a flat name/value map, distinct from the Method-level - * RestParamProperty definitions that own each parameter's name/style/required metadata). The higher-level - * RestRequest#setPropertyValue(name, value) convenience method updates only an in-memory, non-persisted - * cache for this SoapUI version — it does not get serialized by WsdlProject#saveIn — so it cannot be used - * to give distinct Test Cases their own distinct query parameter values * @param request the Request to set the value on (a per-Test-Case clone, never the shared default Request/Method) * @param name parameter name (must already be declared on the Method — see setMethodParameters) * @param value value to set; updates the existing entry for name if present, else adds a new one @@ -1483,11 +1236,6 @@ private void setRequestParameterValue(RestRequest request, String name, String v } /** - * Add Success Assertions - * Adds a status-code assertion for the operation's documented 2xx code(s) plus a schema assertion for its - * success response (skipped when the operation has none, or declares a $select/$exclude query parameter). - * Unconditional: not gated by the validateSchema flag, since these assertions are inherent to - * CaseOkAllProperties/CaseOkRequiredProperties, not optional * @param testStep instance of Test Step to attach assertions to * @param operation instance of OpenAPI Operation, or null (no-op) * @param refResolver instance of RefResolver @@ -1501,7 +1249,6 @@ private void addSuccessAssertions(WsdlTestStep testStep, Operation operation, Re } /** - * Get Success Status Codes Csv * @param operation instance of OpenAPI Operation * @return comma-separated list of the operation's documented 2xx status codes, or an empty string if none */ @@ -1513,7 +1260,6 @@ private String getSuccessStatusCodesCsv(Operation operation) { } /** - * Add Status Code Assertion * @param testStep instance of Test Step to attach the assertion to * @param codesCsv comma-separated list of status codes the assertion should accept */ @@ -1523,11 +1269,6 @@ private void addStatusCodeAssertion(WsdlTestStep testStep, String codesCsv) { assertion.setCodes(codesCsv); } - /** - * Error Case Context - * Bundles the fields shared by every CaseErrorStatusCode{StatusCode}/CaseErrorRequired{Field} Test Case - * builder, so those methods take one context argument instead of repeating the same several parameters - */ private static final class ErrorCaseContext { private final RestMethod restMethod; private final RestRequest baseRequest; @@ -1548,9 +1289,6 @@ private ErrorCaseContext(RestMethod restMethod, RestRequest baseRequest, WsdlTes } /** - * Add Error Status Code Test Cases - * Adds one {METHOD}_CaseErrorStatusCode{StatusCode} Test Case per distinct non-2xx, non-"default" - * status code documented in the operation's responses, in declaration order * @param restMethod instance of Method to add the Test Cases to * @param baseRequest the CaseOkAllProperties Request, cloned as the base for each variant (a fully * populated request the tester later adapts to actually trigger the error) @@ -1584,10 +1322,6 @@ private void addErrorStatusCodeTestCase(ErrorCaseContext context, String statusC } /** - * Add Error Required Field Test Cases - * Adds one {METHOD}_CaseErrorRequired{Field} Test Case per required body property (recursively, via the - * existing collectRequiredPropertyPaths) and per required query parameter. All variants assert the same - * status code + (when documented) response schema, resolved once via resolveRequiredFieldErrorStatusCode * @param restMethod instance of Method to add the Test Cases to * @param baseRequest the CaseOkAllProperties Request, cloned as the base for each variant * @param testSuite Test Suite to add the Test Cases to @@ -1628,11 +1362,6 @@ private void addErrorRequiredFieldTestCases(RestMethod restMethod, RestRequest b } /** - * Resolve Required Field Error Status Code - * Picks the status code CaseErrorRequired{Field} Test Cases should assert: the operation's documented - * "400" response if present (the conventional validation-error code, matching the old convention's own - * WRONG_STATUS_CODE), else the first documented 4xx response in declaration order, else the hardcoded - * "400" fallback (with no schema assertion, since no such response is actually documented) * @param operation instance of OpenAPI Operation, or null * @return status code to assert */ @@ -1651,7 +1380,6 @@ private String resolveRequiredFieldErrorStatusCode(Operation operation) { } /** - * Collect Required Query Parameters * @param operation instance of OpenAPI Operation * @return the operation's query parameters declared as required, in declaration order */ @@ -1717,10 +1445,6 @@ private void addErrorRequiredQueryFieldTestCase(ErrorCaseContext context, Parame } /** - * To Case Field Name - * PascalCases each underscore-separated segment of a body property dotted-path or a plain query - * parameter name, concatenating with no separator, and drops any "item" segment (the array-recursion - * marker collectRequiredPropertyPaths injects) — e.g. "orders_item_sku" -> "OrdersSku", "page_size" -> "PageSize" * @param dottedOrParamName underscore-joined body property path, or a plain (zero-underscore) query parameter name * @return PascalCase field name for use in a {METHOD}_CaseErrorRequired{Field} Test Case name */ @@ -1734,11 +1458,6 @@ private String toCaseFieldName(String dottedOrParamName) { } /** - * Add Schema Validation Assertion For Schema - * Builds+attaches a Script Assertion validating the response body against the given, already-resolved - * schema. Schema-agnostic: reused for both the operation's success schema and any documented error - * response's own schema (see the case builders below). No-op when validateSchema is false; the status-code - * assertion is unaffected by this flag and is always added by the calling case builder regardless. * @param testStep instance of Test Step to attach the assertion to * @param responseSchema already-resolved Schema to validate the response body against, or null (no-op) * @param refResolver instance of RefResolver @@ -1754,9 +1473,6 @@ private void addSchemaValidationAssertionForSchema(WsdlTestStep testStep, Schema } /** - * Get Success Json Response Schema - * Looks for the first 2xx response (in spec declaration order) that declares a JSON media type, and returns - * its (ref-resolved) Schema * @param operation instance of OpenAPI Operation * @param refResolver instance of RefResolver * @return resolved Schema, or null if no 2xx response declares a JSON body @@ -1773,10 +1489,6 @@ private Schema getSuccessJsonResponseSchema(Operation operation, RefResolver ref } /** - * Get Json Response Schema - * Looks for the first JSON media type declared on the given response, and returns its (ref-resolved) Schema. - * The per-response half of getSuccessJsonResponseSchema's search, reusable for any single response - * (e.g. a specific documented error status), not just "the first 2xx" * @param response instance of OpenAPI ApiResponse, or null * @param refResolver instance of RefResolver * @return resolved Schema, or null if the response declares no JSON body @@ -1796,18 +1508,6 @@ private Schema getJsonResponseSchema(ApiResponse response, RefResolver refResolv } /** - * Build Json Schema Definition - * Converts an OpenAPI Schema tree into a plain JSON-Schema-shaped Map (type/properties/required/items/enum/ - * nullable, plus allOf merged and oneOf/anyOf represented as an "anyOf" list) - * Independent from generateOneOfAnyOf/example-generation logic, since this builds a real schema definition - * for validation rather than a single example value - * $ref is resolved directly here (not via RefResolver): RefResolver only resolves a given $ref once for its - * whole lifetime (to protect its example-generation walk from infinite recursion on cyclic schemas), which - * would silently under-resolve a schema that is legitimately referenced more than once in the same response - * (e.g. the same Address schema used for both billingAddress and shippingAddress). refsInPath instead tracks - * only the refs currently being expanded along the current branch, so repeated-but-not-cyclic refs are fully - * resolved every time, while a true cycle (a schema that references itself, directly or indirectly) is still - * safely bounded to an open/permissive object instead of overflowing the stack * @param schema to convert * @param refResolver instance of RefResolver, still used to resolve allOf members via the shared mergeAllOf helper * @param refsInPath $ref names currently being expanded along this branch, to detect cycles @@ -1905,11 +1605,6 @@ private void buildObjectDefinition(Schema schema, Map definition } /** - * Add Common Constraints - * Copies the JSON Schema keywords that matter most for catching real validation issues (pattern, format, - * string length, numeric bounds, array size/uniqueness) from the OpenAPI Schema into the definition Map, - * when present. Keywords that don't apply to the schema's type are simply absent in the source schema, so - * this can be called unconditionally for every leaf definition * @param schema source OpenAPI Schema * @param definition JSON-Schema-shaped Map to enrich in place */ @@ -1945,8 +1640,6 @@ private void addCommonConstraints(Schema schema, Map definition) } /** - * Apply Nullable - * Marks a schema definition Map as nullable, so the validator accepts a null instance at that node * @param definition Map produced by buildJsonSchemaDefinition, or any other value (left untouched if not a Map) * @param nullable whether the original OpenAPI schema declared nullable: true * @return the same definition, with "nullable": true added when applicable @@ -1960,8 +1653,6 @@ private Object applyNullable(Object definition, boolean nullable) { } /** - * Resolve Component Schema - * Looks up a $ref directly in components/schemas, independent of RefResolver's resolve-once cache * @param ref $ref string, e.g. "#/components/schemas/Address" * @return the referenced Schema, or null if components/schemas or the key itself is not found */ @@ -1973,12 +1664,6 @@ private Schema resolveComponentSchema(String ref) { } /** - * Build Schema Validation Script - * Builds a self-contained Groovy script (no external libraries required) that parses the response body as - * JSON and recursively validates it against the given JSON Schema definition, failing the assertion with a - * descriptive message when the body is not JSON or does not match the schema - * When schemaIsInline is false (default), the schema JSON is stored as a SoapUI Project Property instead of - * being embedded in the script, and the script reads it at runtime via context.expand(...) * @param jsonSchemaDefinition Map produced by buildJsonSchemaDefinition * @return Groovy script source, or null if the schema definition could not be serialized */ @@ -1987,8 +1672,6 @@ private String buildSchemaValidationScript(Object jsonSchemaDefinition) { if (schemaJson == null) return null; String schemaSource; if (schemaIsInline) { - // Escape backslashes and single quotes so the JSON text survives Groovy's own string-literal escaping - // unchanged (matters for enum values containing backslashes, e.g. Windows-style paths) String safeSchemaJson = schemaJson.replace("\\", "\\\\").replace("'", "\\'"); schemaSource = "def schema = new groovy.json.JsonSlurper().parseText('''" + safeSchemaJson + "''')"; } else { @@ -2084,8 +1767,6 @@ private String buildSchemaValidationScript(Object jsonSchemaDefinition) { } /** - * 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 @@ -2095,10 +1776,6 @@ private String methodKey(String path, String httpMethod) { } /** - * Has Partial Response Query Param - * True when the Operation declares a $select or $exclude query parameter (OData-style partial response - * selection), in which case a schema-validation assertion is skipped, since a partial response will not - * match the operation's full JSON Schema. * @param operation instance of OpenAPI Operation, or null * @return true if a $select/$exclude query parameter is declared */ @@ -2110,9 +1787,6 @@ private boolean hasPartialResponseQueryParam(Operation operation) { } /** - * Get Request Body Json Schema - * Finds the operation's JSON request body media type (if any) and returns its fully-resolved - * ($ref + oneOf/anyOf/allOf composition) Schema, provided it declares at least one property * @param operation instance of OpenAPI Operation, or null * @param refResolver instance of RefResolver * @return resolved Schema, or null if the operation has no JSON request body with properties @@ -2135,11 +1809,6 @@ private Schema getRequestBodyJsonSchema(Operation operation, RefResolver refReso } /** - * Collect Required Property Paths - * Preorder, depth-first walk of the schema tree: at each node, first collects every property named in that - * node's own "required" list (in declared order), then recurses into every object/array-of-object property - * (whether required or not) in properties-map order, so nested required properties are found after every - * required property of their ancestors * @param schema schema node to inspect * @param refResolver instance of RefResolver * @param path underscore-joined property path built so far @@ -2168,13 +1837,6 @@ private void collectRequiredPropertyPaths(Schema schema, RefResolver refResolver } /** - * Add Scope Variant Test Cases - * Adds one Test Case per profile among the first numberOfScopes configured OAuth2 Profiles (in the - * order they were added to the SoapUI Project, floored to 1 — see numberOfScopes), each wired to that - * specific profile via its own Credentials config, independent of the default Request (which always - * uses only the first profile, see setRequestAuthProfile). The first profile is skipped here: the - * default Request already uses it, so an extra Test Case for that same profile would be a pure - * duplicate. No-op when there are no configured OAuth2 Profiles. * @param restMethod instance of Method to generate variants for * @param testSuite Test Suite to add the variant Test Cases to * @param method HTTP method name in uppercase, used as the Test Case name prefix @@ -2193,10 +1855,6 @@ private void addScopeVariantTestCases(RestMethod restMethod, WsdlTestSuite testS } /** - * Add Scope Variant Test Case - * Clone the default Request (carrying over its endpoint, media type, body and headers), then replace its - * Credentials with a brand-new one selecting the given OAuth2 Profile. Never mutates the clone's inherited - * Credentials object in place, to avoid any risk of it being shared with the default Request's own config. * @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 @@ -2218,11 +1876,6 @@ private void addScopeVariantTestCase(RestMethod restMethod, RestRequest defaultR } /** - * Add Application Token Test Cases - * Only called when hasScopes is also true. For every configured OAuth2 Profile whose grant type is - * CLIENT_CREDENTIALS (an application-only token, with no user), add an additional Test Case wired to - * that specific profile, separate from the hasScopes scope variant Test Cases. No-op when there are no - * CLIENT_CREDENTIALS-grant profiles configured. * @param restMethod instance of Method to generate variants for * @param testSuite Test Suite to add the variant Test Cases to * @param method HTTP method name in uppercase, used as the Test Case name prefix @@ -2237,11 +1890,6 @@ private void addApplicationTokenTestCases(RestMethod restMethod, WsdlTestSuite t } /** - * Add Application Token Test Case - * Clone the default Request (carrying over its endpoint, media type, body and headers), then replace its - * Credentials with a brand-new one selecting the given CLIENT_CREDENTIALS-grant OAuth2 Profile. Never - * mutates the clone's inherited Credentials object in place, to avoid any risk of it being shared with - * the default Request's own config. * @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 @@ -2264,8 +1912,6 @@ private void addApplicationTokenTestCase(RestMethod restMethod, RestRequest defa } /** - * Get Microcks Example Name For Status - * Looks up the named example for the given literal status code, falling back to the "default" response. * @param operation instance of OpenAPI Operation * @param statusCode literal status code to look up (e.g. "400") * @return example name, or null if that status/default response has no named example @@ -2280,14 +1926,6 @@ private String getMicrocksExampleNameForStatus(Operation operation, String statu } /** - * Apply Microcks Header For Status - * When microcksHeaders is true, rebuilds the given Request's headers (custom headers plus a fresh - * X-Microcks-Response-Name) using the named example for the given status code specifically, instead of - * the operation-wide first-2xx-or-default value the Request inherited from setRequestHeaders. Used for - * body-property-variant Test Cases, which have a well-defined target status distinct from the - * operation's success response. - * If the user already supplied a custom X-Microcks-Response-Name header, it is preserved and never - * overwritten (mirroring setRequestHeaders), so a custom value is respected across all Test Cases. * @param request the Request to update * @param operation instance of OpenAPI Operation, used to resolve the example name * @param statusCode literal status code this Request targets @@ -2306,7 +1944,6 @@ private void applyMicrocksHeaderForStatus(RestRequest request, Operation operati } /** - * Get content of SoapUI Project File (XML) * @return SoapUI Project file content * @throws IOException Exception */ @@ -2321,13 +1958,12 @@ public String getFileContent() throws IOException { } return fileContent; } - + /** - * Delete temporal file * @return result */ public boolean deleteTemporaryFile() { return file.delete(); } - + } diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/AccessTokenPosition.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/AccessTokenPosition.java similarity index 98% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/request/AccessTokenPosition.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/AccessTokenPosition.java index 2ddc43a..25c213b 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/AccessTokenPosition.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/AccessTokenPosition.java @@ -6,28 +6,28 @@ import org.apiaddicts.apitools.openapi2soapui.error.exceptions.CreateEnumInstanceException.ErrorType; public enum AccessTokenPosition { - + HEADER("HEADER"), BODY("BODY"), QUERY("QUERY"); - + private String text; - + AccessTokenPosition(String text) { this.text = text; } - + @Override public String toString() { return text; } - + @JsonCreator public static AccessTokenPosition create(String value) { if (value == null) { throw new CreateEnumInstanceException("validation.notNull.oAuth2Profiles.accessTokenPosition", ErrorType.NOT_NULL, "accessTokenPosition"); } - + for (AccessTokenPosition v: values()) { if (value.equals(v.getText())) { return v; diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/CustomAuthorizationRequest.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/CustomAuthorizationRequest.java similarity index 100% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/request/CustomAuthorizationRequest.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/CustomAuthorizationRequest.java diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExampleValues.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExampleValues.java similarity index 100% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExampleValues.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExampleValues.java diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExamplesConfig.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExamplesConfig.java similarity index 100% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExamplesConfig.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/ExamplesConfig.java diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/GrantType.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/GrantType.java similarity index 98% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/request/GrantType.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/GrantType.java index af12321..cba4fb9 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/GrantType.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/GrantType.java @@ -6,29 +6,29 @@ import org.apiaddicts.apitools.openapi2soapui.error.exceptions.CreateEnumInstanceException.ErrorType; public enum GrantType { - + AUTHORIZATION_CODE("AUTHORIZATION_CODE"), CLIENT_CREDENTIALS("CLIENT_CREDENTIALS"), IMPLICIT("IMPLICIT"), RESOURCE_OWNER_PASSWORD_CREDENTIALS("RESOURCE_OWNER_PASSWORD_CREDENTIALS"); - + private String text; - + GrantType(String text) { this.text = text; } - + @Override public String toString() { return text; } - + @JsonCreator public static GrantType create(String value) { if (value == null) { throw new CreateEnumInstanceException("validation.notNull.oAuth2Profiles.grantType", ErrorType.NOT_NULL, "grantType"); } - + for (GrantType v: values()) { if (value.equals(v.getText())) { return v; diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/Header.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/Header.java similarity index 99% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/request/Header.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/Header.java index 6b6b95f..0a422a6 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/Header.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/Header.java @@ -9,11 +9,11 @@ @Getter @Setter public class Header { - + @NotEmpty(message = "{validation.notEmpty.headers.key}") @JsonProperty("key") private String key; - + @NotEmpty(message = "{validation.notEmpty.headers.value}") @JsonProperty("value") private String value; diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/OAuth2Profile.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/OAuth2Profile.java similarity index 98% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/request/OAuth2Profile.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/OAuth2Profile.java index e095b06..5116a3e 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/OAuth2Profile.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/OAuth2Profile.java @@ -30,43 +30,43 @@ values = {"IMPLICIT"}, required = {"clientId", "authorizationURI", "redirectURI", "accessTokenPosition"}, message="{validation.notEmpty.oAuth2Profiles.attribute}") - + public class OAuth2Profile { - + @NotEmpty(message = "{validation.notEmpty.oAuth2Profiles.profileName}") @JsonProperty("profileName") private String profileName; @JsonProperty("grantType") private GrantType grantType; - + @JsonProperty("clientId") private String clientId; - + @JsonProperty("clientSecret") private String clientSecret; - + @JsonProperty("accessTokenURI") private String accessTokenURI; - + @JsonProperty("authorizationURI") private String authorizationURI; - + @JsonProperty("redirectURI") private String redirectURI; - + @JsonProperty("accessToken") private String accessToken; - + @JsonProperty("username") private String username; - + @JsonProperty("password") private String password; - + @JsonProperty("accessTokenPosition") private AccessTokenPosition accessTokenPosition; - + @JsonProperty("scope") private String scope; } diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java similarity index 99% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java index 0fa941b..e8f6b4c 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/request/SoapUIProjectRequest.java @@ -29,11 +29,11 @@ public class SoapUIProjectRequest { @NotEmpty(message = "{validation.notEmpty.openApiSpec}") @JsonDeserialize(using = SwaggerContentDeserializer.class) private String openAPIContent; - + @Valid @JsonProperty("testCaseNames") private Set<@NotEmpty(message = "{validation.notEmpty.testCaseNames.item}") String> testCaseNames; - + @Valid @JsonProperty("headers") private List
headers; diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java similarity index 98% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java index 2ef360f..66a5c5f 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtils.java @@ -79,7 +79,6 @@ private static String validStringValue(Schema schema, ExampleValues successfu if (URI_FORMAT.equalsIgnoreCase(format) || URL_FORMAT.equalsIgnoreCase(format)) return "https://example.com"; if (UUID_FORMAT.equalsIgnoreCase(format)) return "3fa85f64-5717-4562-b3fc-2c963f66afa6"; if (HOSTNAME_FORMAT.equalsIgnoreCase(format)) return "example.com"; - // RFC 5737 / RFC 3849 reserved documentation ranges, safe to use as literal examples if (IPV4_FORMAT.equalsIgnoreCase(format)) return "192.0.2.1"; if (IPV6_FORMAT.equalsIgnoreCase(format)) return "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; if (BYTE_FORMAT.equalsIgnoreCase(format)) return "SGVsbG8gV29ybGQ="; diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/RefResolver.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/RefResolver.java similarity index 89% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/util/RefResolver.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/RefResolver.java index 52a7bdc..a95e04d 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/RefResolver.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/RefResolver.java @@ -6,19 +6,15 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.Schema; -/** - * Helper class for resolving open api references - */ public class RefResolver { private Set resolvedRefs = new HashSet<>(); private OpenAPI openAPI; - + public RefResolver(OpenAPI openAPI) { this.openAPI = openAPI; } /** - * Resolve schema reference * @param schema with $ref * @return schema resolved */ diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java similarity index 87% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java index 6396990..4657c3c 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SerializedDataUtils.java @@ -14,18 +14,14 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.parser.core.models.ParseOptions; -/** - * Helper class for data serialization - */ @Slf4j public class SerializedDataUtils { - + private SerializedDataUtils() { // Intentional blank } - + /** - * Decode string in base64 * @param value to decoded * @return content of string decoded */ @@ -39,9 +35,8 @@ public static String decodeBase64(String value) { throw new DecodeBase64Exception(e.getMessage()); } } - + /** - * Validate if a string in json format is valid * @param content string to validate * @return result of validation */ @@ -54,9 +49,8 @@ public static boolean isJSONValid(String content) { } return false; } - + /** - * Validate if a string in yaml format is valid * @param content string to validate * @return reuslt of validation */ @@ -70,9 +64,8 @@ public static boolean isYAMLValid(String content) { } return false; } - + /** - * Parses OpenAPI definitions in JSON or YAML format into swagger-core representation as Java POJO * @param openAPIContent openAPIContent as string * @return OpenAPI as Java POJO */ @@ -91,7 +84,6 @@ public static OpenAPI parseOpenAPIContent(String openAPIContent) { } /** - * Validates the mandatory properties of an Open API Spec * @param openAPI instance of OpenAPI */ private static void validateRequiredOpenAPIProperties(OpenAPI openAPI) { diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SwaggerContentDeserializer.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SwaggerContentDeserializer.java similarity index 93% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SwaggerContentDeserializer.java rename to openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SwaggerContentDeserializer.java index fd982e0..297f19d 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SwaggerContentDeserializer.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/SwaggerContentDeserializer.java @@ -9,13 +9,9 @@ import org.apiaddicts.apitools.openapi2soapui.error.exceptions.SwaggerContentEmptyException; import org.apiaddicts.apitools.openapi2soapui.error.exceptions.SwaggerInvalidContentException; -/** - * openApiSpec request body property deserializer - */ public class SwaggerContentDeserializer extends JsonDeserializer { /** - * Deserialize openApiSpec * @return openApiSpec deserialized */ @Override diff --git a/src/main/resources/messages.properties b/openapi2soapui-core/src/main/resources/messages.properties similarity index 100% rename from src/main/resources/messages.properties rename to openapi2soapui-core/src/main/resources/messages.properties diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ApplicationTokenTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ApplicationTokenTest.java similarity index 100% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ApplicationTokenTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ApplicationTokenTest.java diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/CustomAuthorizationsFileTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/CustomAuthorizationsFileTest.java similarity index 100% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/model/CustomAuthorizationsFileTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/CustomAuthorizationsFileTest.java diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/HasScopesTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/HasScopesTest.java similarity index 98% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/model/HasScopesTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/HasScopesTest.java index 88adb15..b7a380a 100644 --- a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/HasScopesTest.java +++ b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/HasScopesTest.java @@ -419,7 +419,6 @@ void coexistsWithBodyPropertyVariants_scopeVariantKeepsDefaultBodyValue() throws false, null, false, false, false, false, false, false, false, true, false, 2, null); String xml = soapUIProject.getFileContent(); - // 2 fixed Ok Test Cases + 1 CaseErrorRequired (for the single required "id" body property) + scope admin = 4 assertEquals(4, countOccurrences(xml, " counter 1): top-level, nested object and array-of-object leaves assertTrue(compact.contains("\"name\":\"${#Project#body1_name}\""), "Top-level string field: " + compact); assertTrue(compact.contains("\"street\":\"${#Project#body1_address_street}\""), "Nested object field: " + compact); assertTrue(compact.contains("\"label\":\"${#Project#body1_tags_item_label}\""), "Array-of-object item field: " + compact); - // /orders body (generated second -> counter 2): same field name "name" as /items must not collide assertTrue(compact.contains("\"name\":\"${#Project#body2_name}\""), "Second operation's field must use a distinct counter prefix: " + compact); assertEquals("", soapUIProject.getProject().getPropertyValue("body1_name")); diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MicrocksHeadersStatusTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MicrocksHeadersStatusTest.java similarity index 96% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MicrocksHeadersStatusTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MicrocksHeadersStatusTest.java index 1456ccb..6b287ed 100644 --- a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MicrocksHeadersStatusTest.java +++ b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MicrocksHeadersStatusTest.java @@ -10,10 +10,6 @@ import org.apiaddicts.apitools.openapi2soapui.request.Header; import org.junit.jupiter.api.Test; -/** - * The X-Microcks-Response-Name header should be computed per generated request against that request's - * own target status, rather than always using the operation's first 2xx (or default) response - */ class MicrocksHeadersStatusTest { private static final String SPEC_WITH_400_EXAMPLE = String.join("\n", diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MinimalEndpointsTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MinimalEndpointsTest.java similarity index 94% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MinimalEndpointsTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MinimalEndpointsTest.java index e524432..75c63b0 100644 --- a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MinimalEndpointsTest.java +++ b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/MinimalEndpointsTest.java @@ -109,8 +109,6 @@ private String decode(String xml) { String d = xml.replace("<", "<").replace(">", ">") .replace(""", "\"").replace("'", "'") .replace("&", "&"); - // Collapse the pretty-printed JSON body's newlines+indentation only; leaves genuine inline - // spaces between XML attributes untouched return d.replaceAll("\\r?\\n\\s*", ""); } @@ -169,14 +167,11 @@ void errorRequiredVariant_omitsOnlyTheTargetedPropertyFromTheBody() throws Excep false, null, false, false, false, false, false, true, true, null); String decoded = decode(soapUIProject.getFileContent()); - // CaseOkAllProperties' body has all three properties assertTrue(decoded.contains("\"name\": \"\""), decoded); assertTrue(decoded.contains("\"age\": 0"), decoded); int errorRequiredAgeStart = decoded.indexOf("name=\"ErrorRequiredage\""); assertTrue(errorRequiredAgeStart >= 0, decoded); - // The inner {json} tag holds the body; stop at its closing tag so the - // block doesn't spill into the next sibling ... element int bodyCloseTag = decoded.indexOf("", errorRequiredAgeStart); assertTrue(bodyCloseTag > errorRequiredAgeStart, decoded); String errorRequiredAgeBlock = decoded.substring(errorRequiredAgeStart, bodyCloseTag); diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/SchemaIsInlineTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/SchemaIsInlineTest.java similarity index 100% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/model/SchemaIsInlineTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/SchemaIsInlineTest.java diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/SchemaPrettyPrintTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/SchemaPrettyPrintTest.java similarity index 100% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/model/SchemaPrettyPrintTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/SchemaPrettyPrintTest.java diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServerPatternTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServerPatternTest.java similarity index 94% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServerPatternTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServerPatternTest.java index df29e05..2d8e321 100644 --- a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServerPatternTest.java +++ b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServerPatternTest.java @@ -6,10 +6,6 @@ import org.apiaddicts.apitools.openapi2soapui.util.SerializedDataUtils; import org.junit.jupiter.api.Test; -/** - * When no pattern is given, serverPattern should default to only the first declared server, not every - * declared server - */ class ServerPatternTest { private static final String MULTI_SERVER_SPEC = String.join("\n", diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionCompositionTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionCompositionTest.java similarity index 100% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionCompositionTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionCompositionTest.java diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionTest.java similarity index 92% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionTest.java index e456930..9a4c93b 100644 --- a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionTest.java +++ b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ServiceApiConventionTest.java @@ -112,25 +112,25 @@ private String decode(String xml) { private SoapUIProject buildProject(OpenAPI openAPI) throws Exception { return new SoapUIProject( - "TestApi", // apiName - openAPI, // openAPI - null, // oAuth2Profiles - null, // headers - null, // testCaseNames - false, // readOnly - null, // serverPattern - false, // minimalEndpoints - false, // microcksHeaders - false, // generateOneOfAnyOf - false, // validateSchema - false, // schemaIsInline - false, // isInline - true, // schemaPrettyPrint - false, // hasScopes - false, // applicationToken - null, // numberOfScopes - null, // examples - null // customAuthorizationsFile + "TestApi", + openAPI, + null, + null, + null, + false, + null, + false, + false, + false, + false, + false, + false, + true, + false, + false, + null, + null, + null ); } diff --git a/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/Swagger2SpecTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/Swagger2SpecTest.java new file mode 100644 index 0000000..56ac629 --- /dev/null +++ b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/Swagger2SpecTest.java @@ -0,0 +1,101 @@ +package org.apiaddicts.apitools.openapi2soapui.model; + +import io.swagger.v3.oas.models.OpenAPI; + +import org.apiaddicts.apitools.openapi2soapui.util.SerializedDataUtils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class Swagger2SpecTest { + + private static final String SWAGGER_2_SPEC = """ + swagger: "2.0" + info: + title: Legacy Petstore + version: "1.0.0" + host: petstore.swagger.io + basePath: /v2 + schemes: [https] + paths: + /pet/{petId}: + get: + operationId: getPetById + produces: [application/json] + parameters: + - name: petId + in: path + required: true + type: integer + format: int64 + responses: + 200: + description: ok + schema: + type: object + properties: + id: { type: integer, format: int64 } + name: { type: string } + """; + + private static final String OPENAPI_3_SPEC = """ + openapi: 3.0.0 + info: + title: Legacy Petstore + version: "1.0.0" + servers: + - url: https://petstore.swagger.io/v2 + paths: + /pet/{petId}: + get: + operationId: getPetById + parameters: + - name: petId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: ok + content: + application/json: + schema: + type: object + properties: + id: { type: integer, format: int64 } + name: { type: string } + """; + + @Test + void readsSwagger2SpecAndConvertsItToOpenAPI3() { + OpenAPI openAPI = SerializedDataUtils.parseOpenAPIContent(SWAGGER_2_SPEC); + + assertNotNull(openAPI, "a Swagger 2.0 spec must be readable"); + assertEquals("1.0.0", openAPI.getInfo().getVersion()); + assertEquals("https://petstore.swagger.io/v2", openAPI.getServers().get(0).getUrl()); + assertTrue(openAPI.getPaths().containsKey("/pet/{petId}"), "the declared path is missing"); + } + + @Test + void swagger2AndOpenApi3SpecsOfTheSameApiGenerateTheSameProject() throws Exception { + String fromV2 = generate(SWAGGER_2_SPEC); + String fromV3 = generate(OPENAPI_3_SPEC); + + assertEquals(fromV3, fromV2, "the v2 spec must generate the same project as its v3 equivalent"); + } + + private String generate(String spec) throws Exception { + OpenAPI openAPI = SerializedDataUtils.parseOpenAPIContent(spec); + SoapUIProject project = new SoapUIProject("Legacy", openAPI, null, null, null, + false, null, false, false, false, false, false, false, null); + try { + return project.getFileContent().replaceAll(" id=\"[^\"]*\"", ""); + } finally { + project.deleteTemporaryFile(); + } + } +} diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ValidateSchemaScriptTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ValidateSchemaScriptTest.java similarity index 95% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ValidateSchemaScriptTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ValidateSchemaScriptTest.java index a5545e8..3ab8e31 100644 --- a/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ValidateSchemaScriptTest.java +++ b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/model/ValidateSchemaScriptTest.java @@ -13,11 +13,6 @@ import io.swagger.v3.oas.models.OpenAPI; import org.apiaddicts.apitools.openapi2soapui.util.SerializedDataUtils; -/** - * Throwaway end-to-end verification for the validateSchema Groovy script: builds a real SoapUIProject, - * extracts the generated Script Assertion text from the actual serialized XML, and executes it with - * GroovyShell against valid and invalid sample response bodies - */ class ValidateSchemaScriptTest { private static final String SPEC = String.join("\n", diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java b/openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java similarity index 100% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java rename to openapi2soapui-core/src/test/java/org/apiaddicts/apitools/openapi2soapui/util/QueryParamExampleUtilsTest.java diff --git a/openapi2soapui-core/src/test/resources/soapui-test-log4j.xml b/openapi2soapui-core/src/test/resources/soapui-test-log4j.xml new file mode 100644 index 0000000..9f16fba --- /dev/null +++ b/openapi2soapui-core/src/test/resources/soapui-test-log4j.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/openapi2soapui-rest/pom.xml b/openapi2soapui-rest/pom.xml new file mode 100644 index 0000000..f54ade1 --- /dev/null +++ b/openapi2soapui-rest/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + net.cloudappi + openapi2soapui + 2.1.0 + ../pom.xml + + + openapi2soapui-rest + ${packaging.type} + + openapi2soapui-rest + Openapi to SoapUI Project - HTTP service + + + openapi2soapui + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + net.cloudappi + openapi2soapui-core + ${project.version} + + + + org.springframework.boot + spring-boot-starter-web + + + org.apache.logging.log4j + log4j-to-slf4j + + + + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-validation + + + org.apache.logging.log4j + log4j-to-slf4j + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.vaadin.external.google + android-json + + + org.apache.logging.log4j + log4j-to-slf4j + + + + + + org.hibernate.validator + hibernate-validator + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + + + diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/Openapi2SoapUIApplication.java b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/Openapi2SoapUIApplication.java similarity index 100% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/Openapi2SoapUIApplication.java rename to openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/Openapi2SoapUIApplication.java diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/config/MessageSourceConfig.java b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/config/MessageSourceConfig.java similarity index 81% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/config/MessageSourceConfig.java rename to openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/config/MessageSourceConfig.java index 66562c8..d7debee 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/config/MessageSourceConfig.java +++ b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/config/MessageSourceConfig.java @@ -6,14 +6,10 @@ import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -/** - * Application message source configuration - */ @Configuration public class MessageSourceConfig { - - /** - * Setting messages.properties as a message source + + /** * @return messageSource */ @Bean @@ -23,9 +19,8 @@ public MessageSource messageSource() { messageSource.setDefaultEncoding("UTF-8"); return messageSource; } - - /** - * Creating local validator bean and setting message.properties as its validation message source + + /** * @return bean */ @Bean diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java similarity index 100% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java rename to openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectController.java diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/Error.java b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/Error.java similarity index 100% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/Error.java rename to openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/Error.java diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/ObjectErrorTypes.java b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/ObjectErrorTypes.java similarity index 100% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/ObjectErrorTypes.java rename to openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/ObjectErrorTypes.java diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/Result.java b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/Result.java similarity index 97% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/Result.java rename to openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/Result.java index 601eaf1..bf32b06 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/Result.java +++ b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/Result.java @@ -13,9 +13,6 @@ import java.util.ArrayList; import java.util.List; -/** - * Custom error response format - */ @Getter @Setter @JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.CUSTOM, property = "error", visible = true) diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/ValidationError.java b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/ValidationError.java similarity index 99% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/ValidationError.java rename to openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/ValidationError.java index f9e2ec7..971d1e7 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/ValidationError.java +++ b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/ValidationError.java @@ -9,7 +9,7 @@ public class ValidationError implements Error { private int errorCode; private String message; private String solution; - + public ValidationError(String message, String solution) { setCodeAndMessage(message); this.solution = solution; @@ -18,7 +18,7 @@ public ValidationError(String message, String solution) { public ValidationError(String message) { setCodeAndMessage(message); } - + private void setCodeAndMessage(String message) { String[] messageParts = message.split("\\|"); if (messageParts.length > 1) { diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/WebControllerAdvice.java b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/WebControllerAdvice.java similarity index 93% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/error/WebControllerAdvice.java rename to openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/WebControllerAdvice.java index 4bd28d4..cf4917a 100644 --- a/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/WebControllerAdvice.java +++ b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/error/WebControllerAdvice.java @@ -32,17 +32,13 @@ import static org.apiaddicts.apitools.openapi2soapui.error.ObjectErrorTypes.BAD_REQUEST; import static org.apiaddicts.apitools.openapi2soapui.error.ObjectErrorTypes.INTERNAL_SERVER_ERROR; -/** - * Exception handler - */ @Slf4j @ControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) public class WebControllerAdvice { private ResourceBundle resourceBundle = ResourceBundle.getBundle("messages"); - + /** - * Handle error when parsing Open API Spec content * @param ex Exception * @return custom error response */ @@ -59,7 +55,6 @@ public Result handleParseOpenAPIException(ParseOpenAPIException ex) { } /** - * Handle error when API version not found in Open API Spec * @param ex Exception * @return custom error response */ @@ -76,7 +71,6 @@ public Result handleAPIVersionNotFoundException(APIVersionNotFoundException ex) } /** - * Handle error when deserializing any property or reading the content of the request body * @param ex Exception * @return custom error response */ @@ -104,7 +98,6 @@ public Result handleHttpMessageNotReadableException(HttpMessageNotReadableExcept } /** - * Handle error when input does not match expected * @param ex Exception * @return custom error response */ @@ -133,7 +126,6 @@ private Result handleMismatchedInputException(MismatchedInputException mostSpeci } /** - * List specific property errors when value of property input does not match expected * @param ex Exception * @return custom error response */ @@ -148,7 +140,6 @@ private List getMismatchedInputErrors(String field, String targetTy } /** - * Add default error to error array in response object * @return error array */ private List getDefaultBadRequestErrors() { @@ -156,7 +147,6 @@ private List getDefaultBadRequestErrors() { } /** - * Handle error when validation of a property by constraint validator is not successful * @param ex Exception * @return custom error response */ @@ -175,7 +165,6 @@ public Result handleException(MethodArgumentNotValidException ex) { } /** - * Handle unexpected errors * @param ex Exception * @return custom error response */ @@ -192,7 +181,6 @@ public Result handleException(Exception ex) { } /** - * Handle error when Open API Spec is empty * @param ex Exception * @return custom error response */ @@ -204,9 +192,8 @@ private Result handleSwaggerContentEmptyException(SwaggerContentEmptyException e result.addValidationError(errors); return result; } - + /** - * Handle when Open API Spec has content errors * @param ex Exception * @return custom error response */ @@ -217,11 +204,10 @@ private Result handleSwaggerContentException(RuntimeException ex) { List errors = Collections.singletonList(new ObjectError(BAD_REQUEST, resourceBundle.getString("validation.content.openApiSpec"))); result.addValidationError(errors); return result; - + } /** - * Handle error when creating enum instance * @param ex Exception * @return custom error response */ @@ -229,7 +215,7 @@ private Result handleCreateEnumInstanceException(CreateEnumInstanceException ex) log.warn("CreateEnumInstanceException", ex); Result result = new Result(); result.setResponseCode(0); - + ObjectError error; if (ex.getErrorType().equals(ErrorType.NOT_NULL)) { error = new ObjectError(BAD_REQUEST, diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java similarity index 100% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java rename to openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectService.java diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java similarity index 100% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java rename to openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/service/SoapUIProjectServiceImpl.java diff --git a/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/LowerCaseClassNameResolver.java b/openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/LowerCaseClassNameResolver.java similarity index 100% rename from src/main/java/org/apiaddicts/apitools/openapi2soapui/util/LowerCaseClassNameResolver.java rename to openapi2soapui-rest/src/main/java/org/apiaddicts/apitools/openapi2soapui/util/LowerCaseClassNameResolver.java diff --git a/openapi2soapui-rest/src/main/resources/application.properties b/openapi2soapui-rest/src/main/resources/application.properties new file mode 100644 index 0000000..a640552 --- /dev/null +++ b/openapi2soapui-rest/src/main/resources/application.properties @@ -0,0 +1,4 @@ +basepath=/api-openapi-to-soapui/v1 +springdoc.swagger-ui.url=/api.yaml +springdoc.swagger-ui.path=${basepath}/swagger-ui.html +springdoc.override-with-generic-response=false \ No newline at end of file diff --git a/src/main/resources/banner.txt b/openapi2soapui-rest/src/main/resources/banner.txt similarity index 100% rename from src/main/resources/banner.txt rename to openapi2soapui-rest/src/main/resources/banner.txt diff --git a/src/main/resources/log4j.properties b/openapi2soapui-rest/src/main/resources/log4j.properties similarity index 100% rename from src/main/resources/log4j.properties rename to openapi2soapui-rest/src/main/resources/log4j.properties diff --git a/src/main/resources/static/api.yaml b/openapi2soapui-rest/src/main/resources/static/api.yaml similarity index 100% rename from src/main/resources/static/api.yaml rename to openapi2soapui-rest/src/main/resources/static/api.yaml diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/Openapi2soapuiApplicationTests.java b/openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/Openapi2soapuiApplicationTests.java similarity index 100% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/Openapi2soapuiApplicationTests.java rename to openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/Openapi2soapuiApplicationTests.java diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerApplicationTokenTest.java b/openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerApplicationTokenTest.java similarity index 100% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerApplicationTokenTest.java rename to openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerApplicationTokenTest.java diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerCustomAuthorizationsFileTest.java b/openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerCustomAuthorizationsFileTest.java similarity index 100% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerCustomAuthorizationsFileTest.java rename to openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerCustomAuthorizationsFileTest.java diff --git a/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerHasScopesTest.java b/openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerHasScopesTest.java similarity index 100% rename from src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerHasScopesTest.java rename to openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerHasScopesTest.java diff --git a/pom.xml b/pom.xml index c8e267d..8ffa767 100644 --- a/pom.xml +++ b/pom.xml @@ -10,8 +10,8 @@ net.cloudappi openapi2soapui - 2.0.0 - ${packaging.type} + 2.1.0 + pom openapi2soapui Openapi to SoapUI Project @@ -35,15 +35,12 @@ https://github.com/apiaddicts/openapi2soapui/tree/master - - openapi2soapui - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + openapi2soapui-core + openapi2soapui-rest + openapi2soapui-cli + UTF-8 @@ -55,8 +52,35 @@ 2.1.45 20260522 1.18.46 + 1.0.76 + 1.6.16 + + + + io.swagger + swagger-parser + ${swagger-v2-parser.version} + + + io.swagger + swagger-core + ${swagger-v2-core.version} + + + io.swagger + swagger-models + ${swagger-v2-core.version} + + + io.swagger + swagger-annotations + ${swagger-v2-core.version} + + + + jar @@ -93,350 +117,6 @@ - - - - org.springframework.boot - spring-boot-starter-web - - - - org.apache.logging.log4j - log4j-to-slf4j - - - - - - org.springframework.boot - spring-boot-starter-tomcat - provided - - - - org.projectlombok - lombok - provided - - - - org.springframework.boot - spring-boot-starter-validation - - - - org.apache.logging.log4j - log4j-to-slf4j - - - - - - org.springframework.boot - spring-boot-starter-test - test - - - com.vaadin.external.google - android-json - - - - org.apache.logging.log4j - log4j-to-slf4j - - - - - - org.hibernate.validator - hibernate-validator - - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - ${springdoc.version} - - - - com.smartbear.soapui - soapui - ${soapui.version} - - - javax.xml.bind - jsr173_api - - - - com.smartbear.utils.analytics - analytics-core - - - com.smartbear.utils.analytics - out-app-analytics-provider - - - - com.graphql-java - graphql-java - - - commons-collections - commons-collections - - - commons-collections - commons-lang - - - commons-collections - commons-io - - - org.slf4j - slf4j-log4j12 - - - com.jgoodies - forms - - - com.jgoodies - looks - - - com.jgoodies - binding - - - org.openjfx - javafx-base - - - org.openjfx - javafx-controls - - - org.openjfx - javafx-graphics - - - org.openjfx - javafx-media - - - org.openjfx - javafx-web - - - org.openjfx - javafx-swing - - - jetty - jetty - - - jetty - jetty-util - - - jetty - servlet-api - - - org.apache.ws.security - wss4j - - - net.sourceforge.htmlunit - htmlunit - - - org.w3c.css - sac - - - org.apache.httpcomponents - httpclient - - - org.apache.httpcomponents - httpmime - - - org.apache.httpcomponents - httpclient-cache - - - org.apache.httpcomponents - httpcore - - - org.apache.httpcomponents - httpcore-nio - - - org.apache.oltu.oauth2 - org.apache.oltu.oauth2.client - - - org.apache.oltu.oauth2 - org.apache.oltu.oauth2.httpclient4 - - - com.google.oauth-client - google-oauth-client - - - org.apache.maven - maven-plugin-api - - - javax.activation - activation - - - javax.mail - mail - - - com.narupley - not-going-to-be-commons-ssl - - - swingx - swingx - - - rhino - js - - - bouncycastle - bcprov - - - jtidy - jtidy - - - hermesjms - hermes - - - net.sourceforge.cssparser - cssparser - - - net.sourceforge.nekohtml - nekohtml - - - org.samba.jcifs - jcifs - - - l2fprod - l2fprod-common-directorychooser - - - l2fprod - l2fprod-common-fontchooser - - - org.apache.ws.commons.util - ws-commons-util - - - commons-httpclient - commons-httpclient - - - org.sonatype.install4j - i4jruntime - - - org.codehaus.mojo - animal-sniffer-annotations - - - ezmorph - ezmorph - - - xom - xom - - - commons-codec - commons-codec - - - saxon - saxon - - - saxon - saxon-dom - - - xmlunit - xmlunit - - - javax.jms - jms - - - thoughtworks - xstream - - - org.apache.santuario - xmlsec - - - org.apache.xerces - xml-apis - - - - - - io.swagger.parser.v3 - swagger-parser - ${swagger-parser.version} - - - - org.yaml - snakeyaml - - - - - org.json - json - ${json.version} - - - - SmartBearPluginRepository diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 3af5a79..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,8 +0,0 @@ -basepath=/api-openapi-to-soapui/v1 -springdoc.swagger-ui.url=/api.yaml -springdoc.swagger-ui.path=${basepath}/swagger-ui.html - -# The service publishes its own curated OpenAPI contract as the static /api.yaml (see springdoc.swagger-ui.url). -# Do NOT attach @ControllerAdvice-derived generic responses to operations: under springdoc 2.x that builds a -# schema from Spring's ObjectError which serializes with a null map key, making /v3/api-docs fail with HTTP 500. -springdoc.override-with-generic-response=false \ No newline at end of file From 2d6fd3587d1bb6b5b97aeb3deced951f8edb310e Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Wed, 26 Aug 2026 19:02:46 -0500 Subject: [PATCH 2/6] feat: update openapi2soapui CLI options --- .../skills/generar-proyecto-soapui/SKILL.md | 48 ++----------------- CHANGELOG.md | 26 ++++------ README.md | 6 +-- openapi2soapui-cli/pom.xml | 2 +- .../apitools/openapi2soapui/cli/CliArgs.java | 13 ++--- .../openapi2soapui/cli/Openapi2SoapUICli.java | 5 +- .../openapi2soapui/cli/SoapUILogging.java | 10 ++-- .../cli/Openapi2SoapUICliTest.java | 35 ++++++++++++++ openapi2soapui-core/pom.xml | 2 +- openapi2soapui-rest/pom.xml | 2 +- pom.xml | 2 +- 11 files changed, 67 insertions(+), 84 deletions(-) diff --git a/.claude/skills/generar-proyecto-soapui/SKILL.md b/.claude/skills/generar-proyecto-soapui/SKILL.md index 9b801e8..9c66133 100644 --- a/.claude/skills/generar-proyecto-soapui/SKILL.md +++ b/.claude/skills/generar-proyecto-soapui/SKILL.md @@ -1,51 +1,13 @@ --- name: generar-proyecto-soapui -description: Enseña cómo generar un proyecto SoapUI en XML a partir de un spec OpenAPI con este repo (openapi2soapui), por sus dos vías — la API HTTP propia y el CLI (`openapi2soapui-cli.jar`) — incluyendo el contrato completo del request (parámetros, defaults, validaciones) y su equivalencia en flags. Úsala cuando el usuario pida generar el proyecto/colección SoapUI con la API o por línea de comandos, llamar al endpoint de openapi2soapui, crear la colección vía API/CLI, o necesite saber qué parámetros/configuración acepta la generación (oAuth2Profiles, headers, customAuthorizationsFile, testCaseNames, flags como readOnly/hasScopes/validateSchema, etc.), incluso si no menciona el nombre exacto del endpoint. NO cubre ejecutar las pruebas generadas con SoapUI TestRunner ni levantar el servicio con Docker — para eso no uses esta skill. +description: Enseña cómo llamar la API propia de este repo (openapi2soapui) para generar un proyecto SoapUI en XML a partir de un spec OpenAPI, incluyendo el contrato completo del request (parámetros, defaults, validaciones). Úsala cuando el usuario pida generar el proyecto/colección SoapUI con la API, llamar al endpoint de openapi2soapui, crear la colección vía API, o necesite saber qué parámetros/configuración acepta la generación (oAuth2Profiles, headers, customAuthorizationsFile, testCaseNames, flags como readOnly/hasScopes/validateSchema, etc.), incluso si no menciona el nombre exacto del endpoint. NO cubre ejecutar las pruebas generadas con SoapUI TestRunner ni levantar el servicio con Docker — para eso no uses esta skill. --- -# Generar proyecto SoapUI con openapi2soapui +# Generar proyecto SoapUI vía API de openapi2soapui -Esta skill cubre **solo** cómo generar un proyecto SoapUI a partir de un spec OpenAPI con este repo, y qué configuración acepta. No cubre ejecutar el proyecto generado (SoapUI TestRunner) ni levantar el servicio (Docker/Maven) — si el usuario pide eso, es trabajo aparte. +Esta skill cubre **solo** cómo llamar el endpoint de este repo que genera un proyecto SoapUI a partir de un spec OpenAPI, y qué configuración acepta. No cubre ejecutar el proyecto generado (SoapUI TestRunner) ni levantar el servicio (Docker/Maven) — si el usuario pide eso, es trabajo aparte. -## Elegir la vía: CLI o API - -Hay dos front ends sobre el mismo motor. Generan **XML idéntico** con los mismos defaults y las mismas validaciones, así que la tabla de parámetros de más abajo aplica a ambos. - -| | CLI (`openapi2soapui-cli.jar`) | API HTTP | -|---|---|---| -| Requisitos | El jar y un JRE 21. Nada corriendo | Servicio levantado + URL base confirmada | -| Spec | Fichero plano JSON/YAML (`-f`) | Base64 dentro del JSON body | -| Salida | Fichero `.xml` en disco | Body de la respuesta | - -**Si el servicio no está levantado y confirmado, prefiere el CLI**: evita el paso 0, el base64 y el manejo de la respuesta HTTP. Usa la API cuando el usuario ya la tiene corriendo o pide explícitamente el endpoint. - -### Vía CLI - -Construir el jar (si no existe): `mvn clean package -DskipTests` → `openapi2soapui-cli/target/openapi2soapui-cli.jar`. - -```bash -# spec plano + flags, salida en ./output/{apiName}_{apiVersion}-soapui-project.xml -java -jar openapi2soapui-cli.jar -f archivo.yaml -n MiApi -o ./output - -# configuración completa: el MISMO JSON body que la API (openApiSpec en base64) -java -jar openapi2soapui-cli.jar -c request.json -o proyecto-soapui.xml - -# config para lo anidado + spec como fichero plano -java -jar openapi2soapui-cli.jar -c request.json -f archivo.yaml -``` - -Equivalencias con la tabla de parámetros: - -- `apiName` → `-n/--api-name` (si no se da, el CLI lo deriva del `info.title` del spec; la API sí lo exige). -- `openApiSpec` → `-f/--file` (texto plano, sin base64). -- `headers` → `-H/--header "clave:valor"`, repetible. `testCaseNames` → `--test-case-names a,b`. `serverPattern` → `--server-pattern`. `numberOfScopes` → `--number-of-scopes`. -- Flags booleanos: `--read-only`, `--minimal-endpoints`, `--microcks-headers`, `--generate-one-of-any-of`, `--schema-is-inline`, `--is-inline`, `--has-scopes`, `--application-token`. Los dos que vienen activados por defecto se apagan con `--no-validate-schema` y `--no-schema-pretty-print`. -- `oAuth2Profiles`, `customAuthorizationsFile` y `examples` son objetos anidados: **solo por `-c`**, con la misma forma documentada abajo. -- Exit codes: `0` ok, `1` error de generación/validación, `2` error de uso. Los errores salen por stderr con los mismos códigos de la tabla de diagnóstico (`-v` para la traza completa). - -Para la vía API, sigue con el paso 0. - -## Paso 0 — obtener la URL base (obligatorio, solo vía API) +## Paso 0 — obtener la URL base (obligatorio) La URL base del servicio (host:puerto, ej. `http://localhost:8080`) **nunca se asume**. Si no está ya confirmada en la conversación actual, pregúntala al usuario antes de construir cualquier request. No uses `localhost:8080` por defecto sin que el usuario lo confirme — puede estar corriendo en otro puerto, en Docker con otro mapeo, o en un host remoto. @@ -203,7 +165,7 @@ Cada entrada define un request de bootstrap de autenticación (ej. un fetch de t ## Nota sobre el spec propio publicado -El propio `api.yaml` del servicio (`openapi2soapui-rest/src/main/resources/static/api.yaml`) tiene un typo conocido en el discriminator `oneOf` de `OAuth2ProfileToGetToken`: el mapping entre `IMPLICIT` y `RESOURCE_OWNER_PASSWORD_CREDENTIALS` está invertido. No afecta la validación real (que corre en Java vía `AuthenticationConditionalValidator`), solo es ruido en esa documentación — no te confundas si lo comparás contra ese YAML. +El propio `api.yaml` del servicio (`src/main/resources/static/api.yaml`) tiene un typo conocido en el discriminator `oneOf` de `OAuth2ProfileToGetToken`: el mapping entre `IMPLICIT` y `RESOURCE_OWNER_PASSWORD_CREDENTIALS` está invertido. No afecta la validación real (que corre en Java vía `AuthenticationConditionalValidator`), solo es ruido en esa documentación — no te confundas si lo comparás contra ese YAML. ## Fuera de alcance diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f6b5b6..d0b3369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,26 +6,20 @@ 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). -## [2.1.0] - 2026-08-26 - -### Fixed -- **OpenAPI/Swagger 2.0 specs can be read again.** Since the Java 21 / Spring Boot 3.5.16 upgrade in 2.0.0, every v2 spec failed (HTTP 500 from the endpoint) with `NoSuchMethodError`, even though v2 has always been advertised as supported. swagger-parser's v2 converter delegates to the Swagger 1.x stack, and SoapUI drags in an older copy of it (`swagger-inflector` 1.0.19 → `swagger-parser` 1.0.54, `swagger-core` 1.6.2) which Maven's nearest-wins resolution preferred; those releases are built against snakeyaml 1.x, removed in the snakeyaml 2.4 that Spring Boot manages. The parent pom now pins that stack to the versions `swagger-compat-spec-parser` 1.0.76 declares (`swagger-parser` 1.0.76, `swagger-core`/`swagger-models`/`swagger-annotations` 1.6.16), which target snakeyaml 2.4. A v2 spec and its v3 equivalent now generate byte for byte the same project; `Swagger2SpecTest` guards both facts. Generation from v3 specs is unaffected. +## [2.1.0-beta-1] - 2026-08-26 ### Added -- **Command line interface.** `openapi2soapui-cli.jar` generates a SoapUI project from an OpenAPI spec without starting the service: `java -jar openapi2soapui-cli.jar -f petstore.yaml -o ./output`. It reuses the same engine, the same request model and the same bean validation constraints as the HTTP endpoint, so both produce identical projects (verified against `demo/petstore-ok-only-run`). - - `-f`/`--file` takes the spec as plain JSON or YAML; `-c`/`--config` takes the very same JSON body the REST API accepts, `openApiSpec` base64 encoded included, so existing request files work unchanged. When both are given, `-f` provides the spec. - - Every scalar parameter has a flag (`--read-only`, `--minimal-endpoints`, `--microcks-headers`, `--generate-one-of-any-of`, `--schema-is-inline`, `--is-inline`, `--has-scopes`, `--application-token`, `--number-of-scopes`, `--server-pattern`, `--test-case-names`, `-H`/`--header`, `--no-validate-schema`, `--no-schema-pretty-print`) and overrides the config file. `oAuth2Profiles`, `customAuthorizationsFile` and `examples` are nested objects, reachable only through `-c`. - - Defaults are identical to the HTTP API. The only difference is `apiName`: required by the API, derived from the spec title (or the spec file name) by the CLI when neither `-n` nor the config provide one. - - Output defaults to `./output/{apiName}_{apiVersion}-soapui-project.xml`; an `-o` value ending in `.xml` is taken as the exact file. Exit codes: `0` success, `1` generation or validation error, `2` usage error. Errors and SoapUI logs go to stderr, leaving stdout with just the result line. - - SoapUI's bundled log4j2 configuration is replaced at runtime (via the `soapui.log4j.config` property), so a run no longer prints DEBUG on stdout nor writes `soapui.log`, `soapui-errors.log` and `global-groovy.log` under `${user.home}/.soapuios/logs`. +- **Command line interface** (`openapi2soapui-cli.jar`): generates a SoapUI project without starting the service, `java -jar openapi2soapui-cli.jar -f petstore.yaml -o ./out`. Uses the same engine, request model and validations as the endpoint, so both produce identical projects. +- `-f` takes the spec as plain JSON/YAML and `-c` the same JSON body the REST API accepts (`openApiSpec` in base64), so existing request files work unchanged. Every scalar parameter has a flag; `oAuth2Profiles`, `customAuthorizationsFile` and `examples` are reachable only through `-c`. Defaults match the API, except `apiName`, derived from the spec title when not given. +- Exit codes `0` success, `1` generation or validation error, `2` usage error. SoapUI's DEBUG output and its log files are silenced, so a run prints one line on stdout and nothing on stderr. + +### Fixed +- **OpenAPI/Swagger 2.0 specs can be read again**, broken since 2.0.0 with `NoSuchMethodError` (HTTP 500 from the endpoint). SoapUI drags in an older Swagger 1.x stack built against snakeyaml 1.x and Maven preferred it; the parent pom now pins that stack to the versions `swagger-compat-spec-parser` 1.0.76 declares. Generation from v3 specs is unaffected. ### Changed -- **Split into a Maven multi module build**: `openapi2soapui-core` (conversion engine and request model, free of Spring and of any web dependency), `openapi2soapui-rest` (the HTTP service) and `openapi2soapui-cli`. Java packages are unchanged, so no import in the existing code was touched. - - The service artifact is still `openapi2soapui.war` (or `openapi2soapui.jar` with `-Pjar`), now under `openapi2soapui-rest/target/`. The `war`, `jar`, `INTE`, `TEST` and `PROD` profiles behave as before. **Its Maven coordinates change from `net.cloudappi:openapi2soapui` to `net.cloudappi:openapi2soapui-rest`**, and `docker-compose.yml` now points `JAR_FILE` at the new path. - - Running the service from the reactor now needs the module: `mvn -pl openapi2soapui-rest -am spring-boot:run`. - - `messages.properties` moved to the core module so the HTTP service and the CLI report the same validation messages and codes. - - `AuthenticationConditionalValidator` no longer uses Spring's `ObjectUtils.isEmpty`, keeping the core module free of Spring. Behaviour is unchanged, blank but non empty values are still not considered empty. - - The engine tests now run with a quiet SoapUI log4j2 configuration instead of flooding the build log with DEBUG. +- **Split into a Maven multi module build**: `openapi2soapui-core` (conversion engine, free of Spring and of any web dependency), `openapi2soapui-rest` (the HTTP service) and `openapi2soapui-cli`. Java packages are unchanged. +- The service artifact is still `openapi2soapui.war`, or `openapi2soapui.jar` with `-Pjar`, now under `openapi2soapui-rest/target/`. **Its Maven coordinates change from `net.cloudappi:openapi2soapui` to `net.cloudappi:openapi2soapui-rest`**, `docker-compose.yml` points at the new path, and the service runs from the reactor with `mvn -pl openapi2soapui-rest -am spring-boot:run`. +- `messages.properties` moved to the core module, so the service and the CLI report the same validation codes and texts. ## [2.0.0] - 2026-08-25 diff --git a/README.md b/README.md index 0ccf5eb..94d1835 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ $ mvn clean package -DskipTests * The jar is produced at `openapi2soapui-cli/target/openapi2soapui-cli.jar` ```shell -# generate from a spec file into ./output +# generate from a spec file into ./out $ java -jar openapi2soapui-cli.jar -f petstore.yaml # name the API and pick the exact output file @@ -235,10 +235,10 @@ Notes: * Defaults match the HTTP API exactly, including `validateSchema` and `schemaPrettyPrint` being enabled unless turned off with `--no-validate-schema` / `--no-schema-pretty-print`. `apiName` is the only difference: the API requires it, while the CLI derives it from the spec title when neither `-n` nor the config provide one. -* Output defaults to `./output/{apiName}_{apiVersion}-soapui-project.xml`. An `-o` value ending in `.xml` is +* Output defaults to `./out/{apiName}_{apiVersion}-soapui-project.xml`. An `-o` value ending in `.xml` is taken as the exact file, anything else as a folder. * Exit codes: `0` success, `1` generation or validation error, `2` usage error. Errors go to stderr, so stdout - only ever carries the result line. Add `-v` for stack traces and SoapUI logs. + only ever carries the result line. ## Files and Directories Structure diff --git a/openapi2soapui-cli/pom.xml b/openapi2soapui-cli/pom.xml index 3c6b05b..d555a70 100644 --- a/openapi2soapui-cli/pom.xml +++ b/openapi2soapui-cli/pom.xml @@ -5,7 +5,7 @@ net.cloudappi openapi2soapui - 2.1.0 + 2.1.0-beta-1 ../pom.xml diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java index f2f57db..e20ec18 100644 --- a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java @@ -19,7 +19,7 @@ final class CliArgs { - static final String DEFAULT_OUTPUT = "./output"; + static final String DEFAULT_OUTPUT = "./out"; private static final String OUTPUT_FILE_SUFFIX = "-soapui-project.xml"; @@ -43,7 +43,6 @@ final class CliArgs { private Boolean hasScopes; private Boolean applicationToken; private Integer numberOfScopes; - private boolean verbose; private boolean help; private boolean version; @@ -58,7 +57,6 @@ static CliArgs parse(String[] args) { switch (option) { case "-h", "--help" -> parsed.help = true; case "-V", "--version" -> parsed.version = true; - case "-v", "--verbose" -> parsed.verbose = true; case "-f", "--file" -> parsed.specFile = value(tokens, ++i, option); case "-c", "--config" -> parsed.configFile = value(tokens, ++i, option); case "-o", "--output" -> parsed.output = value(tokens, ++i, option); @@ -209,10 +207,6 @@ String getConfigFile() { return configFile; } - boolean isVerbose() { - return verbose; - } - boolean isHelp() { return help; } @@ -236,7 +230,7 @@ static String usage() { Output: -o, --output Folder, or a path ending in .xml for an exact file name - (default: ./output) + (default: ./out) Generation options, they override the config file: -n, --api-name apiName (default: the spec title, or the spec file name) @@ -256,14 +250,13 @@ static String usage() { --no-schema-pretty-print Serialize the schema compactly (pretty by default) Other: - -v, --verbose Print stack traces and SoapUI logs on stderr -h, --help Show this help -V, --version Show the version Exit codes: 0 success, 1 generation or validation error, 2 usage error Examples: - java -jar openapi2soapui-cli.jar -f petstore.yaml -o ./output + java -jar openapi2soapui-cli.jar -f petstore.yaml -o ./out java -jar openapi2soapui-cli.jar -f petstore.yaml -n Petstore --read-only java -jar openapi2soapui-cli.jar -c request.json -o petstore-project.xml java -jar openapi2soapui-cli.jar -c request.json -f petstore.yaml diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java index 057a684..de5ebfe 100644 --- a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java @@ -49,12 +49,11 @@ public int run(String[] args) { return usageError("no input given, pass -f and/or -c "); } - SoapUILogging.install(cli.isVerbose()); + SoapUILogging.install(); try { return generate(cli); } catch (Throwable t) { - if (cli.isVerbose()) t.printStackTrace(); return error(describe(t)); } } @@ -83,7 +82,7 @@ private int generate(CliArgs cli) throws Exception { return EXIT_ERROR; } - SoapUIProject project = SoapUILogging.withoutStdout(!cli.isVerbose(), + SoapUIProject project = SoapUILogging.withoutStdout( () -> new SoapUIProject(request.getApiName(), openAPI, request.getOAuth2Profiles(), request.getHeaders(), request.getTestCaseNames(), request.getReadOnly(), request.getServerPattern(), request.getMinimalEndpoints(), request.getMicrocksHeaders(), diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java index 0ecdcfa..47dadb7 100644 --- a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java @@ -23,9 +23,10 @@ final class SoapUILogging { private SoapUILogging() { } - static void install(boolean verbose) { - System.setProperty(LOG_LEVEL_PROPERTY, verbose ? "DEBUG" : "WARN"); - System.setProperty(PARSER_LOG_LEVEL_PROPERTY, verbose ? "DEBUG" : "ERROR"); + static void install() { + // -Dopenapi2soapui.cli.logLevel=DEBUG still works as an escape hatch for diagnosing a run + if (System.getProperty(LOG_LEVEL_PROPERTY) == null) System.setProperty(LOG_LEVEL_PROPERTY, "WARN"); + if (System.getProperty(PARSER_LOG_LEVEL_PROPERTY) == null) System.setProperty(PARSER_LOG_LEVEL_PROPERTY, "ERROR"); if (System.getProperty(LOG4J_CONFIG_PROPERTY) != null) return; @@ -40,8 +41,7 @@ static void install(boolean verbose) { } } - static T withoutStdout(boolean mute, Callable action) throws Exception { - if (!mute) return action.call(); + static T withoutStdout(Callable action) throws Exception { PrintStream original = System.out; System.setOut(new PrintStream(OutputStream.nullOutputStream(), true, StandardCharsets.UTF_8)); try { diff --git a/openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java b/openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java index 7f6a9d1..626424b 100644 --- a/openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java +++ b/openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java @@ -6,6 +6,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -21,6 +23,9 @@ class Openapi2SoapUICliTest { private static final String SPEC = "petstore.yaml"; private static final String CONFIG = "request.json"; + private static final Pattern SCHEMA_PROPERTY = + Pattern.compile("schema\\d+(.*?)", Pattern.DOTALL); + @TempDir Path outputDir; @@ -87,6 +92,26 @@ void appliesFlagsOverConfigFile() throws Exception { assertFalse(xml.contains("GroovyScriptAssertion"), "no-validate-schema should drop the schema assertion"); } + @Test + void validateSchemaAndSchemaPrettyPrintAreOnUnlessTheNoFlagIsGiven() throws Exception { + assertEquals(Openapi2SoapUICli.EXIT_OK, + run("-f", resource(SPEC), "-n", "Defaults", "-o", outputDir.toString()), stderr()); + assertEquals(Openapi2SoapUICli.EXIT_OK, + run("-f", resource(SPEC), "-n", "NoSchema", "--no-validate-schema", "-o", outputDir.toString()), stderr()); + assertEquals(Openapi2SoapUICli.EXIT_OK, + run("-f", resource(SPEC), "-n", "Compact", "--no-schema-pretty-print", "-o", outputDir.toString()), stderr()); + + String defaults = project("Defaults"); + assertTrue(defaults.contains("GroovyScriptAssertion"), + "validateSchema is on by default, the schema assertion must appear with no flag given"); + assertFalse(project("NoSchema").contains("GroovyScriptAssertion"), + "--no-validate-schema must be what removes the schema assertion, not its absence"); + assertTrue(schemaProperty(defaults).contains("\n"), + "schemaPrettyPrint is on by default, the stored schema must be indented with no flag given"); + assertFalse(schemaProperty(project("Compact")).contains("\n"), + "--no-schema-pretty-print must be what compacts the schema, not its absence"); + } + @Test void missingSpecFileIsAnError() { int exitCode = run("-f", outputDir.resolve("nope.yaml").toString(), "-o", outputDir.toString()); @@ -130,6 +155,16 @@ private String resource(String name) throws Exception { return Path.of(getClass().getClassLoader().getResource(name).toURI()).toString(); } + private String project(String apiName) throws Exception { + return Files.readString(outputDir.resolve(apiName + "_1.0.0-soapui-project.xml")); + } + + private static String schemaProperty(String xml) { + Matcher matcher = SCHEMA_PROPERTY.matcher(xml); + assertTrue(matcher.find(), "the response schema should be stored as a SoapUI project property"); + return matcher.group(1); + } + private String stdout() { return out.toString(StandardCharsets.UTF_8); } diff --git a/openapi2soapui-core/pom.xml b/openapi2soapui-core/pom.xml index ff31198..566e51c 100644 --- a/openapi2soapui-core/pom.xml +++ b/openapi2soapui-core/pom.xml @@ -5,7 +5,7 @@ net.cloudappi openapi2soapui - 2.1.0 + 2.1.0-beta-1 ../pom.xml diff --git a/openapi2soapui-rest/pom.xml b/openapi2soapui-rest/pom.xml index f54ade1..22bdcde 100644 --- a/openapi2soapui-rest/pom.xml +++ b/openapi2soapui-rest/pom.xml @@ -5,7 +5,7 @@ net.cloudappi openapi2soapui - 2.1.0 + 2.1.0-beta-1 ../pom.xml diff --git a/pom.xml b/pom.xml index 8ffa767..2778ff8 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ net.cloudappi openapi2soapui - 2.1.0 + 2.1.0-beta-1 pom openapi2soapui From 6548078a44eeaaf54e16270dc342e6e4000641bd Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Fri, 28 Aug 2026 08:17:50 -0500 Subject: [PATCH 3/6] fix: handle titles and spec files in the CLI --- .../apitools/openapi2soapui/cli/CliArgs.java | 30 ++-- .../cli/Openapi2SoapUICliTest.java | 134 ++++++++++++++++++ 2 files changed, 151 insertions(+), 13 deletions(-) diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java index e20ec18..cfa435d 100644 --- a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.CharacterCodingException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -23,7 +24,7 @@ final class CliArgs { private static final String OUTPUT_FILE_SUFFIX = "-soapui-project.xml"; - private static final Pattern UNSAFE_NAME_CHARS = Pattern.compile("[^A-Za-z0-9._-]+"); + private static final Pattern UNSAFE_NAME_CHARS = Pattern.compile("[^\\p{L}\\p{N}._-]+"); private String specFile; private String configFile; @@ -64,7 +65,7 @@ static CliArgs parse(String[] args) { case "-H", "--header" -> parsed.headers.add(header(value(tokens, ++i, option))); case "--server-pattern" -> parsed.serverPattern = value(tokens, ++i, option); case "--test-case-names" -> parsed.testCaseNames = testCaseNames(value(tokens, ++i, option)); - case "--number-of-scopes" -> parsed.numberOfScopes = integer(value(tokens, ++i, option), option); + case "--number-of-scopes" -> parsed.numberOfScopes = integer(rawValue(tokens, ++i, option), option); case "--read-only" -> parsed.readOnly = Boolean.TRUE; case "--minimal-endpoints" -> parsed.minimalEndpoints = Boolean.TRUE; case "--microcks-headers" -> parsed.microcksHeaders = Boolean.TRUE; @@ -96,14 +97,18 @@ private static List normalize(String[] args) { } private static String value(List tokens, int index, String option) { - if (index >= tokens.size()) throw new UsageException("option " + option + " requires a value"); - String value = tokens.get(index); + String value = rawValue(tokens, index, option); if (value.length() > 1 && value.startsWith("-")) { throw new UsageException("option " + option + " requires a value, found " + value); } return value; } + private static String rawValue(List tokens, int index, String option) { + if (index >= tokens.size()) throw new UsageException("option " + option + " requires a value"); + return tokens.get(index); + } + private static Header header(String value) { int separator = value.indexOf(':'); if (separator < 1 || separator == value.length() - 1) { @@ -160,7 +165,14 @@ private static SoapUIProjectRequest readConfig(String path) throws IOException { } private static String readSpec(String path) throws IOException { - return Files.readString(requireFile(path, "OpenAPI").toPath()); + String spec; + try { + spec = Files.readString(requireFile(path, "OpenAPI").toPath()); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException("OpenAPI file is not valid UTF-8: " + path); + } + if (spec.isBlank()) throw new IllegalArgumentException("OpenAPI file is empty: " + path); + return spec; } private static File requireFile(String path, String description) { @@ -252,14 +264,6 @@ static String usage() { Other: -h, --help Show this help -V, --version Show the version - - Exit codes: 0 success, 1 generation or validation error, 2 usage error - - Examples: - java -jar openapi2soapui-cli.jar -f petstore.yaml -o ./out - java -jar openapi2soapui-cli.jar -f petstore.yaml -n Petstore --read-only - java -jar openapi2soapui-cli.jar -c request.json -o petstore-project.xml - java -jar openapi2soapui-cli.jar -c request.json -f petstore.yaml """; } diff --git a/openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java b/openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java index 626424b..9c68135 100644 --- a/openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java +++ b/openapi2soapui-cli/src/test/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICliTest.java @@ -26,6 +26,33 @@ class Openapi2SoapUICliTest { private static final Pattern SCHEMA_PROPERTY = Pattern.compile("schema\\d+(.*?)", Pattern.DOTALL); + private static final String SWAGGER_2_SPEC = """ + swagger: "2.0" + info: + title: Legacy + version: "1.0.0" + host: api.example.com + basePath: /v1 + schemes: [https] + paths: + /ping: + get: + produces: [application/json] + responses: + 200: + description: ok + schema: + type: object + properties: + ok: { type: boolean } + """; + + private static final String OPENAPI_3_JSON_SPEC = """ + {"openapi":"3.0.0","info":{"title":"Json","version":"1.0.0"}, + "servers":[{"url":"https://api.example.com/v1"}], + "paths":{"/ping":{"get":{"responses":{"200":{"description":"ok"}}}}}} + """; + @TempDir Path outputDir; @@ -112,6 +139,107 @@ void validateSchemaAndSchemaPrettyPrintAreOnUnlessTheNoFlagIsGiven() throws Exce "--no-schema-pretty-print must be what compacts the schema, not its absence"); } + @Test + void readsASwagger2Spec() throws Exception { + Path spec = write("legacy.yaml", SWAGGER_2_SPEC); + + assertEquals(Openapi2SoapUICli.EXIT_OK, + run("-f", spec.toString(), "-n", "Legacy", "-o", outputDir.toString()), stderr()); + + String xml = project("Legacy"); + assertTrue(xml.contains("https://api.example.com"), "host + schemes should become the endpoint"); + assertTrue(xml.contains("/ping"), "the declared path is missing"); + } + + @Test + void readsASpecWrittenInJson() throws Exception { + Path spec = write("api.json", OPENAPI_3_JSON_SPEC); + + assertEquals(Openapi2SoapUICli.EXIT_OK, + run("-f", spec.toString(), "-n", "Json", "-o", outputDir.toString()), stderr()); + + assertTrue(project("Json").contains("/ping"), "the declared path is missing"); + } + + @Test + void keepsAccentsInTheDerivedApiName() throws Exception { + Path spec = write("acentos.yaml", SWAGGER_2_SPEC.replace("title: Legacy", "title: \"Gestión de Añadidos\"")); + + assertEquals(Openapi2SoapUICli.EXIT_OK, run("-f", spec.toString(), "-o", outputDir.toString()), stderr()); + + assertTrue(Files.exists(outputDir.resolve("GestióndeAñadidos_1.0.0-soapui-project.xml")), + "accents must survive, found " + Arrays.toString(outputDir.toFile().list())); + } + + @Test + void reportsAnEmptySpecFile() throws Exception { + Path spec = write("empty.yaml", ""); + + assertEquals(Openapi2SoapUICli.EXIT_ERROR, run("-f", spec.toString(), "-o", outputDir.toString())); + assertTrue(stderr().contains("OpenAPI file is empty"), "unexpected stderr: " + stderr()); + } + + @Test + void reportsASpecFileThatIsNotUtf8() throws Exception { + Path spec = outputDir.resolve("latin1.yaml"); + Files.write(spec, SWAGGER_2_SPEC.replace("Legacy", "Añadidos").getBytes(StandardCharsets.ISO_8859_1)); + + assertEquals(Openapi2SoapUICli.EXIT_ERROR, run("-f", spec.toString(), "-o", outputDir.toString())); + assertTrue(stderr().contains("not valid UTF-8"), "unexpected stderr: " + stderr()); + } + + @Test + void acceptsANegativeNumberOfScopes() throws Exception { + assertEquals(Openapi2SoapUICli.EXIT_OK, + run("-f", resource(SPEC), "-n", "Neg", "--number-of-scopes", "-1", "-o", outputDir.toString()), + stderr()); + } + + @Test + void acceptsTheOptionEqualsValueForm() throws Exception { + assertEquals(Openapi2SoapUICli.EXIT_OK, + run("--file=" + resource(SPEC), "--api-name=Equals", "--output=" + outputDir), stderr()); + + assertTrue(Files.exists(outputDir.resolve("Equals_1.0.0-soapui-project.xml")), + "expected the project, found " + Arrays.toString(outputDir.toFile().list())); + } + + @Test + void ignoresUnknownPropertiesInTheConfigFile() throws Exception { + Path config = write("config.json", "{\"apiName\":\"Extra\",\"inventado\":true,\"otro\":{\"a\":1}}"); + + assertEquals(Openapi2SoapUICli.EXIT_OK, + run("-c", config.toString(), "-f", resource(SPEC), "-o", outputDir.toString()), stderr()); + + assertTrue(project("Extra").contains("name=\"Extra_1.0.0\""), "the known properties must still apply"); + } + + @Test + void specFileReplacesTheSpecCarriedByTheConfig() throws Exception { + Path spec = write("legacy.yaml", SWAGGER_2_SPEC); + + assertEquals(Openapi2SoapUICli.EXIT_OK, + run("-c", resource(CONFIG), "-f", spec.toString(), "-o", outputDir.toString()), stderr()); + + String xml = project("Petstore"); + assertTrue(xml.contains("/ping"), "-f must provide the spec over the config's base64 openApiSpec"); + assertFalse(xml.contains("findByStatus"), "the config's own spec must not be used"); + } + + @Test + void mergesHeadersFromTheConfigAndTheCommandLine() throws Exception { + Path config = write("config.json", + "{\"apiName\":\"Merged\",\"headers\":[{\"key\":\"X-From-Config\",\"value\":\"1\"}]}"); + + assertEquals(Openapi2SoapUICli.EXIT_OK, + run("-c", config.toString(), "-f", resource(SPEC), "-H", "X-From-Flag:2", "-o", outputDir.toString()), + stderr()); + + String xml = project("Merged"); + assertTrue(xml.contains("X-From-Config"), "the config header is missing"); + assertTrue(xml.contains("X-From-Flag"), "the -H header is missing"); + } + @Test void missingSpecFileIsAnError() { int exitCode = run("-f", outputDir.resolve("nope.yaml").toString(), "-o", outputDir.toString()); @@ -159,6 +287,12 @@ private String project(String apiName) throws Exception { return Files.readString(outputDir.resolve(apiName + "_1.0.0-soapui-project.xml")); } + private Path write(String name, String content) throws Exception { + Path file = outputDir.resolve(name); + Files.writeString(file, content); + return file; + } + private static String schemaProperty(String xml) { Matcher matcher = SCHEMA_PROPERTY.matcher(xml); assertTrue(matcher.find(), "the response schema should be stored as a SoapUI project property"); From 7f64b494539c9881864cb2870b87712d164dfd06 Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Fri, 28 Aug 2026 09:57:52 -0500 Subject: [PATCH 4/6] feat: update readme with cli commands, and changelog --- CHANGELOG.md | 14 ++-- README.md | 138 +++++++++++++++++++++++++++--------- openapi2soapui-cli/pom.xml | 9 --- openapi2soapui-core/pom.xml | 3 - pom.xml | 2 +- 5 files changed, 111 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b3369..b4f599d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,20 +6,14 @@ 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). -## [2.1.0-beta-1] - 2026-08-26 +## [2.1.0-beta-1] - 2026-08-28 ### Added -- **Command line interface** (`openapi2soapui-cli.jar`): generates a SoapUI project without starting the service, `java -jar openapi2soapui-cli.jar -f petstore.yaml -o ./out`. Uses the same engine, request model and validations as the endpoint, so both produce identical projects. -- `-f` takes the spec as plain JSON/YAML and `-c` the same JSON body the REST API accepts (`openApiSpec` in base64), so existing request files work unchanged. Every scalar parameter has a flag; `oAuth2Profiles`, `customAuthorizationsFile` and `examples` are reachable only through `-c`. Defaults match the API, except `apiName`, derived from the spec title when not given. -- Exit codes `0` success, `1` generation or validation error, `2` usage error. SoapUI's DEBUG output and its log files are silenced, so a run prints one line on stdout and nothing on stderr. - -### Fixed -- **OpenAPI/Swagger 2.0 specs can be read again**, broken since 2.0.0 with `NoSuchMethodError` (HTTP 500 from the endpoint). SoapUI drags in an older Swagger 1.x stack built against snakeyaml 1.x and Maven preferred it; the parent pom now pins that stack to the versions `swagger-compat-spec-parser` 1.0.76 declares. Generation from v3 specs is unaffected. +- **Command line interface** (`openapi2soapui-cli.jar`): generates a SoapUI project from an OpenAPI spec without starting the service, `java -jar openapi2soapui-cli.jar -f petstore.yaml`. Same engine, request model and validations as the HTTP endpoint, so both produce identical projects. +- Input with `-f` (spec as plain JSON or YAML) or `-c` (the same JSON body the REST API takes, `openApiSpec` base64 encoded), output with `-o` (folder, or a path ending in `.xml`, default `./out`), plus `-n`, `-H`, `--server-pattern`, `--test-case-names`, `--number-of-scopes`, `--read-only`, `--minimal-endpoints`, `--microcks-headers`, `--generate-one-of-any-of`, `--schema-is-inline`, `--is-inline`, `--has-scopes`, `--application-token`, `--no-validate-schema`, `--no-schema-pretty-print`, `-h` and `-V`. Defaults match the HTTP API. ### Changed -- **Split into a Maven multi module build**: `openapi2soapui-core` (conversion engine, free of Spring and of any web dependency), `openapi2soapui-rest` (the HTTP service) and `openapi2soapui-cli`. Java packages are unchanged. -- The service artifact is still `openapi2soapui.war`, or `openapi2soapui.jar` with `-Pjar`, now under `openapi2soapui-rest/target/`. **Its Maven coordinates change from `net.cloudappi:openapi2soapui` to `net.cloudappi:openapi2soapui-rest`**, `docker-compose.yml` points at the new path, and the service runs from the reactor with `mvn -pl openapi2soapui-rest -am spring-boot:run`. -- `messages.properties` moved to the core module, so the service and the CLI report the same validation codes and texts. +- Split into `openapi2soapui-core`, `openapi2soapui-rest` and `openapi2soapui-cli`. The service keeps its artifact file name, but its Maven coordinates become `net.cloudappi:openapi2soapui-rest`. ## [2.0.0] - 2026-08-25 diff --git a/README.md b/README.md index 94d1835..8aa88be 100644 --- a/README.md +++ b/README.md @@ -195,50 +195,129 @@ The war is produced at `openapi2soapui-rest/target/openapi2soapui.war`. ## 🖥️ Command line interface (CLI) The same generation is available as a standalone command, with no server involved: an OpenAPI spec in JSON or -YAML goes in, the SoapUI project XML comes out. It reuses the exact same engine and the same request model as -the HTTP service, so both produce identical projects. +YAML goes in, the SoapUI project XML comes out. It reuses the exact same engine, request model and validations +as the HTTP service, so both produce identical projects. -* To build the CLI jar +### Build ```shell $ mvn clean package -DskipTests ``` -* The jar is produced at `openapi2soapui-cli/target/openapi2soapui-cli.jar` +The jar is produced at `openapi2soapui-cli/target/openapi2soapui-cli.jar` + +### Options + +| Option | Default | What it does | +|---|---|---| +| `-f`, `--file ` | — | OpenAPI spec, v2 or v3, JSON or YAML, as plain text | +| `-c`, `--config ` | — | JSON file with the same body as the REST API, `openApiSpec` base64 encoded. The only way to pass `oAuth2Profiles`, `customAuthorizationsFile` and `examples`. With `-f`, the spec comes from `-f` | +| `-o`, `--output ` | `./out` | Folder, or a path ending in `.xml` for an exact file name | +| `-n`, `--api-name ` | spec title, or spec file name | `apiName` | +| `-H`, `--header ` | none | Request header, repeatable, appended to the config's | +| `--server-pattern ` | first server in the spec | Pick the spec server whose URL contains this text. Accepts the `%text%` form too | +| `--test-case-names ` | none | Extra test cases, comma separated | +| `--number-of-scopes ` | `0` | `numberOfScopes`, only relevant with `--has-scopes` | +| `--read-only` | off | Generate only GET and OPTIONS test cases | +| `--minimal-endpoints` | off | Collapse the `CaseErrorRequired{Field}` test cases into one | +| `--microcks-headers` | off | Add the `X-Microcks-Response-Name` header to every request | +| `--generate-one-of-any-of` | off | Resolve `oneOf`/`anyOf` using their first candidate | +| `--schema-is-inline` | off | Embed the response schema instead of using a project property | +| `--is-inline` | off | Embed body example values instead of project properties | +| `--has-scopes` | off | One extra test case per `oAuth2Profiles` entry | +| `--application-token` | off | Extra test case per `CLIENT_CREDENTIALS` profile | +| `--no-validate-schema` | schema assertion **on** | Do not add the schema assertion | +| `--no-schema-pretty-print` | pretty print **on** | Serialize the schema compactly | +| `-h`, `--help` | — | Show the help | +| `-V`, `--version` | — | Show the version | + +Every option also accepts the `--option=value` form. + +### Commands + +Show the available options, and the build version. + +```shell +$ java -jar openapi2soapui-cli.jar --help +$ java -jar openapi2soapui-cli.jar --version +``` + +Generate a project. Without `-o` it lands in `./out`, named `{apiName}_{apiVersion}-soapui-project.xml`; +an `-o` value ending in `.xml` is taken as the exact file. ```shell -# generate from a spec file into ./out $ java -jar openapi2soapui-cli.jar -f petstore.yaml +$ java -jar openapi2soapui-cli.jar -f petstore.yaml -n Orders -o ./projects +$ java -jar openapi2soapui-cli.jar -f petstore.yaml -n Orders -o ./orders.xml +``` -# name the API and pick the exact output file -$ java -jar openapi2soapui-cli.jar -f petstore.yaml -n Petstore -o ./petstore-project.xml +Narrow down what gets generated: `--read-only` keeps only the GET and OPTIONS cases, `--minimal-endpoints` +collapses the `CaseErrorRequired` ones, and `--test-case-names` adds a copy of the happy path under each name. -# only GET and OPTIONS test cases, with a custom header -$ java -jar openapi2soapui-cli.jar -f petstore.yaml --read-only -H "X-Api-Key:secret" +```shell +$ java -jar openapi2soapui-cli.jar -f petstore.yaml --read-only +$ java -jar openapi2soapui-cli.jar -f petstore.yaml --minimal-endpoints +$ java -jar openapi2soapui-cli.jar -f petstore.yaml --test-case-names Smoke,Regression +``` -# full configuration: the very same JSON body the REST API takes, openApiSpec included as base64 -$ java -jar openapi2soapui-cli.jar -c request.json +Send the same headers with every request. `-H` is repeatable; `--microcks-headers` adds the +`X-Microcks-Response-Name` header a Microcks mock expects. + +```shell +$ java -jar openapi2soapui-cli.jar -f petstore.yaml -H "Authorization:Bearer " -H "X-Correlation-Id:qa-42" +$ java -jar openapi2soapui-cli.jar -f petstore.yaml --microcks-headers +``` + +Control the response schema assertion, which is added by default: drop it, embed the schema in the script +instead of storing it as a project property, or serialize it compactly. + +```shell +$ java -jar openapi2soapui-cli.jar -f petstore.yaml --no-validate-schema +$ java -jar openapi2soapui-cli.jar -f petstore.yaml --schema-is-inline +$ java -jar openapi2soapui-cli.jar -f petstore.yaml --no-schema-pretty-print +``` + +Write the request body example values literally, instead of referencing project properties. + +```shell +$ java -jar openapi2soapui-cli.jar -f petstore.yaml --is-inline +``` -# config file for everything else, spec as a plain file +Target one environment when the spec declares several servers. The value is matched as a substring, so +`%staging%` works too. + +```shell +$ java -jar openapi2soapui-cli.jar -f petstore.yaml --server-pattern staging +``` + +Resolve `oneOf` and `anyOf` bodies using their first candidate, instead of leaving a plain placeholder. + +```shell +$ java -jar openapi2soapui-cli.jar -f petstore.yaml --generate-one-of-any-of +``` + +Pass the full configuration. `-c` takes the same JSON body the REST API accepts, and is the only way to +provide `oAuth2Profiles`, `customAuthorizationsFile` and `examples`. Add `-f` to read the spec from a plain +file instead of the base64 `openApiSpec` the config carries. + +```shell +$ java -jar openapi2soapui-cli.jar -c request.json $ java -jar openapi2soapui-cli.jar -c request.json -f petstore.yaml +``` -$ java -jar openapi2soapui-cli.jar --help +Generate the OAuth2 scope variants once the profiles are in place. + +```shell +$ java -jar openapi2soapui-cli.jar -c request.json -f petstore.yaml --has-scopes --number-of-scopes 2 +$ java -jar openapi2soapui-cli.jar -c request.json -f petstore.yaml --has-scopes --application-token ``` -Notes: - -* `-f` takes the spec as plain text; `-c` takes the REST API body, where `openApiSpec` is base64 encoded, so - an existing request such as [demo/petstore-ok-only-run/request.json](demo/petstore-ok-only-run/request.json) - works as is. When both are given, `-f` provides the spec. -* `oAuth2Profiles`, `customAuthorizationsFile` and `examples` are nested objects and are only reachable - through `-c`. Every other parameter has a flag, listed by `--help`. -* Defaults match the HTTP API exactly, including `validateSchema` and `schemaPrettyPrint` being enabled unless - turned off with `--no-validate-schema` / `--no-schema-pretty-print`. `apiName` is the only difference: the - API requires it, while the CLI derives it from the spec title when neither `-n` nor the config provide one. -* Output defaults to `./out/{apiName}_{apiVersion}-soapui-project.xml`. An `-o` value ending in `.xml` is - taken as the exact file, anything else as a folder. -* Exit codes: `0` success, `1` generation or validation error, `2` usage error. Errors go to stderr, so stdout - only ever carries the result line. +Any supported input works the same way: OpenAPI 3 or Swagger 2.0, written in YAML or JSON. + +```shell +$ java -jar openapi2soapui-cli.jar -f petstore-v2.yaml +$ java -jar openapi2soapui-cli.jar -f petstore.json +``` ## Files and Directories Structure @@ -246,8 +325,6 @@ The project directory has a particular directory structure. A representative pro ### Project Structure -The build is a Maven multi module project: the conversion engine is shared by the HTTP service and the CLI. - ```text . ├── pom.xml parent: packaging pom, shared properties and profiles @@ -283,9 +360,6 @@ The build is a Maven multi module project: the conversion engine is shared by th │ ├── cli.properties version reported by the version flag │ ├── logback.xml │ └── soapui-cli-log4j.xml keeps SoapUI from flooding stdout -├── demo sample specs, requests and generated projects -├── Dockerfile -├── docker-compose.yml ├── lombok.config ├── mvnw ├── mvnw.cmd diff --git a/openapi2soapui-cli/pom.xml b/openapi2soapui-cli/pom.xml index d555a70..b1b7834 100644 --- a/openapi2soapui-cli/pom.xml +++ b/openapi2soapui-cli/pom.xml @@ -19,7 +19,6 @@ openapi2soapui-cli - ${basedir}/src/main/resources true @@ -37,10 +36,6 @@ - org.springframework.boot spring-boot-maven-plugin @@ -59,14 +54,11 @@ ${project.version} - ch.qos.logback logback-classic - org.apache.tomcat.embed tomcat-embed-el @@ -81,7 +73,6 @@ com.vaadin.external.google android-json - org.apache.logging.log4j log4j-to-slf4j diff --git a/openapi2soapui-core/pom.xml b/openapi2soapui-core/pom.xml index 566e51c..2b1fa14 100644 --- a/openapi2soapui-core/pom.xml +++ b/openapi2soapui-core/pom.xml @@ -272,9 +272,6 @@ snakeyaml - org.json json diff --git a/pom.xml b/pom.xml index 2778ff8..3c3aa98 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent 3.5.16 - + net.cloudappi openapi2soapui From dbcc50bb8654a5785c66ac859dc71b41998594ea Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Fri, 28 Aug 2026 12:12:36 -0500 Subject: [PATCH 5/6] fix: sonar security issues --- .../apitools/openapi2soapui/cli/CliArgs.java | 184 ++++++++++-------- .../openapi2soapui/cli/Openapi2SoapUICli.java | 12 +- .../openapi2soapui/cli/SoapUILogging.java | 6 +- 3 files changed, 114 insertions(+), 88 deletions(-) diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java index cfa435d..7d2d51d 100644 --- a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java @@ -10,6 +10,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.function.Consumer; import java.util.regex.Pattern; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -22,10 +23,52 @@ final class CliArgs { static final String DEFAULT_OUTPUT = "./out"; + static final String USAGE = """ + openapi2soapui - generates a SoapUI project (XML) from an OpenAPI specification + + Usage: java -jar openapi2soapui-cli.jar [options] + + Input (at least one of -f, -c is required): + -f, --file OpenAPI spec, JSON or YAML, as plain text (not base64) + -c, --config JSON file with the same body as the REST API, where + openApiSpec is base64 encoded. It is the only way to pass + oAuth2Profiles, customAuthorizationsFile and examples. + When -f is also given, -f provides the spec. + + Output: + -o, --output Folder, or a path ending in .xml for an exact file name + (default: ./out) + + Generation options, they override the config file: + -n, --api-name apiName (default: the spec title, or the spec file name) + -H, --header Request header, repeatable + --server-pattern Pick the spec server whose URL contains this text + --test-case-names Extra test cases, comma separated + --number-of-scopes numberOfScopes, only relevant with --has-scopes + --read-only Generate only GET and OPTIONS test cases + --minimal-endpoints Collapse the ErrorRequired test cases into one + --microcks-headers Add the X-Microcks-Response-Name header to every request + --generate-one-of-any-of Resolve oneOf/anyOf using their first candidate + --schema-is-inline Embed the response schema instead of using a project property + --is-inline Embed body example values instead of project properties + --has-scopes One extra test case per oAuth2Profiles entry + --application-token Extra test case per CLIENT_CREDENTIALS profile + --no-validate-schema Do not add the schema assertion (on by default) + --no-schema-pretty-print Serialize the schema compactly (pretty by default) + + Other: + -h, --help Show this help + -V, --version Show the version + """; + private static final String OUTPUT_FILE_SUFFIX = "-soapui-project.xml"; private static final Pattern UNSAFE_NAME_CHARS = Pattern.compile("[^\\p{L}\\p{N}._-]+"); + private static final Pattern LEADING_DASHES = Pattern.compile("^-+"); + + private static final Pattern TRAILING_DASHES = Pattern.compile("-+$"); + private String specFile; private String configFile; private String output; @@ -52,20 +95,20 @@ private CliArgs() { static CliArgs parse(String[] args) { CliArgs parsed = new CliArgs(); - List tokens = normalize(args); - for (int i = 0; i < tokens.size(); i++) { - String option = tokens.get(i); + Tokens tokens = new Tokens(normalize(args)); + while (tokens.hasNext()) { + String option = tokens.next(); switch (option) { case "-h", "--help" -> parsed.help = true; case "-V", "--version" -> parsed.version = true; - case "-f", "--file" -> parsed.specFile = value(tokens, ++i, option); - case "-c", "--config" -> parsed.configFile = value(tokens, ++i, option); - case "-o", "--output" -> parsed.output = value(tokens, ++i, option); - case "-n", "--api-name" -> parsed.apiName = value(tokens, ++i, option); - case "-H", "--header" -> parsed.headers.add(header(value(tokens, ++i, option))); - case "--server-pattern" -> parsed.serverPattern = value(tokens, ++i, option); - case "--test-case-names" -> parsed.testCaseNames = testCaseNames(value(tokens, ++i, option)); - case "--number-of-scopes" -> parsed.numberOfScopes = integer(rawValue(tokens, ++i, option), option); + case "-f", "--file" -> parsed.specFile = tokens.value(option); + case "-c", "--config" -> parsed.configFile = tokens.value(option); + case "-o", "--output" -> parsed.output = tokens.value(option); + case "-n", "--api-name" -> parsed.apiName = tokens.value(option); + case "-H", "--header" -> parsed.headers.add(header(tokens.value(option))); + case "--server-pattern" -> parsed.serverPattern = tokens.value(option); + case "--test-case-names" -> parsed.testCaseNames = testCaseNames(tokens.value(option)); + case "--number-of-scopes" -> parsed.numberOfScopes = integer(tokens.rawValue(option), option); case "--read-only" -> parsed.readOnly = Boolean.TRUE; case "--minimal-endpoints" -> parsed.minimalEndpoints = Boolean.TRUE; case "--microcks-headers" -> parsed.microcksHeaders = Boolean.TRUE; @@ -96,19 +139,6 @@ private static List normalize(String[] args) { return tokens; } - private static String value(List tokens, int index, String option) { - String value = rawValue(tokens, index, option); - if (value.length() > 1 && value.startsWith("-")) { - throw new UsageException("option " + option + " requires a value, found " + value); - } - return value; - } - - private static String rawValue(List tokens, int index, String option) { - if (index >= tokens.size()) throw new UsageException("option " + option + " requires a value"); - return tokens.get(index); - } - private static Header header(String value) { int separator = value.indexOf(':'); if (separator < 1 || separator == value.length() - 1) { @@ -139,26 +169,30 @@ SoapUIProjectRequest toRequest() throws IOException { SoapUIProjectRequest request = (configFile != null) ? readConfig(configFile) : new SoapUIProjectRequest(); if (specFile != null) request.setOpenAPIContent(readSpec(specFile)); - - if (apiName != null) request.setApiName(apiName); - if (serverPattern != null) request.setServerPattern(serverPattern); - if (testCaseNames != null) request.setTestCaseNames(testCaseNames); if (!headers.isEmpty()) request.setHeaders(mergeHeaders(request.getHeaders())); - if (readOnly != null) request.setReadOnly(readOnly); - if (minimalEndpoints != null) request.setMinimalEndpoints(minimalEndpoints); - if (microcksHeaders != null) request.setMicrocksHeaders(microcksHeaders); - if (generateOneOfAnyOf != null) request.setGenerateOneOfAnyOf(generateOneOfAnyOf); - if (validateSchema != null) request.setValidateSchema(validateSchema); - if (schemaIsInline != null) request.setSchemaIsInline(schemaIsInline); - if (schemaPrettyPrint != null) request.setSchemaPrettyPrint(schemaPrettyPrint); - if (isInline != null) request.setIsInline(isInline); - if (hasScopes != null) request.setHasScopes(hasScopes); - if (applicationToken != null) request.setApplicationToken(applicationToken); - if (numberOfScopes != null) request.setNumberOfScopes(numberOfScopes); + + apply(apiName, request::setApiName); + apply(serverPattern, request::setServerPattern); + apply(testCaseNames, request::setTestCaseNames); + apply(readOnly, request::setReadOnly); + apply(minimalEndpoints, request::setMinimalEndpoints); + apply(microcksHeaders, request::setMicrocksHeaders); + apply(generateOneOfAnyOf, request::setGenerateOneOfAnyOf); + apply(validateSchema, request::setValidateSchema); + apply(schemaIsInline, request::setSchemaIsInline); + apply(schemaPrettyPrint, request::setSchemaPrettyPrint); + apply(isInline, request::setIsInline); + apply(hasScopes, request::setHasScopes); + apply(applicationToken, request::setApplicationToken); + apply(numberOfScopes, request::setNumberOfScopes); return request; } + private static void apply(T value, Consumer setter) { + if (value != null) setter.accept(value); + } + private static SoapUIProjectRequest readConfig(String path) throws IOException { ObjectMapper mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); return mapper.readValue(requireFile(path, "config"), SoapUIProjectRequest.class); @@ -196,7 +230,9 @@ Path resolveOutput(String apiName, String apiVersion) { private static String sanitize(String value) { if (value == null || value.isBlank()) return "project"; - String safe = UNSAFE_NAME_CHARS.matcher(value).replaceAll("-").replaceAll("^-+|-+$", ""); + String safe = UNSAFE_NAME_CHARS.matcher(value).replaceAll("-"); + safe = LEADING_DASHES.matcher(safe).replaceAll(""); + safe = TRAILING_DASHES.matcher(safe).replaceAll(""); return safe.isEmpty() ? "project" : safe; } @@ -227,44 +263,36 @@ boolean isVersion() { return version; } - static String usage() { - return """ - openapi2soapui - generates a SoapUI project (XML) from an OpenAPI specification - - Usage: java -jar openapi2soapui-cli.jar [options] - - Input (at least one of -f, -c is required): - -f, --file OpenAPI spec, JSON or YAML, as plain text (not base64) - -c, --config JSON file with the same body as the REST API, where - openApiSpec is base64 encoded. It is the only way to pass - oAuth2Profiles, customAuthorizationsFile and examples. - When -f is also given, -f provides the spec. - - Output: - -o, --output Folder, or a path ending in .xml for an exact file name - (default: ./out) - - Generation options, they override the config file: - -n, --api-name apiName (default: the spec title, or the spec file name) - -H, --header Request header, repeatable - --server-pattern Pick the spec server whose URL contains this text - --test-case-names Extra test cases, comma separated - --number-of-scopes numberOfScopes, only relevant with --has-scopes - --read-only Generate only GET and OPTIONS test cases - --minimal-endpoints Collapse the ErrorRequired test cases into one - --microcks-headers Add the X-Microcks-Response-Name header to every request - --generate-one-of-any-of Resolve oneOf/anyOf using their first candidate - --schema-is-inline Embed the response schema instead of using a project property - --is-inline Embed body example values instead of project properties - --has-scopes One extra test case per oAuth2Profiles entry - --application-token Extra test case per CLIENT_CREDENTIALS profile - --no-validate-schema Do not add the schema assertion (on by default) - --no-schema-pretty-print Serialize the schema compactly (pretty by default) - - Other: - -h, --help Show this help - -V, --version Show the version - """; + private static final class Tokens { + + private final List tokens; + + private int index; + + Tokens(List tokens) { + this.tokens = tokens; + } + + boolean hasNext() { + return index < tokens.size(); + } + + String next() { + return tokens.get(index++); + } + + String value(String option) { + String value = rawValue(option); + if (value.length() > 1 && value.startsWith("-")) { + throw new UsageException("option " + option + " requires a value, found " + value); + } + return value; + } + + String rawValue(String option) { + if (!hasNext()) throw new UsageException("option " + option + " requires a value"); + return next(); + } } static final class UsageException extends RuntimeException { diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java index de5ebfe..51f39de 100644 --- a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/Openapi2SoapUICli.java @@ -13,6 +13,7 @@ import org.apiaddicts.apitools.openapi2soapui.request.SoapUIProjectRequest; import org.apiaddicts.apitools.openapi2soapui.util.SerializedDataUtils; +@SuppressWarnings("java:S106") public final class Openapi2SoapUICli { static final int EXIT_OK = 0; @@ -38,7 +39,7 @@ public int run(String[] args) { } if (cli.isHelp()) { - System.out.println(CliArgs.usage()); + System.out.println(CliArgs.USAGE); return EXIT_OK; } if (cli.isVersion()) { @@ -49,12 +50,11 @@ public int run(String[] args) { return usageError("no input given, pass -f and/or -c "); } - SoapUILogging.install(); - try { + SoapUILogging.install(); return generate(cli); - } catch (Throwable t) { - return error(describe(t)); + } catch (Exception | LinkageError e) { + return error(describe(e)); } } @@ -111,7 +111,7 @@ private static Path write(String xml, Path target) throws IOException { private static int usageError(String message) { System.err.println("error: " + message); System.err.println(); - System.err.println(CliArgs.usage()); + System.err.println(CliArgs.USAGE); return EXIT_USAGE; } diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java index 47dadb7..6ff1731 100644 --- a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/SoapUILogging.java @@ -10,6 +10,7 @@ import java.nio.file.StandardCopyOption; import java.util.concurrent.Callable; +@SuppressWarnings({"java:S106", "java:S5443"}) final class SoapUILogging { private static final String LOG4J_CONFIG_PROPERTY = "soapui.log4j.config"; @@ -23,8 +24,7 @@ final class SoapUILogging { private SoapUILogging() { } - static void install() { - // -Dopenapi2soapui.cli.logLevel=DEBUG still works as an escape hatch for diagnosing a run + static void install() throws IOException { if (System.getProperty(LOG_LEVEL_PROPERTY) == null) System.setProperty(LOG_LEVEL_PROPERTY, "WARN"); if (System.getProperty(PARSER_LOG_LEVEL_PROPERTY) == null) System.setProperty(PARSER_LOG_LEVEL_PROPERTY, "ERROR"); @@ -36,8 +36,6 @@ static void install() { target.toFile().deleteOnExit(); Files.copy(config, target, StandardCopyOption.REPLACE_EXISTING); System.setProperty(LOG4J_CONFIG_PROPERTY, target.toAbsolutePath().toString()); - } catch (IOException e) { - System.err.println("warning: could not install the SoapUI logging configuration: " + e.getMessage()); } } From 7658956ae2b14713d453dace443a44a642191cca Mon Sep 17 00:00:00 2001 From: Melsy Huamani Date: Fri, 28 Aug 2026 12:35:38 -0500 Subject: [PATCH 6/6] fix: sonar issues --- .../apitools/openapi2soapui/cli/CliArgs.java | 10 +-- .../openapi2soapui/model/SoapUIProject.java | 16 ++-- .../util/QueryParamExampleUtilsTest.java | 84 +++++++------------ .../controller/SoapUIProjectController.java | 4 +- .../error/WebControllerAdvice.java | 15 ++-- ...ontrollerCustomAuthorizationsFileTest.java | 58 +++++-------- 6 files changed, 70 insertions(+), 117 deletions(-) diff --git a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java index 7d2d51d..b5c0ba1 100644 --- a/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java +++ b/openapi2soapui-cli/src/main/java/org/apiaddicts/apitools/openapi2soapui/cli/CliArgs.java @@ -265,20 +265,20 @@ boolean isVersion() { private static final class Tokens { - private final List tokens; + private final List values; private int index; - Tokens(List tokens) { - this.tokens = tokens; + Tokens(List values) { + this.values = values; } boolean hasNext() { - return index < tokens.size(); + return index < values.size(); } String next() { - return tokens.get(index++); + return values.get(index++); } String value(String option) { diff --git a/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java index c2b6770..c9dff61 100644 --- a/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java +++ b/openapi2soapui-core/src/main/java/org/apiaddicts/apitools/openapi2soapui/model/SoapUIProject.java @@ -292,9 +292,7 @@ private void setRestServiceEndpoints(List servers, String serverPattern) Optional match = servers.stream() .filter(s -> s.getUrl().contains(cleanPattern)) .findFirst(); - filtered = match.isPresent() - ? Collections.singletonList(match.get()) - : Collections.singletonList(servers.get(0)); + filtered = Collections.singletonList(match.orElseGet(() -> servers.get(0))); } else { filtered = Collections.singletonList(servers.get(0)); } @@ -841,9 +839,9 @@ private String mapObjectToJsonString(Object object) { */ private String mapObjectToJsonString(Object object, boolean prettyPrint) { String jsonString = null; - if (object instanceof JSONObject) { + if (object instanceof JSONObject json) { try { - jsonString = prettyPrint ? ((JSONObject) object).toString(2) : ((JSONObject) object).toString(); + jsonString = prettyPrint ? json.toString(2) : json.toString(); } catch (JSONException e) { log.debug("Error mapObjectToJsonString", e); } @@ -851,7 +849,7 @@ private String mapObjectToJsonString(Object object, boolean prettyPrint) { ObjectMapper mapper = new ObjectMapper(); try { jsonString = prettyPrint - ? mapper.writerWithDefaultPrettyPrinter().writeValueAsString(object).replaceAll("\\r", "") + ? mapper.writerWithDefaultPrettyPrinter().writeValueAsString(object).replace("\r", "") : mapper.writeValueAsString(object); } catch (JsonProcessingException e) { log.debug("Error mapObjectToJsonString", e); @@ -944,8 +942,10 @@ private void setAuthProfiles(List errors = ex.getBindingResult().getFieldErrors().stream() .map(fe -> new ObjectError(BAD_REQUEST, fe.getDefaultMessage())) - .collect(Collectors.toList()); + .toList(); result.addValidationError(errors); return result; } diff --git a/openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerCustomAuthorizationsFileTest.java b/openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerCustomAuthorizationsFileTest.java index 7b3c57e..13f4275 100644 --- a/openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerCustomAuthorizationsFileTest.java +++ b/openapi2soapui-rest/src/test/java/org/apiaddicts/apitools/openapi2soapui/controller/SoapUIProjectControllerCustomAuthorizationsFileTest.java @@ -14,6 +14,8 @@ import java.util.Map; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -101,47 +103,8 @@ void customAuthorizationsFileEmpty_endToEndNoAuthorizationsSuite() throws Except .andExpect(content().string(not(containsString("authorizations_TestApi_1.0-Suite")))); } - @Test - void customAuthorizationMissingName_returns400WithValidationErrorCode1501() throws Exception { - Map customAuthorization = validCustomAuthorization("Application token"); - customAuthorization.remove("name"); - Map body = baseRequestBody(); - body.put("customAuthorizationsFile", List.of(customAuthorization)); - mockMvc.perform(post(basePath + "/soap-ui-projects") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(body))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.result.errors[0].errorCode").value(1501)); - } - @Test - void customAuthorizationMissingMethod_returns400WithValidationErrorCode1502() throws Exception { - Map customAuthorization = validCustomAuthorization("Application token"); - customAuthorization.remove("method"); - Map body = baseRequestBody(); - body.put("customAuthorizationsFile", List.of(customAuthorization)); - - mockMvc.perform(post(basePath + "/soap-ui-projects") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(body))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.result.errors[0].errorCode").value(1502)); - } - - @Test - void customAuthorizationMissingEndpoint_returns400WithValidationErrorCode1503() throws Exception { - Map customAuthorization = validCustomAuthorization("Application token"); - customAuthorization.remove("endpoint"); - Map body = baseRequestBody(); - body.put("customAuthorizationsFile", List.of(customAuthorization)); - - mockMvc.perform(post(basePath + "/soap-ui-projects") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(body))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.result.errors[0].errorCode").value(1503)); - } @Test void customAuthorizationInvalidMethod_returns400WithValidationErrorCode1504() throws Exception { @@ -225,4 +188,21 @@ void customAuthorizationWithHeadersAndBody_endToEndAppliesThem() throws Exceptio .andExpect(content().string(containsString("grant_type=client_credentials"))) .andExpect(content().string(containsString("abc123"))); } + + @ParameterizedTest + @CsvSource({"name, 1501", "method, 1502", "endpoint, 1503"}) + void customAuthorizationMissingRequiredField_returns400WithItsValidationErrorCode(String field, int errorCode) + throws Exception { + Map customAuthorization = validCustomAuthorization("Application token"); + customAuthorization.remove(field); + Map body = baseRequestBody(); + body.put("customAuthorizationsFile", List.of(customAuthorization)); + + mockMvc.perform(post(basePath + "/soap-ui-projects") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.result.errors[0].errorCode").value(errorCode)); + } + }