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

io.github.microcks.minion.async.AsyncAPITestManager 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.minion.async;

import io.github.microcks.domain.EventMessage;
import io.github.microcks.domain.TestReturn;
import io.github.microcks.minion.async.client.MicrocksAPIConnector;
import io.github.microcks.minion.async.client.dto.TestCaseReturnDTO;
import io.github.microcks.minion.async.consumer.AMQPMessageConsumptionTask;
import io.github.microcks.minion.async.consumer.AmazonSNSMessageConsumptionTask;
import io.github.microcks.minion.async.consumer.AmazonSQSMessageConsumptionTask;
import io.github.microcks.minion.async.consumer.ConsumedMessage;
import io.github.microcks.minion.async.consumer.GooglePubSubMessageConsumptionTask;
import io.github.microcks.minion.async.consumer.KafkaMessageConsumptionTask;
import io.github.microcks.minion.async.consumer.MQTTMessageConsumptionTask;
import io.github.microcks.minion.async.consumer.MessageConsumptionTask;
import io.github.microcks.minion.async.consumer.WebSocketMessageConsumptionTask;
import io.github.microcks.minion.async.consumer.NATSMessageConsumptionTask;
import io.github.microcks.util.SchemaMap;
import io.github.microcks.util.asyncapi.AsyncAPISchemaValidator;

import com.fasterxml.jackson.databind.JsonNode;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Manager that takes care of launching and running an AsyncAPI test from an
 * AsyncTestSpecification.
 * @author laurent
 */
@ApplicationScoped
public class AsyncAPITestManager {

   @Inject
   @RestClient
   MicrocksAPIConnector microcksAPIConnector;

   @Inject
   SchemaRegistry schemaRegistry;

   @ConfigProperty(name = "io.github.microcks.minion.async.client.MicrocksAPIConnector/mp-rest/url")
   String microcksUrl;

   /**
    * Launch a new test using this specification. This is a fire and forget operation.
    * Tests results will be reported later once finished using a MicrocksAPIConnector.
    * @param specification The specification of Async test to launch.
    */
   public void launchTest(AsyncTestSpecification specification) {
      AsyncAPITestThread thread = new AsyncAPITestThread(specification);
      thread.start();
   }

   /**
    * Actually run the Async test by instantiating a MessageConsumptionTask,
    * gathering its outputs and validating them against an AsyncAPI schema.
    */
   class AsyncAPITestThread extends Thread {

      /** Get a JBoss logging logger. */
      private final Logger logger = Logger.getLogger(getClass());

      private final AsyncTestSpecification specification;

      private final ExecutorService executorService = Executors.newSingleThreadExecutor();

      private long startTime;

      /**
       * Build a new test thread from a test specification.
       * @param specification The specification for async test to run.
       */
      public AsyncAPITestThread(AsyncTestSpecification specification) {
         this.specification = specification;
      }

      @Override
      public void run() {
         logger.infof("Launching a new AsyncAPITestThread for {%s} on {%s}",
               specification.getTestResultId(), specification.getEndpointUrl());

         // Start initialising a TestCaseReturn and build the correct MessageConsumptionTasK.
         TestCaseReturnDTO testCaseReturn = new TestCaseReturnDTO(specification.getOperationName());
         MessageConsumptionTask messageConsumptionTask = buildMessageConsumptionTask(specification);

         // Actually start test counter.
         startTime = System.currentTimeMillis();
         List outputs = null;

         if (messageConsumptionTask != null) {
            try {
               logger.debugf("Starting consuming messages for {%d} ms", specification.getTimeoutMS());
               // Adding an extra seconds to allow to close and clean all the machinery ;-)
               outputs = executorService.invokeAny(Collections.singletonList(messageConsumptionTask),
                     specification.getTimeoutMS() + 1000L, TimeUnit.MILLISECONDS);
               logger.debugf("Consumption ends and we got {%d} messages to validate", outputs.size());
            } catch (InterruptedException e) {
               logger.infof("AsyncAPITestThread for {%s} was interrupted", specification.getTestResultId());
            } catch (ExecutionException e) {
               logger.errorf(e, "AsyncAPITestThread for {%s} raise an ExecutionException", specification.getTestResultId());
               testCaseReturn.addTestReturn(
                     new TestReturn(TestReturn.FAILURE_CODE, specification.getTimeoutMS(),
                           "ExecutionException: no message received in " + specification.getTimeoutMS() + " ms",
                           null, null)
               );
            } catch (TimeoutException e) {
               // Message consumption has timed-out, add an empty test return with failure and message.
               logger.infof("AsyncAPITestThread for {%s} was timed-out", specification.getTestResultId());
               testCaseReturn.addTestReturn(
                     new TestReturn(TestReturn.FAILURE_CODE, specification.getTimeoutMS(),
                           "Timeout: no message received in " + specification.getTimeoutMS() + " ms",
                           null, null)
               );
            } catch (Throwable t) {
               // We faced a low-level issue... add an empty test return with failure and message.
               logger.error("Caught a low-level throwable", t);
               testCaseReturn.addTestReturn(
                     new TestReturn(TestReturn.FAILURE_CODE, System.currentTimeMillis() - startTime,
                           "Exception: low-level failure: " + t.getMessage(),
                           null, null)
               );
            } finally {
               if (messageConsumptionTask != null) {
                  try {
                     messageConsumptionTask.close();
                  } catch (Throwable t) {
                     // This was best effort, just ignore the exception...
                  }
               }
               executorService.shutdown();
            }
         } else {
            logger.errorf("Found no suitable MessageConsumptionTask implementation. {%s} is not a supported endpoint", specification.getEndpointUrl());
            testCaseReturn.addTestReturn(
                  new TestReturn(TestReturn.FAILURE_CODE, System.currentTimeMillis() - startTime,
                        "Exception: found no suitable MessageConsumptionTask implementation for endpoint",
                        null, null)
            );
         }

         // Validate all the received outputs if any.
         if (outputs != null && !outputs.isEmpty()) {
            validateConsumedMessage(testCaseReturn, outputs);
         } else {
            logger.infof("No consumed message to validate, test {%s} will be marked as timed-out", specification.getTestResultId());
         }

         // Finally, report the testCase results using Microcks API.
         microcksAPIConnector.reportTestCaseResult(specification.getTestResultId(), testCaseReturn);
      }

      /**
       * Validate consumed messages and complete {@code testCaseReturn} with {@code TestReturn} objects.
       * @param testCaseReturn The TestCase to complete with schema validation results
       * @param outputs The consumed messages from tested endpoint.
       */
      private void validateConsumedMessage(TestCaseReturnDTO testCaseReturn, List outputs) {
         JsonNode specificationNode = null;
         try {
            specificationNode = AsyncAPISchemaValidator.getJsonNodeForSchema(specification.getAsyncAPISpec());
         } catch (IOException e) {
            logger.errorf("Retrieval of AsyncAPI schema for validation fails for {%s}", specification.getTestResultId());
         }

         // Compute operation JSON pointer to navigate the spec.
         String[] operationElements = specification.getOperationName().split(" ");
         String operationNamePtr = "/channels/" + operationElements[1].replaceAll("/", "~1");
         if ("SUBSCRIBE".equals(operationElements[0])) {
            operationNamePtr += "/subscribe";
         } else {
            operationNamePtr += "/publish";
         }
         String messagePathPointer = operationNamePtr + "/message";

         // Retrieve expected content type from specification and produce a schema registry snapshot.
         String expectedContentType = null;
         SchemaMap schemaMap = new SchemaMap();
         if (specificationNode != null) {
            expectedContentType = getExpectedContentType(specificationNode, messagePathPointer);
            if (expectedContentType.contains("avro")) {
               logger.debug("Expected content type is Avro so extracting service resources into a SchemaMap");
               schemaRegistry.updateRegistryForService(specification.getServiceId());
               schemaRegistry.getSchemaEntries(specification.getServiceId())
                     .stream().forEach(schemaEntry -> schemaMap.putSchemaEntry(schemaEntry.getPath(), schemaEntry.getContent()));
            }
         }

         for (int i=0; i errors = null;
                  if (Constants.AVRO_BINARY_CONTENT_TYPES.contains(expectedContentType)) {
                     // Use the correct Avro message validation method depending on what has been read.
                     if (message.getPayloadRecord() != null) {
                        errors = AsyncAPISchemaValidator.validateAvroMessage(specificationNode, message.getPayloadRecord(),
                              messagePathPointer, schemaMap);
                     } else {
                        errors = AsyncAPISchemaValidator.validateAvroMessage(specificationNode, message.getPayload(),
                              messagePathPointer, schemaMap);
                     }
                  } else if (expectedContentType.contains("application/json")) {
                     // Now parse the payloadNode and validate it according the operation message
                     // found in specificationNode.
                     JsonNode payloadNode = AsyncAPISchemaValidator.getJsonNode(responseContent);
                     errors = AsyncAPISchemaValidator.validateJsonMessage(specificationNode, payloadNode, messagePathPointer, microcksUrl + "/api/resources/");
                  }

                  if (errors == null || errors.isEmpty()) {
                     // This is a success.
                     logger.infof("No errors found while validating message payload! Reporting a success for test {%s}", specification.getTestResultId());
                     testReturn = new TestReturn(TestReturn.SUCCESS_CODE, elapsedTime, eventMessage);
                  } else {
                     // This is a failure. Records all errors using \\n as delimiter.
                     logger.infof("Validation errors found... Reporting a failure for test {%s}", specification.getTestResultId());
                     testReturn = new TestReturn(TestReturn.FAILURE_CODE, elapsedTime, String.join("\\n", errors), eventMessage);
                  }
               } catch (IOException e) {
                  logger.error("Exception while parsing the output message", e);
                  testReturn = new TestReturn(TestReturn.FAILURE_CODE, elapsedTime,
                        "Message content cannot be parsed as JSON", eventMessage);
               }
            }
            testCaseReturn.addTestReturn(testReturn);
         }
      }

      /** Retrieve the expected content type for an AsyncAPI message. */
      private String getExpectedContentType(JsonNode specificationNode, String messagePathPointer) {
         // Retrieve default content type, defaulting to application/json.
         String defaultContentType = specificationNode.path("defaultContentType").asText("application/json");

         // Get message real content type if defined.
         String contentType = defaultContentType;
         JsonNode messageNode = specificationNode.at(messagePathPointer);
         // If it's a $ref, then navigate to it.
         if (messageNode.has("$ref")) {
            // $ref: '#/components/messages/lightMeasured'
            String ref = messageNode.path("$ref").asText();
            messageNode = messageNode.at(ref.substring(1));
         }
         if (messageNode.has("contentType")) {
            contentType = messageNode.path("contentType").asText();
         }
         return contentType;
      }

      /** Find the appropriate MessageConsumptionTask implementation depending on specification. */
      private MessageConsumptionTask buildMessageConsumptionTask(AsyncTestSpecification testSpecification) {
         if (KafkaMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) {
            return new KafkaMessageConsumptionTask(testSpecification);
         }
         if (MQTTMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) {
            return new MQTTMessageConsumptionTask(testSpecification);
         }
         if (NATSMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) {
            return new NATSMessageConsumptionTask(testSpecification);
         }
         if (WebSocketMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) {
            return new WebSocketMessageConsumptionTask(testSpecification);
         }
         if (AMQPMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) {
            return new AMQPMessageConsumptionTask(testSpecification);
         }
         if (GooglePubSubMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) {
            return new GooglePubSubMessageConsumptionTask(testSpecification);
         }
         if (AmazonSQSMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) {
            return new AmazonSQSMessageConsumptionTask(testSpecification);
         }
         if (AmazonSNSMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) {
            return new AmazonSNSMessageConsumptionTask(testSpecification);
         }
         return null;
      }
   }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy