diff --git a/README.md b/README.md index 96b9e4b33..896c95ad1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ The project is very much Work In Progress and will be published on maven central # Release Notes BOAT is still under development and subject to change. +## 0.15.0 +* *Maven Plugin* + * Added new goal `boat:radio`; see the description in the [plugin documentation](boat-maven-plugin/README.md#boatradio). ## 0.14.12 * *Boat Scaffold* * References to /examples/foo now are also dereferenced diff --git a/boat-engine/pom.xml b/boat-engine/pom.xml index 7a27d6557..80a6b02f8 100644 --- a/boat-engine/pom.xml +++ b/boat-engine/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.14.13-SNAPSHOT + 0.15.0-SNAPSHOT boat-engine jar diff --git a/boat-maven-plugin/README.md b/boat-maven-plugin/README.md index 0e340c274..9842102fe 100644 --- a/boat-maven-plugin/README.md +++ b/boat-maven-plugin/README.md @@ -255,6 +255,105 @@ Usage Or hook up to your build process by adding ```executions``` configuration. +## boat:radio + +Upload specs (one of more) to Boat-Bay. + +Available parameters: + + artifactId (Default: ${project.artifactId}) + User property: artifactId + Project ArtifactId in Boat-Bay. Defaults to ${project.artifactId} + + boatBayUrl + Required: true + User property: boatBayUrl + Boat-Bay domain. eg. https://boatbay.mycompany.eu + + failOnBreakingChange (Default: false) + User property: failOnBreakingChange + Fail the build for breaking changes in specs + + failOnLintViolation (Default: false) + User property: failOnLintViolation + Fail the build if the spec has lint violation (Violation with Severity.MUST) + + + groupId (Default: ${project.groupId}) + User property: groupId + Project GroupId in Boat-Bay. Defaults to ${project.groupId} + + password + User property: password + Defines the password of the username which can access the Boat-Bay upload + API. Required if boat-bay APIs are protected. + + portalKey + Required: true + User property: portalKey + Project portal Identifier in Boat-Bay. + + radioOutput (Default: + ${project.build.directory}/target/boat-radio-report) + Output directory for boat-radio report. + + sourceKey + Required: true + User property: sourceKey + Project source identifier in Boat-Bay. + + specs + Required: true + User property: specs + Array of spec to be uploaded. Spec fields: + + key : Spec Key in Boat-Bay. Defaults to filename.lastIndexOf('-'). For + example - By default my-service-api-v3.1.4.yaml would be evaluated to + my-service-api + + name : Spec Name in Boat-Bay. Defaults to filename. + + inputSpec : Location of the OpenAPI spec, as URL or local file glob + pattern. If the input is a local file, the value of this property is + considered a glob pattern that must resolve to a unique file. The glob + pattern allows to express the input specification in a version neutral + way. For instance, if the actual file is my-service-api-v3.1.4.yaml the + expression could be my-service-api-v*.yaml. + + username + User property: username + Defines the username which can access Boat-Bay upload API. Required if + boat-bay APIs are protected. + + version (Default: ${project.version}) + User property: version + Project Version in Boat-Bay. Defaults to ${project.version} + + +Configuration example: + +```$xml + + upload-specs + install + + radio + + + pet-store-bom + example + https://boatbay.backbase.eu + admin + admin + + + ${project.build.directory}/spec/bundled/pet-store-client-api-*.yaml + + + + +``` + ## boat:transform Apply transformers to an existing specification. diff --git a/boat-maven-plugin/pom.xml b/boat-maven-plugin/pom.xml index a1e56321f..e7bd78212 100644 --- a/boat-maven-plugin/pom.xml +++ b/boat-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.14.13-SNAPSHOT + 0.15.0-SNAPSHOT boat-maven-plugin @@ -24,6 +24,11 @@ reuseReports ${project.basedir}/target/jacoco-it.exec + 10.11 + 3.8.0 + 2.9.10 + 8.0.0 + 1.5.24 @@ -140,6 +145,83 @@ test + + io.swagger + swagger-annotations + ${swagger-annotations-version} + + + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + + + io.github.openfeign + feign-core + ${feign-version} + + + io.github.openfeign + feign-jackson + ${feign-version} + + + io.github.openfeign + feign-slf4j + ${feign-version} + + + io.github.openfeign.form + feign-form + ${feign-form-version} + + + io.github.openfeign + feign-okhttp + ${feign-version} + + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + org.openapitools + jackson-databind-nullable + + + com.github.joschi.jackson + jackson-datatype-threetenbp + ${jackson-threetenbp-version} + + + com.github.scribejava + scribejava-core + ${scribejava-version} + + + + + com.squareup.okhttp3 + mockwebserver + 4.9.1 + test + + + @@ -254,6 +336,34 @@ + + org.openapitools + openapi-generator-maven-plugin + 5.0.0 + + + + generate + + + ${project.basedir}/src/main/resources/boat-maven-plugin-api.yaml + java + false + false + false + false + + src/gen/java/main + com.backbase.oss.boat.bay.client.model + com.backbase.oss.boat.bay.client.api + feign + java8-localdatetime + @lombok.AllArgsConstructor @lombok.Builder @lombok.NoArgsConstructor + + + + + \ No newline at end of file diff --git a/boat-maven-plugin/src/main/java/com/backbase/oss/boat/radio/RadioMojo.java b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/radio/RadioMojo.java new file mode 100644 index 000000000..3ec98cea8 --- /dev/null +++ b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/radio/RadioMojo.java @@ -0,0 +1,294 @@ +package com.backbase.oss.boat.radio; + +import com.backbase.oss.boat.Utils; +import com.backbase.oss.boat.bay.client.ApiClient; +import com.backbase.oss.boat.bay.client.api.BoatMavenPluginApi; +import com.backbase.oss.boat.bay.client.model.*; +import com.backbase.oss.boat.loader.OpenAPILoader; +import com.backbase.oss.boat.loader.OpenAPILoaderException; +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.auth.BasicAuthRequestInterceptor; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +import static java.lang.String.format; + +/** + * Upload specs (one of more) to Boat-Bay. + */ +@Mojo(name = "radio", requiresDependencyResolution = ResolutionScope.RUNTIME, threadSafe = true) +@Slf4j +@Getter +@Setter +public class RadioMojo extends AbstractMojo { + + /** + * Project GroupId in Boat-Bay. Defaults to {@code ${project.groupId}} + */ + @Parameter(property = "groupId", defaultValue = "${project.groupId}") + private String groupId; + + /** + * Project ArtifactId in Boat-Bay. Defaults to {@code ${project.artifactId}} + */ + @Parameter(property = "artifactId", defaultValue = "${project.artifactId}") + private String artifactId; + + /** + * Project Version in Boat-Bay. Defaults to {@code ${project.version}} + */ + @Parameter(property = "version", defaultValue = "${project.version}") + private String version; + + /** + * Boat-Bay domain. eg. https://boatbay.mycompany.eu + */ + @Parameter(property = "boatBayUrl", required = true) + private String boatBayUrl; + + /** + * Fail the build for breaking changes in specs + */ + @Parameter(property = "failOnBreakingChange", defaultValue="false") + private boolean failOnBreakingChange; + + /** + * Fail the build if the spec has lint violation (Violation with Severity.MUST) + */ + @Parameter(property = "failOnLintViolation", defaultValue="false") + private boolean failOnLintViolation; + + /** + * Fail the build if boatbay server returns an error + */ + @Parameter(property = "failOnBoatBayErrorResponse", defaultValue="true") + private boolean failOnBoatBayErrorResponse =true; + + /** + * Project portal Identifier in Boat-Bay. + */ + @Parameter(property = "portalKey", required = true) + private String portalKey; + + /** + * Project source identifier in Boat-Bay. + */ + @Parameter(property = "sourceKey", required = true) + private String sourceKey; + + /** + * Defines the username which can access Boat-Bay upload API. Required if boat-bay APIs are protected. + */ + @Parameter(property = "username") + private String username; + + /** + * Defines the password of the username which can access the Boat-Bay upload API. Required if boat-bay APIs are protected. + */ + @Parameter(property = "password") + private String password; + + /** + *

+ * Array of spec to be uploaded. Spec fields: + *

+ *

+ * {@code key} : + * Spec Key in Boat-Bay. Defaults to {@code filename.lastIndexOf("-")}. + * For example - By default {@code my-service-api-v3.1.4.yaml} would be evaluated to {@code my-service-api} + *

+ *

+ * {@code name} : + * Spec Name in Boat-Bay. Defaults to filename. + *

+ *

+ * {@code inputSpec} : + * Location of the OpenAPI spec, as URL or local file glob pattern. + * If the input is a local file, the value of this property is considered a glob pattern that must + * resolve to a unique file. + * The glob pattern allows to express the input specification in a version neutral way. For + * instance, if the actual file is {@code my-service-api-v3.1.4.yaml} the expression could be + * {@code my-service-api-v*.yaml}. + *

+ */ + @Parameter(property = "specs", required = true) + private SpecConfig[] specs; + + /** + * Output directory for boat-radio report. + */ + @Parameter(name = "radioOutput", defaultValue = "${project.build.directory}/target/boat-radio-report") + private File radioOutput; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + + BasicAuthRequestInterceptor basicAuthRequestInterceptor = null; + + ObjectMapper objectMapper = new ObjectMapper(); + + if (username != null && !username.isEmpty() && password != null && !password.isEmpty()) { + getLog().info("Basic Authentication set for username " + username); + basicAuthRequestInterceptor = new BasicAuthRequestInterceptor(username, password); + } else { + getLog().info("No Authentication set"); + } + + List allSpecs = new ArrayList<>(); + + for (SpecConfig spec : specs) { + allSpecs.add(mapToUploadSpec(spec)); + } + + ApiClient apiClient = new ApiClient().setBasePath(boatBayUrl); + if (basicAuthRequestInterceptor != null) { + apiClient.addAuthorization("Basic Auth", basicAuthRequestInterceptor); + } + + BoatMavenPluginApi api = apiClient.buildClient(BoatMavenPluginApi.class); + + UploadRequestBody uploadRequestBody = UploadRequestBody.builder() + .groupId(groupId).artifactId(artifactId).version(version).specs(allSpecs).build(); + + List reports =null; + try { + reports = api.uploadSpec(portalKey, sourceKey, uploadRequestBody); + }catch (Exception e){ + getLog().error("BoatBay error :: " + e.getMessage()); + if(failOnBoatBayErrorResponse) + throw new MojoFailureException("BoatBay error", e); + } + + // Process Result + if(reports!=null) { + try { + File outputFile = new File(getOutput(), "radioOutput.json"); + objectMapper.writerWithDefaultPrettyPrinter().writeValue(outputFile, reports); + // Log summary of report + reports.forEach(report -> { + getLog().info(format("Spec %s summary :", report.getSpec().getKey())); + getLog().info(format("Changes are %s ", report.getSpec().getChanges())); + getLog().info("Number of Violations:" + report.getViolations().size()); + }); + // Log link to reports + getLog().info("UPLOAD TO BOAT-BAY SUCCESSFUL, check the full report: " + outputFile.getCanonicalPath()); + + if (failOnBreakingChange) { + boolean doesSpecsHaveBreakingChanges = reports.stream() + .anyMatch(report -> report.getSpec().getChanges().equals(Changes.BREAKING)); + if (doesSpecsHaveBreakingChanges) + throw new MojoFailureException("Specs have Breaking Changes. Check full report."); + } + + if (failOnLintViolation) { + boolean doesSpecsHaveMustViolations = reports.stream() + .anyMatch(report -> report.getViolations().stream() + .anyMatch(violation -> violation.getSeverity().equals(Severity.MUST))); + if (doesSpecsHaveMustViolations) + throw new MojoFailureException("Specs have Must Violations. Check full report."); + } + } catch (IOException e) { + throw new MojoFailureException("Failed to write output", e); + } + } + + } + + private UploadSpec mapToUploadSpec(SpecConfig spec) throws MojoExecutionException { + + //Validate if the spec file path is valid and unique. + File inputSpecFile = new File(spec.getInputSpec()); + File inputParent = inputSpecFile.getParentFile(); + + if (inputParent.isDirectory()) { + try { + String[] files = Utils.selectInputs(inputParent.toPath(), inputSpecFile.getName()); + + switch (files.length) { + case 0: + String noFileMessage = format("Input spec %s doesn't match any local file", spec.getInputSpec()); + getLog().error(noFileMessage); + throw new MojoExecutionException(noFileMessage); + + case 1: + inputSpecFile = new File(inputParent, files[0]); + spec.setInputSpec(inputSpecFile.getAbsolutePath()); + break; + + default: + String message = format("Input spec %s matches more than one single file", spec.getInputSpec()); + getLog().error(message); + Stream.of(files).forEach(f -> getLog().error(format(" %s", f))); + throw new MojoExecutionException(message); + } + } catch (IOException e) { + throw new MojoExecutionException("Cannot find input " + spec.getInputSpec()); + } + } else { + String message = format("Invalid parent spec folder %s ", spec.getInputSpec()); + getLog().error(message); + throw new MojoExecutionException(message); + } + + //Validate if the spec file is valid open-api spec + String contents; + try { + contents = IOUtils.toString(inputSpecFile.toURI(), Charset.defaultCharset()); + OpenAPILoader.parse(contents); + } catch (IOException e) { + String msg = "Invalid File Path: " + inputSpecFile.getName(); + getLog().error(msg); + throw new MojoExecutionException(msg, e); + } catch (OpenAPILoaderException e) { + String msg = "Invalid Open Api file: " + inputSpecFile.getName(); + getLog().error(msg); + throw new MojoExecutionException(msg, e); + } + + String key = spec.getKey(); + if (key == null || key.isEmpty()) { + key = inputSpecFile.getName().substring(0, inputSpecFile.getName().lastIndexOf("-")); + } + + String name = spec.getName(); + if (name == null || name.isEmpty()) { + name = inputSpecFile.getName(); + } + + //Validation Complete. Prepare UploadSpec. + UploadSpec uploadSpec = UploadSpec.builder() + .fileName(inputSpecFile.getName()).key(key).name(name).openApi(contents).build(); + + return uploadSpec; + + } + + @SneakyThrows + private File getOutput() { + if (radioOutput == null) { + radioOutput = new File("./target/boat-radio-report"); + } + if (!radioOutput.exists()) { + radioOutput.mkdirs(); + } + return radioOutput; + } + + +} + diff --git a/boat-maven-plugin/src/main/java/com/backbase/oss/boat/radio/SpecConfig.java b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/radio/SpecConfig.java new file mode 100644 index 000000000..fdf490548 --- /dev/null +++ b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/radio/SpecConfig.java @@ -0,0 +1,36 @@ +package com.backbase.oss.boat.radio; + +import lombok.Data; + +@Data +/** + * Spec to be uploaded. + */ +public class SpecConfig { + + /** + * Spec Key in Boat-Bay. Defaults to {@code inputSpecFile.getName().lastIndexOf("-")}. + * For example - By default {@code my-service-api-v3.1.4.yaml} would be evaluated to {@code my-service-api} + */ + private String key; + + /** + * Spec Name in Boat-Bay. Defaults to filename. + */ + private String name; + + /** + * Location of the OpenAPI spec, as URL or local file glob pattern. + *

+ * If the input is a local file, the value of this property is considered a glob pattern that must + * resolve to a unique file. + *

+ *

+ * The glob pattern allows to express the input specification in a version neutral way. For + * instance, if the actual file is {@code my-service-api-v3.1.4.yaml} the expression could be + * {@code my-service-api-v*.yaml}. + *

+ */ + private String inputSpec; + +} diff --git a/boat-maven-plugin/src/main/resources/boat-maven-plugin-api.yaml b/boat-maven-plugin/src/main/resources/boat-maven-plugin-api.yaml new file mode 100644 index 000000000..f0cdcbd0a --- /dev/null +++ b/boat-maven-plugin/src/main/resources/boat-maven-plugin-api.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.0 +info: + title: Boat Bay Upload Server + description: Endpoints for uploading Specs to boat bay + license: + name: Backbase + version: 1.0.0 +servers: +- url: http://localhost:8080/ +tags: +- name: boat-maven-plugin + description: Endpoints used by the Boat Maven Plugin +paths: + /api/boat/portals/{portalKey}/boat-maven-plugin/{sourceKey}/upload: + post: + tags: + - boat-maven-plugin + summary: upload and lint specs + operationId: uploadSpec + parameters: + - $ref: 'schemas/definitions.yaml#/components/parameters/PortalKey' + - name: sourceKey + in: path + description: source identifier + required: true + style: simple + explode: false + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: 'schemas/definitions.yaml#/components/schemas/UploadRequestBody' + required: true + responses: + "200": + description: list of lint reports for specs + content: + application/json: + schema: + type: array + items: + $ref: 'schemas/definitions.yaml#/components/schemas/BoatLintReport' diff --git a/boat-maven-plugin/src/main/resources/schemas/definitions.yaml b/boat-maven-plugin/src/main/resources/schemas/definitions.yaml new file mode 100644 index 000000000..d09915508 --- /dev/null +++ b/boat-maven-plugin/src/main/resources/schemas/definitions.yaml @@ -0,0 +1,458 @@ +openapi: 3.0.3 +info: + title: Shared Components BOAT BAY + version: 1.0.0 +paths: {} +components: + schemas: + BoatPortal: + type: object + properties: + id: + type: number + key: + type: string + name: + type: string + content: + type: string + createdOn: + type: string + format: date-time + createdBy: + type: string + numberOfServices: + type: integer + numberOfCapabilities: + type: integer + productDescription: + type: string + lastLintReport: + $ref: '#/components/schemas/BoatLintReport' + statistics: + $ref: '#/components/schemas/BoatStatistics' + required: + - id + - key + - name + BoatProduct: + type: object + properties: + portalKey: + type: string + portalName: + type: string + id: + type: number + key: + type: string + name: + type: string + content: + type: string + createdOn: + type: string + format: date-time + createdBy: + type: string + lastLintReport: + $ref: '#/components/schemas/BoatLintReport' + statistics: + $ref: '#/components/schemas/BoatStatistics' + jiraProjectId: + type: string + required: + - portalKey + - portalName + - id + - key + - name + BoatCapability: + type: object + properties: + id: + type: number + key: + type: string + name: + type: string + content: + type: string + createdOn: + type: string + format: date-time + createdBy: + type: string + services: + type: array + items: + $ref: '#/components/schemas/BoatService' + lastLintReport: + $ref: '#/components/schemas/BoatLintReport' + statistics: + $ref: '#/components/schemas/BoatStatistics' + required: + - id + - key + - name + BoatService: + type: object + properties: + id: + type: number + key: + type: string + name: + type: string + description: + type: string + icon: + type: string + color: + type: string + createdOn: + type: string + format: date-time + createdBy: + type: string + statistics: + $ref: '#/components/schemas/BoatStatistics' + capability: + $ref: '#/components/schemas/BoatCapability' + required: + - id + - key + - name + - capability + BoatSpec: + type: object + properties: + id: + type: number + key: + type: string + name: + type: string + title: + type: string + description: + type: string + icon: + type: string + version: + type: string + grade: + type: string + createdOn: + type: string + format: date-time + createdBy: + type: string + statistics: + $ref: '#/components/schemas/BoatStatistics' + backwardsCompatible: + type: boolean + changes: + $ref: '#/components/schemas/Changes' + capability: + $ref: '#/components/schemas/BoatCapability' + serviceDefinition: + $ref: '#/components/schemas/BoatService' + openApi: + type: string + required: + - id + - key + - name + - version + - capability + - serviceDefinition + BoatLintReport: + type: object + properties: + id: + type: number + spec: + $ref: '#/components/schemas/BoatSpec' + name: + type: string + passed: + type: boolean + lintedOn: + type: string + format: date-time + openApi: + type: string + version: + type: string + grade: + type: string + violations: + type: array + items: + $ref: '#/components/schemas/BoatViolation' + required: + - id + - sec + - name + - passed + - lintedOn + - openApi + - version + - grade + - spec + - violations + BoatLintRule: + type: object + properties: + id: + type: number + ruleId: + type: string + enabled: + type: boolean + title: + type: string + ruleSet: + type: string + severity: + $ref: '#/components/schemas/Severity' + url: + type: string + format: uri + required: + - id + - ruleId + - enabled + - title + - ruleSet + - severity + - url + BoatProductRelease: + type: object + properties: + id: + type: number + key: + type: string + name: + type: string + version: + type: string + releaseDate: + type: string + format: date-time + required: + - id + - key + - name + - version + - releaseDate + BoatTag: + type: object + properties: + name: + type: string + description: + type: string + hide: + type: boolean + color: + type: string + numberOfOccurrences: + type: integer + required: + - name + Resource: + type: array + items: + type: string + format: byte + BoatStatistics: + type: object + properties: + updatedOn: + type: string + format: date-time + mustViolationsCount: + type: integer + format: int64 + shouldViolationsCount: + type: integer + format: int64 + mayViolationsCount: + type: integer + format: int64 + hintViolationsCount: + type: integer + format: int64 + required: + - updatedOn + - mustViolationsCount + - shouldViolationsCount + - mayViolationsCount + - hintViolationsCount + Changes: + type: string + enum: + - INVALID_VERSION + - NOT_APPLICABLE + - ERROR_COMPARING + - UNCHANGED + - COMPATIBLE + - BREAKING + BoatViolation: + type: object + properties: + rule: + $ref: '#/components/schemas/BoatLintRule' + description: + type: string + severity: + $ref: '#/components/schemas/Severity' + lines: + $ref: '#/components/schemas/IntRange' + pointer: + type: string + required: + - rule + - description + - lines + - pointer + - severity + Severity: + type: string + enum: + - MUST + - SHOULD + - MAY + - HINT + UploadRequestBody: + type: object + properties: + specs: + type: array + items: + $ref: '#/components/schemas/UploadSpec' + groupId: + type: string + artifactId: + type: string + version: + type: string + required: + - specs + - artifactId + - groupId + - version + UploadSpec: + type: object + properties: + fileName: + type: string + openApi: + type: string + key: + type: string + name: + type: string + required: + - fileName + - openApi + - name + IntRange: + type: object + properties: + start: + type: integer + endInclusive: + type: integer + required: + - start + - endInclusive + parameters: + PortalKey: + name: portalKey + in: path + description: Portal Identifier + required: true + schema: + type: string + ProductKey: + name: productKey + in: path + description: Product Identifier + required: true + schema: + type: string + CapabilityKey: + name: capabilityKey + in: path + description: Capability Identifier + required: true + schema: + type: string + ServiceKey: + name: serviceKey + in: path + description: Service Identifier + required: true + schema: + type: string + SpecKey: + name: specKey + in: path + description: Spec Identifier + required: true + schema: + type: string + ReleaseKey: + name: releaseKey + in: path + description: Product Release Identifier + required: true + schema: + type: string + SpecId: + name: specId + in: path + description: Unique Spec Identifier + required: true + schema: + type: number + Version: + name: version + in: path + description: Spec Version + required: true + schema: + type: string + Page: + name: page + in: query + required: true + schema: + type: integer + Size: + name: size + in: query + required: true + schema: + type: integer + Sort: + name: sort + in: query + required: true + schema: + type: array + items: + type: string + headers: + X-Total-Count: + description: "Total amount of items" + schema: + type: integer + Link: + description: "Link" + schema: + type: string diff --git a/boat-maven-plugin/src/test/java/com/backbase/oss/boat/radio/RadioMojoTests.java b/boat-maven-plugin/src/test/java/com/backbase/oss/boat/radio/RadioMojoTests.java new file mode 100644 index 000000000..ba1f95291 --- /dev/null +++ b/boat-maven-plugin/src/test/java/com/backbase/oss/boat/radio/RadioMojoTests.java @@ -0,0 +1,596 @@ +package com.backbase.oss.boat.radio; + +import com.backbase.oss.boat.bay.client.model.*; +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.auth.BasicAuthRequestInterceptor; +import java.io.File; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Slf4j +class RadioMojoTests { + + static MockWebServer mockBackEnd; + + ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeAll + @SneakyThrows + static void setUp() { + mockBackEnd = new MockWebServer(); + mockBackEnd.start(); + } + + @Test + @SneakyThrows + void test_all_valid_inputs() { + + final String portalKey = "example"; + final String sourceKey = "pet-store-bom"; + final String specKey = "spec-key"; + final String specValue = "spec-name"; + final String groupId = "com.backbase.boat.samples"; + final String artifactId = "pet-store-bom"; + final String version = "2021.09"; + final String fileNameRequest = "one-client-*-v1.yaml"; + final String fileNameResolved = "one-client-api-v1.yaml"; + + final BigDecimal reportId = BigDecimal.valueOf(10); + final String reportGrade = "A"; + + SpecConfig specConfig = new SpecConfig(); + specConfig.setKey(specKey); + specConfig.setName(specValue); + specConfig.setInputSpec(getFile("/bundler/folder/" + fileNameRequest)); + + RadioMojo mojo = new RadioMojo(); + mojo.setGroupId(groupId); + mojo.setArtifactId(artifactId); + mojo.setVersion(version); + mojo.setPortalKey(portalKey); + mojo.setSourceKey(sourceKey); + mojo.setSpecs(new SpecConfig[]{specConfig}); + mojo.setBoatBayUrl(String.format("http://localhost:%s", mockBackEnd.getPort())); + + final Dispatcher dispatcher = new Dispatcher() { + @SneakyThrows + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/api/boat/portals/" + portalKey + "/boat-maven-plugin/" + sourceKey + "/upload": + + UploadRequestBody requestBody = objectMapper.readValue(request.getBody().readUtf8(), UploadRequestBody.class); + UploadSpec uploadSpec = requestBody.getSpecs().get(0); + + if (requestBody.getGroupId().equals(groupId) && + requestBody.getArtifactId().equals(artifactId) && + requestBody.getVersion().equals(version) && + uploadSpec.getKey().equals(specKey) && + uploadSpec.getName().equals(specValue) && + uploadSpec.getFileName().equals(fileNameResolved) && + uploadSpec.getOpenApi().length() > 0 + ) { + log.info(uploadSpec.getOpenApi()); + List result = getSampleBoatLintReports(specKey, reportId, reportGrade,Changes.COMPATIBLE,Severity.HINT); + return new MockResponse().setResponseCode(200).setBody(objectMapper.writeValueAsString(result)); + } else { + return new MockResponse().setResponseCode(400); + } + } + return new MockResponse().setResponseCode(404); + } + }; + + mockBackEnd.setDispatcher(dispatcher); + + mojo.execute(); + + File output = new File(mojo.getRadioOutput(), "radioOutput.json"); + + assertTrue(output.exists()); + + List result = Arrays.asList(objectMapper.readValue(output, BoatLintReport[].class)); + + assertEquals(1, result.size()); + assertEquals(reportId, result.get(0).getId()); + assertEquals(reportGrade, result.get(0).getGrade()); + + } + + + @Test + @SneakyThrows + void test_empty_specKeyAndName() { + + final String portalKey = "example"; + final String sourceKey = "pet-store-bom"; + final String groupId = "com.backbase.boat.samples"; + final String artifactId = "pet-store-bom"; + final String version = "2021.09"; + final String fileNameRequest = "one-client-*-v2.yaml"; + final String fileNameResolved = "one-client-api-v2.yaml"; + + final BigDecimal reportId = BigDecimal.valueOf(20); + final String reportGrade = "B"; + + SpecConfig specConfig = new SpecConfig(); + specConfig.setInputSpec(getFile("/bundler/folder/" + fileNameRequest)); + + RadioMojo mojo = new RadioMojo(); + mojo.setGroupId(groupId); + mojo.setArtifactId(artifactId); + mojo.setVersion(version); + mojo.setPortalKey(portalKey); + mojo.setSourceKey(sourceKey); + mojo.setSpecs(new SpecConfig[]{specConfig}); + mojo.setBoatBayUrl(String.format("http://localhost:%s", mockBackEnd.getPort())); + + final Dispatcher dispatcher = new Dispatcher() { + @SneakyThrows + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/api/boat/portals/" + portalKey + "/boat-maven-plugin/" + sourceKey + "/upload": + + UploadRequestBody requestBody = objectMapper.readValue(request.getBody().readUtf8(), UploadRequestBody.class); + UploadSpec uploadSpec = requestBody.getSpecs().get(0); + + String expectedDefaultKey = fileNameResolved.substring(0, fileNameResolved.lastIndexOf("-")); + + if (requestBody.getGroupId().equals(groupId) && + requestBody.getArtifactId().equals(artifactId) && + requestBody.getVersion().equals(version) && + uploadSpec.getKey().equals(expectedDefaultKey) && + uploadSpec.getName().equals(fileNameResolved) && + uploadSpec.getFileName().equals(fileNameResolved) && + uploadSpec.getOpenApi().length() > 0 + ) { + log.info(uploadSpec.getOpenApi()); + List result = getSampleBoatLintReports(expectedDefaultKey, reportId, reportGrade,Changes.COMPATIBLE,Severity.HINT); + return new MockResponse().setResponseCode(200).setBody(objectMapper.writeValueAsString(result)); + } else { + return new MockResponse().setResponseCode(400); + } + } + return new MockResponse().setResponseCode(404); + } + }; + + mockBackEnd.setDispatcher(dispatcher); + + mojo.execute(); + + File output = new File(mojo.getRadioOutput(), "radioOutput.json"); + + assertTrue(output.exists()); + + List result = Arrays.asList(objectMapper.readValue(output, BoatLintReport[].class)); + + assertEquals(1, result.size()); + assertEquals(reportId, result.get(0).getId()); + assertEquals(reportGrade, result.get(0).getGrade()); + + } + + @Test + void test_multiple_file_found_error() { + + final String portalKey = "example"; + final String sourceKey = "pet-store-bom"; + final String groupId = "com.backbase.boat.samples"; + final String artifactId = "pet-store-bom"; + final String version = "2021.09"; + final String fileNameRequest = "one-client-api-*.yaml"; + + SpecConfig specConfig = new SpecConfig(); + specConfig.setInputSpec(getFile("/bundler/folder/" + fileNameRequest)); + + RadioMojo mojo = new RadioMojo(); + mojo.setGroupId(groupId); + mojo.setArtifactId(artifactId); + mojo.setVersion(version); + mojo.setPortalKey(portalKey); + mojo.setSourceKey(sourceKey); + mojo.setSpecs(new SpecConfig[]{specConfig}); + mojo.setBoatBayUrl(String.format("http://localhost:%s", mockBackEnd.getPort())); + + final Dispatcher dispatcher = new Dispatcher() { + @SneakyThrows + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/api/boat/portals/" + portalKey + "/boat-maven-plugin/" + sourceKey + "/upload": + + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + + mockBackEnd.setDispatcher(dispatcher); + + assertThrows(MojoExecutionException.class, () -> mojo.execute()); + + } + + @Test + void test_no_file_found_error() { + + final String portalKey = "example"; + final String sourceKey = "pet-store-bom"; + final String groupId = "com.backbase.boat.samples"; + final String artifactId = "pet-store-bom"; + final String version = "2021.09"; + final String invalidFileName = "invalid-client-*.yaml"; + + SpecConfig specConfig = new SpecConfig(); + specConfig.setInputSpec(getFile("/bundler/folder/" + invalidFileName)); + + RadioMojo mojo = new RadioMojo(); + mojo.setGroupId(groupId); + mojo.setArtifactId(artifactId); + mojo.setVersion(version); + mojo.setPortalKey(portalKey); + mojo.setSourceKey(sourceKey); + mojo.setSpecs(new SpecConfig[]{specConfig}); + mojo.setBoatBayUrl(String.format("http://localhost:%s", mockBackEnd.getPort())); + + final Dispatcher dispatcher = new Dispatcher() { + @SneakyThrows + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/api/boat/portals/" + portalKey + "/boat-maven-plugin/" + sourceKey + "/upload": + + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + + mockBackEnd.setDispatcher(dispatcher); + + assertThrows(MojoExecutionException.class, () -> mojo.execute()); + + } + + @Test + void test_invalid_open_api_file() { + + final String portalKey = "example"; + final String sourceKey = "pet-store-bom"; + final String groupId = "com.backbase.boat.samples"; + final String artifactId = "pet-store-bom"; + final String version = "2021.09"; + final String invalidFile = "logback.xml"; + + SpecConfig specConfig = new SpecConfig(); + specConfig.setInputSpec(getFile("/" + invalidFile)); + + RadioMojo mojo = new RadioMojo(); + mojo.setGroupId(groupId); + mojo.setArtifactId(artifactId); + mojo.setVersion(version); + mojo.setPortalKey(portalKey); + mojo.setSourceKey(sourceKey); + mojo.setSpecs(new SpecConfig[]{specConfig}); + mojo.setBoatBayUrl(String.format("http://localhost:%s", mockBackEnd.getPort())); + + final Dispatcher dispatcher = new Dispatcher() { + @SneakyThrows + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/api/boat/portals/" + portalKey + "/boat-maven-plugin/" + sourceKey + "/upload": + + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + + mockBackEnd.setDispatcher(dispatcher); + + assertThrows(MojoExecutionException.class, () -> mojo.execute()); + + } + + @Test + void test_invalid_parent_folder() { + + final String portalKey = "example"; + final String sourceKey = "pet-store-bom"; + final String groupId = "com.backbase.boat.samples"; + final String artifactId = "pet-store-bom"; + final String version = "2021.09"; + final String validName = "one-client-api-v1.yaml"; + + SpecConfig specConfig = new SpecConfig(); + specConfig.setInputSpec(getFile("/invalid-parent/" + validName)); + + RadioMojo mojo = new RadioMojo(); + mojo.setGroupId(groupId); + mojo.setArtifactId(artifactId); + mojo.setVersion(version); + mojo.setPortalKey(portalKey); + mojo.setSourceKey(sourceKey); + mojo.setSpecs(new SpecConfig[]{specConfig}); + mojo.setBoatBayUrl(String.format("http://localhost:%s", mockBackEnd.getPort())); + + final Dispatcher dispatcher = new Dispatcher() { + @SneakyThrows + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/api/boat/portals/" + portalKey + "/boat-maven-plugin/" + sourceKey + "/upload": + + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + + mockBackEnd.setDispatcher(dispatcher); + + Exception exception = assertThrows(MojoExecutionException.class, () -> mojo.execute()); + + assertTrue(exception.getMessage().startsWith("Invalid parent spec folder")); + + } + + @SneakyThrows + @Test + void test_auth() { + + final String portalKey = "example"; + final String sourceKey = "pet-store-bom"; + final String specKey = "spec-key"; + final String groupId = "com.backbase.boat.samples"; + final String artifactId = "pet-store-bom"; + final String version = "2021.09"; + final String validName = "one-client-api-v1.yaml"; + final String username = "admin"; + final String password = "admin"; + final BigDecimal reportId = BigDecimal.valueOf(10); + final String reportGrade = "A"; + + SpecConfig specConfig = new SpecConfig(); + specConfig.setInputSpec(getFile("/bundler/folder/" + validName)); + + RadioMojo mojo = new RadioMojo(); + mojo.setGroupId(groupId); + mojo.setArtifactId(artifactId); + mojo.setVersion(version); + mojo.setPortalKey(portalKey); + mojo.setSourceKey(sourceKey); + mojo.setUsername(username); + mojo.setPassword(password); + mojo.setSpecs(new SpecConfig[]{specConfig}); + mojo.setBoatBayUrl(String.format("http://localhost:%s", mockBackEnd.getPort())); + + final Dispatcher dispatcher = new Dispatcher() { + @SneakyThrows + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/api/boat/portals/" + portalKey + "/boat-maven-plugin/" + sourceKey + "/upload": + + if(request.getHeader("Authorization")!=null && request.getHeader("Authorization").length()>0){ + List result = getSampleBoatLintReports(specKey, reportId, reportGrade,Changes.COMPATIBLE,Severity.HINT); + return new MockResponse().setResponseCode(200).setBody(objectMapper.writeValueAsString(result)); + } + return new MockResponse().setResponseCode(401); + } + return new MockResponse().setResponseCode(404); + } + }; + + mockBackEnd.setDispatcher(dispatcher); + + mojo.execute(); + + File output = new File(mojo.getRadioOutput(), "radioOutput.json"); + + assertTrue(output.exists()); + + } + + @SneakyThrows + @Test + void test_build_fail_on_breaking_changes() { + + final String portalKey = "example"; + final String sourceKey = "pet-store-bom"; + final String groupId = "com.backbase.boat.samples"; + final String artifactId = "pet-store-bom"; + final String specKey = "spec-key"; + final String version = "2021.09"; + final BigDecimal reportId = BigDecimal.valueOf(10); + final String reportGrade = "A"; + final String validName = "one-client-api-v1.yaml"; + + SpecConfig specConfig = new SpecConfig(); + specConfig.setInputSpec(getFile("/bundler/folder/" + validName)); + + RadioMojo mojo = new RadioMojo(); + mojo.setGroupId(groupId); + mojo.setArtifactId(artifactId); + mojo.setVersion(version); + mojo.setPortalKey(portalKey); + mojo.setSourceKey(sourceKey); + mojo.setSpecs(new SpecConfig[]{specConfig}); + mojo.setBoatBayUrl(String.format("http://localhost:%s", mockBackEnd.getPort())); + + final Dispatcher dispatcher = new Dispatcher() { + @SneakyThrows + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/api/boat/portals/" + portalKey + "/boat-maven-plugin/" + sourceKey + "/upload": + List result = getSampleBoatLintReports(specKey, reportId, reportGrade,Changes.BREAKING,Severity.HINT); + return new MockResponse().setResponseCode(200).setBody(objectMapper.writeValueAsString(result)); + } + return new MockResponse().setResponseCode(404); + } + }; + + mockBackEnd.setDispatcher(dispatcher); + + + // Build will not fail if failOnBreakingChange is false which is default. + mojo.execute(); + File output = new File(mojo.getRadioOutput(), "radioOutput.json"); + assertTrue(output.exists()); + + // Build will fail if failOnBreakingChange is true + mojo.setFailOnBreakingChange(true); + Exception exception = assertThrows(MojoFailureException.class, () -> mojo.execute()); + assertTrue(exception.getMessage().startsWith("Specs have Breaking Changes")); + + } + + @SneakyThrows + @Test + void test_build_fail_on_must_violation() { + + final String portalKey = "example"; + final String sourceKey = "pet-store-bom"; + final String groupId = "com.backbase.boat.samples"; + final String artifactId = "pet-store-bom"; + final String specKey = "spec-key"; + final String version = "2021.09"; + final BigDecimal reportId = BigDecimal.valueOf(10); + final String reportGrade = "A"; + final String validName = "one-client-api-v1.yaml"; + + SpecConfig specConfig = new SpecConfig(); + specConfig.setInputSpec(getFile("/bundler/folder/" + validName)); + + RadioMojo mojo = new RadioMojo(); + mojo.setGroupId(groupId); + mojo.setArtifactId(artifactId); + mojo.setVersion(version); + mojo.setPortalKey(portalKey); + mojo.setSourceKey(sourceKey); + mojo.setSpecs(new SpecConfig[]{specConfig}); + mojo.setBoatBayUrl(String.format("http://localhost:%s", mockBackEnd.getPort())); + + final Dispatcher dispatcher = new Dispatcher() { + @SneakyThrows + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/api/boat/portals/" + portalKey + "/boat-maven-plugin/" + sourceKey + "/upload": + List result = getSampleBoatLintReports(specKey, reportId, reportGrade,Changes.COMPATIBLE,Severity.MUST); + return new MockResponse().setResponseCode(200).setBody(objectMapper.writeValueAsString(result)); + } + return new MockResponse().setResponseCode(404); + } + }; + + mockBackEnd.setDispatcher(dispatcher); + + + // Build will not fail if failOnLintViolation is false which is default. + mojo.execute(); + File output = new File(mojo.getRadioOutput(), "radioOutput.json"); + assertTrue(output.exists()); + + // Build will fail if failOnLintViolation is true + mojo.setFailOnLintViolation(true); + Exception exception = assertThrows(MojoFailureException.class, () -> mojo.execute()); + assertTrue(exception.getMessage().startsWith("Specs have Must Violations")); + + } + + @SneakyThrows + @Test + void test_when_boat_bay_is_unavailable() { + + final String portalKey = "example"; + final String sourceKey = "pet-store-bom"; + final String groupId = "com.backbase.boat.samples"; + final String artifactId = "pet-store-bom"; + final String specKey = "spec-key"; + final String version = "2021.09"; + final BigDecimal reportId = BigDecimal.valueOf(10); + final String reportGrade = "A"; + final String validName = "one-client-api-v1.yaml"; + + SpecConfig specConfig = new SpecConfig(); + specConfig.setInputSpec(getFile("/bundler/folder/" + validName)); + + RadioMojo mojo = new RadioMojo(); + mojo.setGroupId(groupId); + mojo.setArtifactId(artifactId); + mojo.setVersion(version); + mojo.setPortalKey(portalKey); + mojo.setSourceKey(sourceKey); + mojo.setSpecs(new SpecConfig[]{specConfig}); + //Set invalid domain + mojo.setBoatBayUrl(String.format("http://invalid-domain:%s", mockBackEnd.getPort())); + + final Dispatcher dispatcher = new Dispatcher() { + @SneakyThrows + @Override + public MockResponse dispatch(RecordedRequest request) { + switch (request.getPath()) { + case "/api/boat/portals/" + portalKey + "/boat-maven-plugin/" + sourceKey + "/upload": + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + + mockBackEnd.setDispatcher(dispatcher); + + + // Build will fail, when failOnBoatBayErrorResponse is true (Defualt) + Exception exception = assertThrows(MojoFailureException.class, () -> mojo.execute()); + assertTrue(exception.getMessage().startsWith("BoatBay error")); + + // Build will not fail if failOnBoatBayErrorResponse is false + mojo.setFailOnBoatBayErrorResponse(false); + //No Exception is thrown + assertDoesNotThrow(() -> mojo.execute()); + File output = new File(mojo.getRadioOutput(), "radioOutput.json"); + //But No output file present + assertTrue(!output.exists()); + + } + + + private String getFile(String glob) { + return (new File("src/test/resources").getAbsolutePath() + glob); + } + + @NotNull + private List getSampleBoatLintReports(String expectedDefaultKey, BigDecimal reportId, String reportGrade, + Changes typeOfChange, Severity sampleSeverityInResponse) { + BoatLintReport boatLintReport = new BoatLintReport(); + boatLintReport.setId(reportId); + boatLintReport.setGrade(reportGrade); + boatLintReport.violations(List.of(BoatViolation.builder().severity(sampleSeverityInResponse).build())); + boatLintReport.setSpec(BoatSpec.builder().key(expectedDefaultKey).changes(typeOfChange).build()); + List result = new ArrayList<>(); + result.add(boatLintReport); + return result; + } + +} \ No newline at end of file diff --git a/boat-quay/boat-quay-lint/pom.xml b/boat-quay/boat-quay-lint/pom.xml index 93de07041..71eb784f4 100644 --- a/boat-quay/boat-quay-lint/pom.xml +++ b/boat-quay/boat-quay-lint/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss boat-quay - 0.14.13-SNAPSHOT + 0.15.0-SNAPSHOT boat-quay-lint jar diff --git a/boat-quay/boat-quay-rules/pom.xml b/boat-quay/boat-quay-rules/pom.xml index 6c2cc02bf..6aa1b391e 100644 --- a/boat-quay/boat-quay-rules/pom.xml +++ b/boat-quay/boat-quay-rules/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss boat-quay - 0.14.13-SNAPSHOT + 0.15.0-SNAPSHOT boat-quay-rules jar diff --git a/boat-quay/pom.xml b/boat-quay/pom.xml index 4bfbd5cac..b11807155 100644 --- a/boat-quay/pom.xml +++ b/boat-quay/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.14.13-SNAPSHOT + 0.15.0-SNAPSHOT diff --git a/boat-scaffold/pom.xml b/boat-scaffold/pom.xml index 7f5f65d1a..746f5b2b8 100644 --- a/boat-scaffold/pom.xml +++ b/boat-scaffold/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.14.13-SNAPSHOT + 0.15.0-SNAPSHOT boat-scaffold @@ -86,7 +86,7 @@ com.backbase.oss boat-trail-resources - 0.14.13-SNAPSHOT + 0.15.0-SNAPSHOT test diff --git a/boat-terminal/pom.xml b/boat-terminal/pom.xml index eab26cbc9..6d02ee95c 100644 --- a/boat-terminal/pom.xml +++ b/boat-terminal/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.14.13-SNAPSHOT + 0.15.0-SNAPSHOT boat-terminal diff --git a/boat-trail-resources/pom.xml b/boat-trail-resources/pom.xml index d310079f4..7adb6cfcf 100644 --- a/boat-trail-resources/pom.xml +++ b/boat-trail-resources/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.14.13-SNAPSHOT + 0.15.0-SNAPSHOT boat-trail-resources diff --git a/pom.xml b/pom.xml index d52b36c35..2a52649dc 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.backbase.oss backbase-openapi-tools - 0.14.13-SNAPSHOT + 0.15.0-SNAPSHOT pom Backbase Open Api Tools will help you converting RAML to OpenAPI plus many more diff --git a/tests/pom.xml b/tests/pom.xml index efe4e5f46..2486df527 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.14.13-SNAPSHOT + 0.15.0-SNAPSHOT tests