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

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

/*
 * Licensed to Laurent Broudoux (the "Author") under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. Author licenses this
 * file to you 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 com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.github.microcks.domain.*;
import io.github.microcks.repository.ResourceRepository;
import io.github.microcks.repository.ResponseRepository;
import io.github.microcks.util.test.HttpTestRunner;
import io.github.microcks.util.test.TestReturn;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;
import java.io.StringWriter;
import java.util.List;

/**
 * This is an implementation of HttpTestRunner that deals with OpenAPI schema validation.
 * @author laurent
 */
public class OpenAPITestRunner extends HttpTestRunner {

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

   private ResourceRepository resourceRepository;
   private ResponseRepository responseRepository;
   private boolean validateResponseCode = false;

   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;
   }

   /**
    * Build the HttpMethod corresponding to string.
    */
   @Override
   public HttpMethod buildMethod(String method){
      return HttpMethod.resolve(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.getRawStatusCode();
         log.debug("Response status code : " + responseCode);
      } catch (IOException ioe) {
         log.debug("IOException while getting raw status code in response", ioe);
         return TestReturn.FAILURE_CODE;
      }

      // If required, compare response code and content-type to expected ones.
      if (validateResponseCode) {
         Response expectedResponse = responseRepository.findById(request.getResponseId()).orElse(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");
            return TestReturn.FAILURE_CODE;
         }

         log.debug("Response media-type is {}", httpResponse.getHeaders().getContentType().toString());
         // Sanitize charset information from media-type.
         String contentType = httpResponse.getHeaders().getContentType().toString();
         if (contentType.contains("charset=") && contentType.indexOf(";") > 0) {
            contentType = contentType.substring(0, contentType.indexOf(";"));
         }
         if (!expectedResponse.getMediaType().equalsIgnoreCase(contentType)) {
            log.debug("Response Content-Type does not match expected one, returning failure");
         }
      }

      // Retrieve the resource corresponding to OpenAPI specification if any.
      Resource openapiSpecResource = null;
      List resources = resourceRepository.findByServiceId(service.getId());
      for (Resource resource : resources) {
         if (ResourceType.OPEN_API_SPEC.equals(resource.getType())) {
            openapiSpecResource = resource;
            break;
         }
      }
      if (openapiSpecResource == null) {
         log.debug("Do not found any OpenAPI specification resource for service {0}, so failing validating", service.getId());
         return TestReturn.FAILURE_CODE;
      }

      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();

      // Sanitize charset information from media-type.
      String contentType = httpResponse.getHeaders().getContentType().toString();
      if (contentType.contains("charset=") && contentType.indexOf(";") > 0) {
         contentType = contentType.substring(0, contentType.indexOf(";"));
      }
      log.debug("Response media-type is {}", contentType);

      String pointer = "/paths/" + path.replace("/", "~1") + "/" + verb
            + "/responses/" + responseCode + "/content/" + contentType.replace("/", "~1");
      log.debug("Looking for responseNode at " + pointer);
      JsonNode responseNode = openapiSpec.at(pointer);
      log.debug("responseNode: " + responseNode);

      // Is there a specified responseNode for this type ??
      if (responseNode != null && !responseNode.isMissingNode()) {
         // 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;
         }

         // Build a schema object with responseNode schema as root and by importing
         // all the common parts that may be referenced by references.
         JsonNode schemaNode = responseNode.path("schema").deepCopy();
         ((ObjectNode) schemaNode).set("components", openapiSpec.path("components").deepCopy());

         lastValidationErrors = OpenAPISchemaValidator.validateJson(schemaNode, contentNode);
         if (!lastValidationErrors.isEmpty()) {
            log.debug("OpenAPI schema validation errors found " + lastValidationErrors.size() + ", marking test as failed.");
            return TestReturn.FAILURE_CODE;
         }
         log.debug("OpenAPI schema validation of response is successful !");
      } else {
         // Do we still have a response body ??
         if (httpResponse.getHeaders().getContentLength() > 0) {
            log.debug("No response expected or defined but response has content, failing");
            code = TestReturn.FAILURE_CODE;
         }
      }
      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();
   }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy