Skip to content

Commit

Permalink
Merging down #74 into main (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
CheesyBoy123 authored Aug 3, 2024
1 parent c185a17 commit fb7baab
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 27 deletions.
35 changes: 19 additions & 16 deletions src/main/java/io/vertx/openapi/contract/OpenAPIContract.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,34 +108,37 @@ static Future<OpenAPIContract> from(Vertx vertx, JsonObject unresolvedContract,
ContextInternal ctx = (ContextInternal) vertx.getOrCreateContext();
Promise<OpenAPIContract> promise = ctx.promise();

version.getRepository(vertx, baseUri).compose(repository -> {
List<Future<SchemaRepository>> validationFutures = new ArrayList<>(additionalContractFiles.size());
version.getRepository(vertx, baseUri)
.compose(repository -> {
List<Future<?>> validationFutures = new ArrayList<>(additionalContractFiles.size());
for (String ref : additionalContractFiles.keySet()) {
// Todo: As soon a more modern Java version is used the validate part could be extracted in a private static
// method and reused below.
Future<SchemaRepository> validationFuture = version.validate(vertx, repository,
additionalContractFiles.get(ref)).map(res -> {
try {
res.checkValidity();
return repository.dereference(ref, JsonSchema.of(ref, additionalContractFiles.get(ref)));
} catch (JsonSchemaValidationException e) {
String msg = "Found issue in specification for reference: " + ref;
throw createInvalidContract(msg, e);
}
});
JsonObject file = additionalContractFiles.get(ref);
Future<?> validationFuture = version.validateAdditionalContractFile(vertx, repository, file)
.compose(v -> vertx.executeBlocking(() -> repository.dereference(ref, JsonSchema.of(ref, file))));

validationFutures.add(validationFuture);
}
return Future.all(validationFutures).map(repository);
}).compose(repository ->
version.validate(vertx, repository, unresolvedContract).compose(res -> {
version.validateContract(vertx, repository, unresolvedContract).compose(res -> {
try {
res.checkValidity();
return version.resolve(vertx, repository, unresolvedContract);
} catch (JsonSchemaValidationException e) {
} catch (JsonSchemaValidationException | UnsupportedOperationException e) {
return failedFuture(createInvalidContract(null, e));
}
}).map(resolvedSpec -> (OpenAPIContract) new OpenAPIContractImpl(resolvedSpec, version, repository))
).onComplete(promise);
})
.map(resolvedSpec -> (OpenAPIContract) new OpenAPIContractImpl(resolvedSpec, version, repository))
).recover(e -> {
//Convert any non-openapi exceptions into an OpenAPIContractException
if(e instanceof OpenAPIContractException) {
return failedFuture(e);
}

return failedFuture(createInvalidContract("Found issue in specification for reference: " + e.getMessage(), e));
}).onComplete(promise);

return promise.future();
}
Expand Down
35 changes: 32 additions & 3 deletions src/main/java/io/vertx/openapi/contract/OpenAPIVersion.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.json.schema.*;
import io.vertx.json.schema.Draft;
import io.vertx.json.schema.JsonFormatValidator;
import io.vertx.json.schema.JsonSchema;
import io.vertx.json.schema.JsonSchemaOptions;
import io.vertx.json.schema.JsonSchemaValidationException;
import io.vertx.json.schema.OutputUnit;
import io.vertx.json.schema.SchemaRepository;
import io.vertx.openapi.impl.OpenAPIFormatValidator;

import java.util.ArrayList;
Expand Down Expand Up @@ -73,13 +79,36 @@ public static OpenAPIVersion fromContract(JsonObject contract) {
}
}

public Future<OutputUnit> validate(Vertx vertx, SchemaRepository repo, JsonObject contract) {
public Future<OutputUnit> validateContract(Vertx vertx, SchemaRepository repo, JsonObject contract) {
return vertx.executeBlocking(() -> repo.validator(mainSchemaFile).validate(contract));
}

/**
* Validates additional contract files against the openapi schema. If validations fails, try to validate against the
* json schema specifications only.
*
* @param vertx The related Vert.x instance.
* @param repo The SchemaRepository to do the validations with.
* @param file The additional json contract to validate.
*/
public Future<Void> validateAdditionalContractFile(Vertx vertx, SchemaRepository repo, JsonObject file) {
return vertx.executeBlocking(() -> repo.validator(draft.getIdentifier()).validate(file))
.compose(this::checkOutputUnit)
.mapEmpty();
}

private Future<Void> checkOutputUnit(OutputUnit ou) {
try {
ou.checkValidity();
return Future.succeededFuture();
} catch (JsonSchemaValidationException e) {
return Future.failedFuture(e);
}
}

public Future<JsonObject> resolve(Vertx vertx, SchemaRepository repo, JsonObject contract) {
return vertx.executeBlocking(() -> {
JsonSchema schema =JsonSchema.of(contract);
JsonSchema schema = JsonSchema.of(contract);
repo.dereference(schema);
return repo.resolve(contract);
});
Expand Down
68 changes: 66 additions & 2 deletions src/test/java/io/vertx/tests/contract/OpenAPIContractTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package io.vertx.tests.contract;

import com.google.common.collect.ImmutableMap;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
Expand All @@ -38,6 +39,7 @@
import static com.google.common.truth.Truth.assertThat;
import static io.vertx.tests.ResourceHelper.getRelatedTestResourcePath;
import static io.vertx.tests.ResourceHelper.loadJson;
import static org.junit.jupiter.api.Assertions.assertTrue;

@ExtendWith(VertxExtension.class)
class OpenAPIContractTest {
Expand Down Expand Up @@ -82,7 +84,7 @@ void testFromWithPath(String path, JsonObject expected, Vertx vertx, VertxTestCo
@Timeout(value = 2, timeUnit = TimeUnit.SECONDS)
@MethodSource
void testFromWithPathAndAdditionalContractFiles(String path, Map<String, String> additionalFiles, JsonObject expected,
Vertx vertx, VertxTestContext testContext) {
Vertx vertx, VertxTestContext testContext) {
OpenAPIContract.from(vertx, path, additionalFiles)
.onComplete(testContext.succeeding(contract -> testContext.verify(() -> {
assertThat(contract.getRawContract()).isEqualTo(expected);
Expand Down Expand Up @@ -126,7 +128,8 @@ void testInvalidAdditionalSpecFiles(Vertx vertx, VertxTestContext testContext) {
.onComplete(testContext.failing(t -> testContext.verify(() -> {
assertThat(t).isInstanceOf(OpenAPIContractException.class);
String expectedErrorMessage =
"The passed OpenAPI contract is invalid: Found issue in specification for reference: https://example.com/petstore";
"The passed OpenAPI contract is invalid: Found issue in specification for reference: " +
"Can't resolve 'https://example.com/petstore#/components/schemas/Pet', only internal refs are supported.";
assertThat(t).hasMessageThat().isEqualTo(expectedErrorMessage);
testContext.completeNow();
})));
Expand All @@ -152,4 +155,65 @@ void testSplitSpec(Vertx vertx, VertxTestContext testContext) {
testContext.completeNow();
})));
}

@Test
@Timeout(value = 2, timeUnit = TimeUnit.SECONDS)
void testValidJsonSchemaProvidedAsAdditionalSpecFiles(Vertx vertx, VertxTestContext testContext) {
Path resourcePath = getRelatedTestResourcePath(OpenAPIContractTest.class).resolve("split");
JsonObject contract = loadJson(vertx, resourcePath.resolve("petstore.json"));
JsonObject invalidComponents = loadJson(vertx, resourcePath.resolve("validJsonSchemaComponents.json"));
JsonObject validComponents = loadJson(vertx, resourcePath.resolve("components.json"));

Map<String, JsonObject> additionalValidSpecFiles = ImmutableMap.of("https://example.com/petstore", validComponents);
Map<String, JsonObject> additionalInvalidSpecFiles = ImmutableMap.of("https://example.com/petstore", invalidComponents);

OpenAPIContract.from(vertx, contract.copy(), additionalValidSpecFiles)
.compose(validResp -> Future.succeededFuture(validResp.getRawContract()))
.onSuccess(validJsonRef -> OpenAPIContract.from(vertx, contract.copy(), additionalInvalidSpecFiles)
.onSuccess(splitResp -> testContext.verify(() -> {
assertThat(splitResp.getRawContract()).isEqualTo(validJsonRef);
testContext.completeNow();
}))
.onFailure(testContext::failNow))
.onFailure(testContext::failNow);
}

@Test
@Timeout(value = 2, timeUnit = TimeUnit.SECONDS)
void testMalformedJsonSchemaProvidedAsAdditionalSpecFiles(Vertx vertx, VertxTestContext testContext) {
Path resourcePath = getRelatedTestResourcePath(OpenAPIContractTest.class).resolve("split");
JsonObject contract = loadJson(vertx, resourcePath.resolve("petstore.json"));
JsonObject malformedComponents = loadJson(vertx, resourcePath.resolve("malformedComponents.json"));

Map<String, JsonObject> additionalMalformedSpecFiles = ImmutableMap.of("https://example.com/petstore", malformedComponents);

OpenAPIContract.from(vertx, contract.copy(), additionalMalformedSpecFiles)
.onComplete(handler -> testContext.verify(() -> {
assertTrue(handler.failed());
assertThat(handler.cause()).isInstanceOf(OpenAPIContractException.class);
assertThat(handler.cause()).hasMessageThat()
.isEqualTo("The passed OpenAPI contract is invalid: Found issue in specification for reference:" +
" -1 is less than 0");
testContext.completeNow();
}));
}

@Test
@Timeout(value = 2, timeUnit = TimeUnit.SECONDS)
public void testAdditionalSchemaFiles(Vertx vertx, VertxTestContext testContext) {
Path resourcePath = getRelatedTestResourcePath(OpenAPIContractTest.class).resolve("additional_schema_files");
Path contractPath = resourcePath.resolve("openapi.yaml");
Path componentsPath = resourcePath.resolve("name.yaml");
JsonObject dereferenced = loadJson(vertx, resourcePath.resolve("dereferenced.json"));

Map<String, String> additionalSpecFiles = ImmutableMap.of("https://schemas/Name.yaml", componentsPath.toString());
OpenAPIContract.from(vertx, contractPath.toString(), additionalSpecFiles)
.onComplete(testContext.succeeding(c -> {
testContext.verify(() -> {
assertThat(c.getRawContract().toString()).isEqualTo(dereferenced.toString());
testContext.completeNow();
});
}));
}

}
36 changes: 30 additions & 6 deletions src/test/java/io/vertx/tests/contract/OpenAPIVersionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.json.schema.JsonSchema;
import io.vertx.json.schema.JsonSchemaValidationException;
import io.vertx.json.schema.OutputUnit;
import io.vertx.json.schema.SchemaRepository;
import io.vertx.junit5.Timeout;
Expand Down Expand Up @@ -43,6 +44,8 @@
import static io.vertx.openapi.impl.Utils.EMPTY_JSON_OBJECT;
import static io.vertx.openapi.contract.OpenAPIVersion.V3_0;
import static io.vertx.openapi.contract.OpenAPIVersion.V3_1;
import static io.vertx.tests.ResourceHelper.getRelatedTestResourcePath;
import static io.vertx.tests.ResourceHelper.loadJson;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.jupiter.api.Assertions.assertThrows;

Expand All @@ -58,7 +61,7 @@ private static Stream<Arguments> provideVersionAndSpec() {
}

private static Stream<Arguments> provideVersionAndInvalidSpec() {
Path basePath = ResourceHelper.getRelatedTestResourcePath(OpenAPIVersionTest.class);
Path basePath = getRelatedTestResourcePath(OpenAPIVersionTest.class);

Function<String, Consumer<OutputUnit>> buildValidator = expectedString -> ou -> {
String error = ou.getErrors().stream().map(OutputUnit::getError).collect(Collectors.joining());
Expand All @@ -75,9 +78,9 @@ private static Stream<Arguments> provideVersionAndInvalidSpec() {
@ParameterizedTest(name = "{index} should validate a contract against OpenAPI version {0}")
@MethodSource(value = "provideVersionAndSpec")
@Timeout(value = 2, timeUnit = SECONDS)
void testValidate(OpenAPIVersion version, Path contractFile, Vertx vertx, VertxTestContext testContext) {
void validateContractTest(OpenAPIVersion version, Path contractFile, Vertx vertx, VertxTestContext testContext) {
JsonObject contract = vertx.fileSystem().readFileBlocking(contractFile.toString()).toJsonObject();
version.getRepository(vertx, DUMMY_BASE_URI).compose(repo -> version.validate(vertx, repo, contract))
version.getRepository(vertx, DUMMY_BASE_URI).compose(repo -> version.validateContract(vertx, repo, contract))
.onComplete(testContext.succeeding(res -> {
testContext.verify(() -> assertThat(res.getValid()).isTrue());
testContext.completeNow();
Expand All @@ -87,10 +90,10 @@ void testValidate(OpenAPIVersion version, Path contractFile, Vertx vertx, VertxT
@ParameterizedTest(name = "{index} should validate an invalid contract against OpenAPI version {0} and find errors")
@MethodSource(value = "provideVersionAndInvalidSpec")
@Timeout(value = 2, timeUnit = SECONDS)
void testValidateError(OpenAPIVersion version, Path contractFile, Consumer<OutputUnit> validator, Vertx vertx,
VertxTestContext testContext) {
void validateContractTestError(OpenAPIVersion version, Path contractFile, Consumer<OutputUnit> validator, Vertx vertx,
VertxTestContext testContext) {
JsonObject contract = vertx.fileSystem().readFileBlocking(contractFile.toString()).toJsonObject();
version.getRepository(vertx, DUMMY_BASE_URI).compose(repo -> version.validate(vertx, repo, contract))
version.getRepository(vertx, DUMMY_BASE_URI).compose(repo -> version.validateContract(vertx, repo, contract))
.onComplete(testContext.succeeding(res -> {
testContext.verify(() -> validator.accept(res));
testContext.completeNow();
Expand Down Expand Up @@ -145,4 +148,25 @@ void testFromSpecException() {
assertThrows(OpenAPIContractException.class, () -> OpenAPIVersion.fromContract(unsupportedContract),
expectedUnsupportedMsg);
}
@ParameterizedTest(name = "{index} should be able to validate additional files against the json schema for {0}")
@EnumSource(OpenAPIVersion.class)
@Timeout(value = 2, timeUnit = SECONDS)
public void testValidationOfAdditionalSchemaFiles(OpenAPIVersion version, Vertx vertx, VertxTestContext testContext) {
Path path = getRelatedTestResourcePath(OpenAPIVersionTest.class).resolve("split");
JsonObject validJsonSchema = loadJson(vertx, path.resolve("validJsonSchemaComponents.json"));
JsonObject malformedJsonSchema = loadJson(vertx, path.resolve("malformedComponents.json"));


version.getRepository(vertx, "https://vertx.io")
.onSuccess(repository -> version.validateAdditionalContractFile(vertx, repository, validJsonSchema)
.onFailure(testContext::failNow)
.onSuccess(ignored -> version.validateAdditionalContractFile(vertx, repository, malformedJsonSchema)
.onComplete(handler -> testContext.verify(() -> {
assertThat(handler.failed()).isTrue();
assertThat(handler.cause()).isInstanceOf(JsonSchemaValidationException.class);
assertThat(handler.cause()).hasMessageThat().isEqualTo("-1 is less than 0");
testContext.completeNow();
}))));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"components": {
"schemas": {
"RequestBody": {
"type": "object",
"properties": {
"name": {
"$ref": "https://schemas/Name.yaml"
}
}
}
}
},
"openapi": "3.1.0",
"paths": {
"/v1/post": {
"post": {
"summary": "Some POST request",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "app:///#/components/schemas/RequestBody"
}
}
}
},
"operationId": "postBody",
"responses": {
"200": {
"description": "Success"
},
"default": {
"description": "An unexpected error occurred"
}
}
}
}
},
"info": {
"title": "My Service",
"version": "1.0.0"
},
"tags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
$schema: https://spec.openapis.org/oas/3.1/dialect/base
description: A case-insensitive string of 1-255 characters, serving as a name (unique identifier)
type: string
minLength: 1
maxLength: 255
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
openapi: 3.1.0
info:
title: My Service
version: 1.0.0
tags: []
paths:
/v1/post:
post:
summary: Some POST request
operationId: postBody
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RequestBody'
responses:
200:
description: Success
default:
description: An unexpected error occurred
components:
schemas:
RequestBody:
type: object
properties:
name:
description: The unique name of the object
$ref: 'https://schemas/Name.yaml'
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"title": "This is an invalid json schema",
"description": "Max length cannot be negative",
"type": "string",
"maxLength": -1
}
Loading

0 comments on commit fb7baab

Please sign in to comment.