All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.microcks.util.openapi.OpenAPITestRunner Maven / Gradle / Ivy

The newest version!
/*
 * Copyright The Microcks Authors.
 *
 * 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.microcks.util.openapi;

import io.github.microcks.domain.Operation;
import io.github.microcks.domain.Request;
import io.github.microcks.domain.Resource;
import io.github.microcks.domain.ResourceType;
import io.github.microcks.domain.Response;
import io.github.microcks.domain.Service;
import io.github.microcks.domain.TestReturn;
import io.github.microcks.repository.ResourceRepository;
import io.github.microcks.repository.ResponseRepository;
import io.github.microcks.util.test.HttpTestRunner;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;

/**
 * This is an implementation of HttpTestRunner that deals with OpenAPI schema validation. This implementation now
 * manages the 2 flavors of OpenAPI: OpenAPI v3.x and Swagger v2.x.
 * @author laurent
 */
public class OpenAPITestRunner extends HttpTestRunner {

   /** A simple logger for diagnostic messages. */
   private static final Logger log = LoggerFactory.getLogger(OpenAPITestRunner.class);

   /** Content-types for JSON that are valid response types validated using JSON Schemas. */
   public static final Pattern APPLICATION_JSON_TYPES_PATTERN = Pattern.compile("application/.*json");

   private final ResourceRepository resourceRepository;
   private final ResponseRepository responseRepository;

   private final boolean validateResponseCode;


   /** The URL of resources used for validation. */
   private String resourceUrl = null;

   private List lastValidationErrors = null;

   /**
    * Build a new OpenAPITestRunner.
    * @param resourceRepository   Access to resources repository
    * @param responseRepository   Access to response repository
    * @param validateResponseCode whether to validate response code
    */
   public OpenAPITestRunner(ResourceRepository resourceRepository, ResponseRepository responseRepository,
         boolean validateResponseCode) {
      this.resourceRepository = resourceRepository;
      this.responseRepository = responseRepository;
      this.validateResponseCode = validateResponseCode;
   }

   /**
    * The URL of resources used for validation.
    * @return The URL of resources used for validation
    */
   public String getResourceUrl() {
      return resourceUrl;
   }

   /**
    * The URL of resources used for validation.
    * @param resourceUrl The URL of resources used for validation.
    */
   public void setResourceUrl(String resourceUrl) {
      this.resourceUrl = resourceUrl;
   }

   /**
    * Build the HttpMethod corresponding to string.
    */
   @Override
   public HttpMethod buildMethod(String method) {
      return HttpMethod.valueOf(method.toUpperCase());
   }

   @Override
   protected int extractTestReturnCode(Service service, Operation operation, Request request,
         ClientHttpResponse httpResponse, String responseContent) {
      int code = TestReturn.SUCCESS_CODE;

      int responseCode = 0;
      try {
         responseCode = httpResponse.getStatusCode().value();
         log.debug("Response status code: {}", responseCode);
      } catch (IOException ioe) {
         log.debug("IOException while getting raw status code in response", ioe);
         return TestReturn.FAILURE_CODE;
      }

      // Extract response content-type in any case.
      String contentType = null;
      if (httpResponse.getHeaders().getContentType() != null) {
         contentType = httpResponse.getHeaders().getContentType().toString();
         log.debug("Response media-type is {}", httpResponse.getHeaders().getContentType());
      }

      // If required, compare response code and content-type to expected ones.
      if (validateResponseCode) {
         Response expectedResponse = responseRepository.findById(request.getResponseId()).orElse(null);
         if (expectedResponse != null) {
            log.debug("Response expected status code: {}", expectedResponse.getStatus());
            if (!String.valueOf(responseCode).equals(expectedResponse.getStatus())) {
               log.debug("Response HttpStatus does not match expected one, returning failure");
               lastValidationErrors = List
                     .of(String.format("Response HttpStatus does not match expected one. Expecting %s but got %d",
                           expectedResponse.getStatus(), responseCode));
               return TestReturn.FAILURE_CODE;
            }

            if (expectedResponse.getMediaType() != null
                  && !contentTypesAreEquivalent(expectedResponse.getMediaType(), contentType)) {
               log.debug("Response Content-Type does not match expected one, returning failure");
               lastValidationErrors = List
                     .of(String.format("Response Content-Type does not match expected one. Expecting %s but got %s",
                           expectedResponse.getMediaType(), contentType));
               return TestReturn.FAILURE_CODE;
            }
         }
      }

      // Do not try to validate response content if no content provided ;-)
      // Also do not try to schema validate something that is not application/.*json for now...
      // Alternatives schemes are on their way for OpenAPI but not yet ready (see https://github.com/OAI/OpenAPI-Specification/pull/1736)
      String shortContentType = getShortContentType(contentType);
      if (responseCode != 204 && shortContentType != null
            && APPLICATION_JSON_TYPES_PATTERN.matcher(shortContentType).matches()) {

         boolean isOpenAPIv3 = true;

         // Retrieve the resource corresponding to OpenAPI specification if any.
         Resource openapiSpecResource = findResourceCandidate(service);
         if (openapiSpecResource == null) {
            log.debug("Found no OpenAPI specification resource for service {} - {}, so failing validating",
                  service.getId(), service.getName());
            return TestReturn.FAILURE_CODE;
         }

         // Check the type so guess the kind of validation.
         if (ResourceType.SWAGGER.equals(openapiSpecResource.getType())) {
            isOpenAPIv3 = false;
         }

         JsonNode openApiSpec = null;
         try {
            openApiSpec = OpenAPISchemaValidator.getJsonNodeForSchema(openapiSpecResource.getContent());
         } catch (IOException ioe) {
            log.debug("OpenAPI specification cannot be transformed into valid JsonNode schema, so failing");
            return TestReturn.FAILURE_CODE;
         }

         // Extract JsonNode corresponding to response.
         String verb = operation.getName().split(" ")[0].toLowerCase();
         String path = operation.getName().split(" ")[1].trim();

         // Get body content as a string.
         JsonNode contentNode = null;
         try {
            contentNode = OpenAPISchemaValidator.getJsonNode(responseContent);
         } catch (IOException ioe) {
            log.debug("Response body cannot be accessed or transformed as Json, returning failure");
            return TestReturn.FAILURE_CODE;
         }
         String jsonPointer = "/paths/" + path.replace("/", "~1") + "/" + verb + "/responses/" + responseCode;

         if (isOpenAPIv3) {
            lastValidationErrors = OpenAPISchemaValidator.validateJsonMessage(openApiSpec, contentNode, jsonPointer,
                  contentType, resourceUrl);
         } else {
            lastValidationErrors = SwaggerSchemaValidator.validateJsonMessage(openApiSpec, contentNode, jsonPointer,
                  resourceUrl);
         }

         if (!lastValidationErrors.isEmpty()) {
            log.debug("OpenAPI schema validation errors found {}, marking test as failed.",
                  lastValidationErrors.size());
            return TestReturn.FAILURE_CODE;
         }
         log.debug("OpenAPI schema validation of response is successful !");
      }
      return code;
   }

   @Override
   protected String extractTestReturnMessage(Service service, Operation operation, Request request,
         ClientHttpResponse httpResponse) {
      StringBuilder builder = new StringBuilder();
      if (lastValidationErrors != null && !lastValidationErrors.isEmpty()) {
         for (String error : lastValidationErrors) {
            builder.append(error).append('\n');
         }
      }
      // Reset just after consumption so avoid side-effects.
      lastValidationErrors = null;
      return builder.toString();
   }

   private boolean contentTypesAreEquivalent(String expectedContentType, String responseContentType) {
      if (responseContentType != null) {
         // If charset is specified, compare ignore case ignoring spaces.
         if (expectedContentType.contains("charset=")) {
            return expectedContentType.replace(" ", "").equalsIgnoreCase(responseContentType.replace(" ", ""));
         }
         // Sanitize charset information from media-type.
         String shortResponseContentType = getShortContentType(responseContentType);
         return expectedContentType.equalsIgnoreCase(shortResponseContentType);
      }
      return false;
   }

   private String getShortContentType(String contentType) {
      // Sanitize charset information from media-type.
      if (contentType != null && contentType.contains("charset=") && contentType.indexOf(";") > 0) {
         return contentType.substring(0, contentType.indexOf(";"));
      }
      return contentType;
   }

   private Resource findResourceCandidate(Service service) {
      Optional candidate = Optional.empty();
      // Try resources marked within mainArtifact first.
      List resources = resourceRepository.findMainByServiceId(service.getId());
      if (!resources.isEmpty()) {
         candidate = getResourceCandidate(resources);
      }
      // Else try all the services resources...
      if (candidate.isEmpty()) {
         resources = resourceRepository.findByServiceId(service.getId());
         if (!resources.isEmpty()) {
            candidate = getResourceCandidate(resources);
         }
      }
      return candidate.orElse(null);
   }

   private Optional getResourceCandidate(List resources) {
      return resources.stream()
            .filter(r -> ResourceType.OPEN_API_SPEC.equals(r.getType()) || ResourceType.SWAGGER.equals(r.getType()))
            .findFirst();
   }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy