/* * * Copyright 2018 Robert Winkler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * */ package io.github.robwin.swagger.test; import io.swagger.models.ArrayModel; import io.swagger.models.ComposedModel; import io.swagger.models.Info; import io.swagger.models.Model; import io.swagger.models.ModelImpl; import io.swagger.models.Operation; import io.swagger.models.Path; import io.swagger.models.RefModel; import io.swagger.models.Response; import io.swagger.models.Swagger; import io.swagger.models.parameters.BodyParameter; import io.swagger.models.parameters.CookieParameter; import io.swagger.models.parameters.FormParameter; import io.swagger.models.parameters.HeaderParameter; import io.swagger.models.parameters.Parameter; import io.swagger.models.parameters.PathParameter; import io.swagger.models.parameters.QueryParameter; import io.swagger.models.parameters.RefParameter; import io.swagger.models.properties.Property; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.assertj.core.api.SoftAssertions; class DocumentationDrivenValidator extends AbstractContractValidator { private static final String[] TYPE_DEFINING_PROPERTIES = {"type", "format"}; private SwaggerAssertionConfig assertionConfig; private SoftAssertions softAssertions; private PropertyValidator propertyValidator; private Swagger actual; private SchemaObjectResolver schemaObjectResolver; // provide means to fall back from local to global properties DocumentationDrivenValidator(Swagger actual, SwaggerAssertionConfig assertionConfig) { this.actual = actual; this.assertionConfig = assertionConfig; softAssertions = new SoftAssertions(); propertyValidator = new PropertyValidator(assertionConfig, softAssertions); } @Override public void validateSwagger(Swagger expected, SchemaObjectResolver schemaObjectResolver) { this.schemaObjectResolver = schemaObjectResolver; validateInfo(actual.getInfo(), expected.getInfo()); // Check Paths if (isAssertionEnabled(SwaggerAssertionType.PATHS)) { final Set<String> filter = assertionConfig.getPathsToIgnoreInExpected(); final Map<String, Path> expectedPaths = findExpectedPaths(expected, assertionConfig); final Map<String, Path> actualPaths = getPathsIncludingBasePath(actual); validatePaths(actualPaths, removeAllFromMap(expectedPaths, filter)); } // Check Definitions if (isAssertionEnabled(SwaggerAssertionType.DEFINITIONS)) { final Set<String> filter = assertionConfig.getDefinitionsToIgnoreInExpected(); validateDefinitions(actual.getDefinitions(), removeAllFromMap(expected.getDefinitions(), filter)); } softAssertions.assertAll(); } private void validateInfo(Info actualInfo, Info expectedInfo) { // Version. OFF by default. if (isAssertionEnabled(SwaggerAssertionType.VERSION)) { softAssertions.assertThat(actualInfo.getVersion()).as("Checking Version").isEqualTo(expectedInfo.getVersion()); } // Everything (but potentially brittle, therefore OFF by default) if (isAssertionEnabled(SwaggerAssertionType.INFO)) { softAssertions.assertThat(actualInfo).as("Checking Info").isEqualToComparingFieldByField(expectedInfo); } } private void validatePaths(Map<String, Path> actualPaths, Map<String, Path> expectedPaths) { if (MapUtils.isNotEmpty(expectedPaths)) { softAssertions.assertThat(actualPaths).as("Checking Paths").isNotEmpty(); if (MapUtils.isNotEmpty(actualPaths)) { softAssertions.assertThat(actualPaths.keySet()).as("Checking Paths").hasSameElementsAs(expectedPaths.keySet()); for (Map.Entry<String, Path> actualPathEntry : actualPaths.entrySet()) { Path expectedPath = expectedPaths.get(actualPathEntry.getKey()); Path actualPath = actualPathEntry.getValue(); String pathName = actualPathEntry.getKey(); validatePath(pathName, actualPath, expectedPath); } } } else { softAssertions.assertThat(actualPaths).as("Checking Paths").isNullOrEmpty(); } } private void validateDefinitions(Map<String, Model> actualDefinitions, Map<String, Model> expectedDefinitions) { if (MapUtils.isNotEmpty(expectedDefinitions)) { softAssertions.assertThat(actualDefinitions).as("Checking Definitions").isNotEmpty(); if (MapUtils.isNotEmpty(actualDefinitions)) { softAssertions.assertThat(actualDefinitions.keySet()).as("Checking Definitions").hasSameElementsAs(expectedDefinitions.keySet()); for (Map.Entry<String, Model> actualDefinitionEntry : actualDefinitions.entrySet()) { Model expectedDefinition = expectedDefinitions.get(actualDefinitionEntry.getKey()); Model actualDefinition = actualDefinitionEntry.getValue(); String definitionName = actualDefinitionEntry.getKey(); validateDefinition(definitionName, actualDefinition, expectedDefinition); } } } else { softAssertions.assertThat(actualDefinitions).as("Checking Definitions").isNullOrEmpty(); } } private void validatePath(String pathName, Path actualPath, Path expectedPath) { if (expectedPath != null) { softAssertions.assertThat(actualPath.getOperations()).as("Checking number of operations of path '%s'", pathName).hasSameSizeAs(actualPath.getOperations()); validateOperation(actualPath.getGet(), expectedPath.getGet(), pathName, "GET"); validateOperation(actualPath.getDelete(), expectedPath.getDelete(), pathName, "DELETE"); validateOperation(actualPath.getPost(), expectedPath.getPost(), pathName, "POST"); validateOperation(actualPath.getPut(), expectedPath.getPut(), pathName, "PUT"); validateOperation(actualPath.getPatch(), expectedPath.getPatch(), pathName, "PATCH"); validateOperation(actualPath.getOptions(), expectedPath.getOptions(), pathName, "OPTIONS"); } } private void validateDefinition(String definitionName, Model actualDefinition, Model expectedDefinition) { if (expectedDefinition != null && actualDefinition != null) { validateModel(actualDefinition, expectedDefinition, String.format("Checking model of definition '%s", definitionName)); validateDefinitionProperties(schemaObjectResolver.resolvePropertiesFromActual(actualDefinition), schemaObjectResolver.resolvePropertiesFromExpected(expectedDefinition), definitionName); if (expectedDefinition instanceof ModelImpl && actualDefinition instanceof ModelImpl) { validateTypeDefinition(actualDefinition, expectedDefinition); validateDefinitionEnum(actualDefinition, expectedDefinition); validateDefinitionRequiredProperties(((ModelImpl) actualDefinition).getRequired(), ((ModelImpl) expectedDefinition).getRequired(), definitionName); } } } private void validateTypeDefinition(Model actualDefinition, Model expectedDefinition) { ModelImpl expectedDefModelImpl = (ModelImpl) expectedDefinition; ModelImpl actualDefModelImpl = (ModelImpl) actualDefinition; softAssertions.assertThat(actualDefModelImpl) .isEqualToComparingOnlyGivenFields(expectedDefModelImpl, TYPE_DEFINING_PROPERTIES); } private void validateDefinitionEnum(Model actualDefinition, Model expectedDefinition) { ModelImpl expectedDefModelImpl = (ModelImpl) expectedDefinition; ModelImpl actualDefModelImpl = (ModelImpl) actualDefinition; List<String> actualEnums = actualDefModelImpl.getEnum(); List<String> expectedEnums = expectedDefModelImpl.getEnum(); if (CollectionUtils.isNotEmpty(expectedEnums)) { softAssertions.assertThat(actualEnums).hasSameElementsAs(expectedEnums); } else { softAssertions.assertThat(actualEnums).isNullOrEmpty(); } } private void validateDefinitionRequiredProperties(List<String> actualRequiredProperties, List<String> expectedRequiredProperties, String definitionName) { if (CollectionUtils.isNotEmpty(expectedRequiredProperties)) { softAssertions.assertThat(actualRequiredProperties).as("Checking required properties of definition '%s'", definitionName).isNotEmpty(); if (CollectionUtils.isNotEmpty(actualRequiredProperties)) { final Set<String> filteredExpectedProperties = filterWhitelistedPropertyNames(definitionName, new HashSet<>(expectedRequiredProperties)); softAssertions.assertThat(actualRequiredProperties).as("Checking required properties of definition '%s'", definitionName).hasSameElementsAs(filteredExpectedProperties); } } else { softAssertions.assertThat(actualRequiredProperties).as("Checking required properties of definition '%s'", definitionName).isNullOrEmpty(); } } private void validateModel(Model actualDefinition, Model expectedDefinition, String message) { if (isAssertionEnabled(SwaggerAssertionType.MODELS)) { if (expectedDefinition instanceof ModelImpl) { // TODO Validate ModelImpl softAssertions.assertThat(actualDefinition).as(message).isExactlyInstanceOf(ModelImpl.class); } else if (expectedDefinition instanceof RefModel) { // TODO Validate RefModel softAssertions.assertThat(actualDefinition).as(message).isExactlyInstanceOf(RefModel.class); } else if (expectedDefinition instanceof ArrayModel) { ArrayModel arrayModel = (ArrayModel) expectedDefinition; // TODO Validate ArrayModel softAssertions.assertThat(actualDefinition).as(message).isExactlyInstanceOf(ArrayModel.class); } else if (expectedDefinition instanceof ComposedModel) { ComposedModel composedModel = (ComposedModel) expectedDefinition; softAssertions.assertThat(actualDefinition).as(message).isInstanceOfAny(ComposedModel.class, ModelImpl.class); } else { // TODO Validate all model types softAssertions.assertThat(actualDefinition).isExactlyInstanceOf(expectedDefinition.getClass()); } } } private void validateDefinitionProperties(Map<String, Property> actualDefinitionProperties, Map<String, Property> expectedDefinitionProperties, String definitionName) { if (MapUtils.isNotEmpty(expectedDefinitionProperties)) { softAssertions.assertThat(actualDefinitionProperties).as("Checking properties of definition '%s", definitionName).isNotEmpty(); if (MapUtils.isNotEmpty(actualDefinitionProperties)) { final Set<String> filteredExpectedProperties = filterWhitelistedPropertyNames(definitionName, expectedDefinitionProperties.keySet()); softAssertions.assertThat(actualDefinitionProperties.keySet()).as("Checking properties of definition '%s'", definitionName).hasSameElementsAs(filteredExpectedProperties); for (Map.Entry<String, Property> actualDefinitionPropertyEntry : actualDefinitionProperties.entrySet()) { Property expectedDefinitionProperty = expectedDefinitionProperties.get(actualDefinitionPropertyEntry.getKey()); Property actualDefinitionProperty = actualDefinitionPropertyEntry.getValue(); String propertyName = actualDefinitionPropertyEntry.getKey(); validateProperty(actualDefinitionProperty, expectedDefinitionProperty, String.format("Checking property '%s' of definition '%s'", propertyName, definitionName)); } } } else { softAssertions.assertThat(actualDefinitionProperties).as("Checking properties of definition '%s", definitionName).isNullOrEmpty(); } } private void validateProperty(Property actualProperty, Property expectedProperty, String message) { propertyValidator.validateProperty(actualProperty, expectedProperty, message); } private void validateOperation(Operation actualOperation, Operation expectedOperation, String path, String httpMethod) { String message = String.format("Checking '%s' operation of path '%s'", httpMethod, path); if (expectedOperation != null) { softAssertions.assertThat(actualOperation).as(message).isNotNull(); if (actualOperation != null) { //Validate consumes validateList(schemaObjectResolver.getActualConsumes(actualOperation), schemaObjectResolver.getExpectedConsumes(expectedOperation), String.format("Checking '%s' of '%s' operation of path '%s'", "consumes", httpMethod, path)); //Validate produces validateList(schemaObjectResolver.getActualProduces(actualOperation), schemaObjectResolver.getExpectedProduces(expectedOperation), String.format("Checking '%s' of '%s' operation of path '%s'", "produces", httpMethod, path)); //Validate parameters validateParameters(actualOperation.getParameters(), expectedOperation.getParameters(), httpMethod, path); //Validate responses validateResponses(actualOperation.getResponses(), expectedOperation.getResponses(), httpMethod, path); } } else { softAssertions.assertThat(actualOperation).as(message).isNull(); } } private void validateParameters(List<Parameter> actualOperationParameters, List<Parameter> expectedOperationParameters, String httpMethod, String path) { String message = String.format("Checking parameters of '%s' operation of path '%s'", httpMethod, path); if (CollectionUtils.isNotEmpty(expectedOperationParameters)) { softAssertions.assertThat(actualOperationParameters).as(message).isNotEmpty(); if (CollectionUtils.isNotEmpty(actualOperationParameters)) { softAssertions.assertThat(actualOperationParameters).as(message).hasSameSizeAs(expectedOperationParameters); softAssertions.assertThat(actualOperationParameters).as(message).usingElementComparatorOnFields("in", "name", "required").hasSameElementsAs(expectedOperationParameters); Map<String, Parameter> expectedParametersAsMap = new HashMap<>(); for (Parameter expectedParameter : expectedOperationParameters) { expectedParametersAsMap.put(expectedParameter.getName(), expectedParameter); } for (Parameter actualParameter : actualOperationParameters) { String parameterName = actualParameter.getName(); Parameter expectedParameter = expectedParametersAsMap.get(parameterName); validateParameter(actualParameter, expectedParameter, parameterName, httpMethod, path); } } } else { softAssertions.assertThat(actualOperationParameters).as(message).isNullOrEmpty(); } } private void validateParameter(Parameter actualParameter, Parameter expectedParameter, String parameterName, String httpMethod, String path) { if (expectedParameter != null) { String message = String.format("Checking parameter '%s' of '%s' operation of path '%s'", parameterName, httpMethod, path); softAssertions.assertThat(actualParameter).as(message).isExactlyInstanceOf(expectedParameter.getClass()); if (expectedParameter instanceof BodyParameter && actualParameter instanceof BodyParameter) { BodyParameter actualBodyParameter = (BodyParameter) expectedParameter; BodyParameter expectedBodyParameter = (BodyParameter) expectedParameter; validateModel(actualBodyParameter.getSchema(), expectedBodyParameter.getSchema(), String.format("Checking model of parameter '%s' of '%s' operation of path '%s'", parameterName, httpMethod, path)); } else if (expectedParameter instanceof PathParameter && actualParameter instanceof PathParameter) { PathParameter actualPathParameter = (PathParameter) actualParameter; PathParameter expectedPathParameter = (PathParameter) expectedParameter; softAssertions.assertThat(actualPathParameter.getType()).as(message).isEqualTo(expectedPathParameter.getType()); List<String> expectedEnums = expectedPathParameter.getEnum(); if (CollectionUtils.isNotEmpty(expectedEnums)) { softAssertions.assertThat(actualPathParameter.getEnum()).as(message).hasSameElementsAs(expectedEnums); } else { softAssertions.assertThat(actualPathParameter.getEnum()).as(message).isNullOrEmpty(); } } else if (expectedParameter instanceof QueryParameter && actualParameter instanceof QueryParameter) { QueryParameter actualQueryParameter = (QueryParameter) actualParameter; QueryParameter expectedQueryParameter = (QueryParameter) expectedParameter; softAssertions.assertThat(actualQueryParameter.getType()).as(message).isEqualTo(expectedQueryParameter.getType()); List<String> expectedEnums = expectedQueryParameter.getEnum(); if (CollectionUtils.isNotEmpty(expectedEnums)) { softAssertions.assertThat(actualQueryParameter.getEnum()).as(message).hasSameElementsAs(expectedEnums); } else { softAssertions.assertThat(actualQueryParameter.getEnum()).as(message).isNullOrEmpty(); } } else if (expectedParameter instanceof HeaderParameter && actualParameter instanceof HeaderParameter) { HeaderParameter actualHeaderParameter = (HeaderParameter) actualParameter; HeaderParameter expectedHeaderParameter = (HeaderParameter) expectedParameter; softAssertions.assertThat(actualHeaderParameter.getType()).as(message).isEqualTo(expectedHeaderParameter.getType()); List<String> expectedEnums = expectedHeaderParameter.getEnum(); if (CollectionUtils.isNotEmpty(expectedEnums)) { softAssertions.assertThat(actualHeaderParameter.getEnum()).as(message).hasSameElementsAs(expectedEnums); } else { softAssertions.assertThat(actualHeaderParameter.getEnum()).as(message).isNullOrEmpty(); } } else if (expectedParameter instanceof FormParameter && actualParameter instanceof FormParameter) { FormParameter actualFormParameter = (FormParameter) actualParameter; FormParameter expectedFormParameter = (FormParameter) expectedParameter; softAssertions.assertThat(actualFormParameter.getType()).as(message).isEqualTo(expectedFormParameter.getType()); List<String> expectedEnums = expectedFormParameter.getEnum(); if (CollectionUtils.isNotEmpty(expectedEnums)) { softAssertions.assertThat(actualFormParameter.getEnum()).as(message).hasSameElementsAs(expectedEnums); } else { softAssertions.assertThat(actualFormParameter.getEnum()).as(message).isNullOrEmpty(); } } else if (expectedParameter instanceof CookieParameter && actualParameter instanceof CookieParameter) { CookieParameter actualCookieParameter = (CookieParameter) actualParameter; CookieParameter expectedCookieParameter = (CookieParameter) expectedParameter; softAssertions.assertThat(actualCookieParameter.getType()).as(message).isEqualTo(expectedCookieParameter.getType()); List<String> expectedEnums = expectedCookieParameter.getEnum(); if (CollectionUtils.isNotEmpty(expectedEnums)) { softAssertions.assertThat(actualCookieParameter.getEnum()).as(message).hasSameElementsAs(expectedEnums); } else { softAssertions.assertThat(actualCookieParameter.getEnum()).as(message).isNullOrEmpty(); } } else if (expectedParameter instanceof RefParameter && actualParameter instanceof RefParameter) { RefParameter expectedRefParameter = (RefParameter) expectedParameter; RefParameter actualRefParameter = (RefParameter) actualParameter; softAssertions.assertThat(actualRefParameter.getSimpleRef()).as(message).isEqualTo(expectedRefParameter.getSimpleRef()); } } } private void validateResponses(Map<String, Response> actualOperationResponses, Map<String, Response> expectedOperationResponses, String httpMethod, String path) { String message = String.format("Checking responses of '%s' operation of path '%s'", httpMethod, path); if (MapUtils.isNotEmpty(expectedOperationResponses)) { softAssertions.assertThat(actualOperationResponses).as(message).isNotEmpty(); if (MapUtils.isNotEmpty(actualOperationResponses)) { validateResponseByConfig(actualOperationResponses, expectedOperationResponses, message); for (Map.Entry<String, Response> actualResponseEntry : actualOperationResponses.entrySet()) { Response expectedResponse = expectedOperationResponses.get(actualResponseEntry.getKey()); Response actualResponse = actualResponseEntry.getValue(); String responseName = actualResponseEntry.getKey(); validateResponse(actualResponse, expectedResponse, responseName, httpMethod, path); } } } else { softAssertions.assertThat(actualOperationResponses).as(message).isNullOrEmpty(); } } private void validateResponseByConfig(Map<String, Response> actualOperationResponses, Map<String, Response> expectedOperationResponses, String message) { if(isAssertionEnabled(SwaggerAssertionType.STRICT_VALIDATION_ON_PATH)) { softAssertions.assertThat(actualOperationResponses.keySet()).as(message).hasSameElementsAs(expectedOperationResponses.keySet()); } else { softAssertions.assertThat(actualOperationResponses.keySet()).as(message).containsAll(expectedOperationResponses.keySet()); } } private void validateResponse(Response actualResponse, Response expectedResponse, String responseName, String httpMethod, String path) { if (expectedResponse != null) { validateProperty(actualResponse.getSchema(), expectedResponse.getSchema(), String.format("Checking response schema of response '%s' of '%s' operation of path '%s'", responseName, httpMethod, path)); validateResponseHeaders(actualResponse.getHeaders(), expectedResponse.getHeaders(), responseName, httpMethod, path); } } private void validateResponseHeaders(Map<String, Property> actualResponseHeaders, Map<String, Property> expectedResponseHeaders, String responseName, String httpMethod, String path) { String message = String.format("Checking response headers of response '%s' of '%s' operation of path '%s'", responseName, httpMethod, path); if (MapUtils.isNotEmpty(expectedResponseHeaders)) { softAssertions.assertThat(actualResponseHeaders).as(message).isNotEmpty(); if (MapUtils.isNotEmpty(actualResponseHeaders)) { softAssertions.assertThat(actualResponseHeaders.keySet()).as(message).hasSameElementsAs(expectedResponseHeaders.keySet()); for (Map.Entry<String, Property> actualResponseHeaderEntry : actualResponseHeaders.entrySet()) { Property expectedResponseHeader = expectedResponseHeaders.get(actualResponseHeaderEntry.getKey()); Property actualResponseHeader = actualResponseHeaderEntry.getValue(); String responseHeaderName = actualResponseHeaderEntry.getKey(); validateProperty(actualResponseHeader, expectedResponseHeader, String.format("Checking response header '%s' of response '%s' of '%s' operation of path '%s'", responseHeaderName, responseName, httpMethod, path)); } } } else { softAssertions.assertThat(actualResponseHeaders).as(message).isNullOrEmpty(); } } private void validateList(List<String> actualList, List<String> expectedList, String message) { if (CollectionUtils.isNotEmpty(expectedList)) { softAssertions.assertThat(actualList).as(message).isNotEmpty(); if (CollectionUtils.isNotEmpty(actualList)) { softAssertions.assertThat(actualList).as(message).hasSameElementsAs(expectedList); } } else { softAssertions.assertThat(actualList).as(message).isNullOrEmpty(); } } private boolean isAssertionEnabled(final SwaggerAssertionType assertionType) { return assertionConfig.swaggerAssertionEnabled(assertionType); } private Set<String> filterWhitelistedPropertyNames(String definitionName, Set<String> expectedPropertyNames) { Set<String> result = new HashSet<>(expectedPropertyNames.size()); final Set<String> ignoredPropertyNames = assertionConfig.getPropertiesToIgnoreInExpected(); for (String property : expectedPropertyNames) { if (!ignoredPropertyNames.contains(definitionName + '.' + property)) { result.add(property); } } return result; } private <K, V> Map<K, V> removeAllFromMap(Map<K, V> map, Set<K> keysToExclude) { final LinkedHashMap<K, V> result = new LinkedHashMap<>(map); result.keySet().removeAll(keysToExclude); return result; } }