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

io.github.microcks.service.TestRunnerService 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.service;

import io.github.microcks.domain.OAuth2AuthorizedClient;
import io.github.microcks.domain.OAuth2ClientContext;
import io.github.microcks.domain.Operation;
import io.github.microcks.domain.Request;
import io.github.microcks.domain.Response;
import io.github.microcks.domain.Secret;
import io.github.microcks.domain.Service;
import io.github.microcks.domain.TestCaseResult;
import io.github.microcks.domain.TestResult;
import io.github.microcks.domain.TestReturn;
import io.github.microcks.domain.TestRunnerType;
import io.github.microcks.domain.TestStepResult;
import io.github.microcks.event.TestCompletionEvent;
import io.github.microcks.repository.RequestRepository;
import io.github.microcks.repository.ResourceRepository;
import io.github.microcks.repository.ResponseRepository;
import io.github.microcks.repository.SecretRepository;
import io.github.microcks.repository.TestResultRepository;
import io.github.microcks.security.AuthorizationException;
import io.github.microcks.security.OAuth2AuthorizedClientProvider;
import io.github.microcks.util.IdBuilder;
import io.github.microcks.util.asyncapi.AsyncAPITestRunner;
import io.github.microcks.util.graphql.GraphQLTestRunner;
import io.github.microcks.util.grpc.GrpcTestRunner;
import io.github.microcks.util.openapi.OpenAPITestRunner;
import io.github.microcks.util.postman.PostmanTestStepsRunner;
import io.github.microcks.util.soapui.SoapUIAssertionsTestRunner;
import io.github.microcks.util.test.AbstractTestRunner;
import io.github.microcks.util.test.HttpTestRunner;
import io.github.microcks.util.test.SoapHttpTestRunner;

import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.scheduling.annotation.Async;

import javax.net.ssl.SSLContext;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

/**
 * Bean managing the launch of new Tests.
 * @author laurent
 */
@org.springframework.stereotype.Service
public class TestRunnerService {

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

   /** Constant representing the header line in a custom CA Cert in PEM format. */
   private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----";
   /** Constant representing the footer line in a custom CA Cert in PEM format. */
   private static final String END_CERTIFICATE = "-----END CERTIFICATE-----";

   private final ResourceRepository resourceRepository;
   private final RequestRepository requestRepository;
   private final ResponseRepository responseRepository;
   private final TestResultRepository testResultRepository;
   private final SecretRepository secretRepository;
   private final ApplicationContext applicationContext;

   @Value("${tests-callback.url}")
   private String testsCallbackUrl = null;

   @Value("${postman-runner.url}")
   private String postmanRunnerUrl = null;

   @Value("${async-minion.url}")
   private String asyncMinionUrl = null;

   @Value("${validation.resourceUrl}")
   private String validationResourceUrl = null;

   /**
    * Build a new TestRunnerService with the required dependencies.
    * @param resourceRepository   The repository to manage persistent resources
    * @param requestRepository    The repository to manage persistent requests
    * @param responseRepository   The repository to manage persistent responses
    * @param testResultRepository The repository to manage persistent testResults
    * @param secretRepository     The repository to manage persistent secrets
    * @param applicationContext   The Spring application context
    */
   public TestRunnerService(ResourceRepository resourceRepository, RequestRepository requestRepository,
         ResponseRepository responseRepository, TestResultRepository testResultRepository,
         SecretRepository secretRepository, ApplicationContext applicationContext) {
      this.resourceRepository = resourceRepository;
      this.requestRepository = requestRepository;
      this.responseRepository = responseRepository;
      this.testResultRepository = testResultRepository;
      this.secretRepository = secretRepository;
      this.applicationContext = applicationContext;
   }

   /**
    * Launch tests using asynchronous/completable future pattern.
    * @param testResult    TestResults to aggregate results within
    * @param service       Service to test
    * @param runnerType    Type of runner for launching the tests
    * @param oAuth2Context An optional OAuth2ClientContext that may complement Secret information
    * @return A Future wrapping test results
    */
   @Async
   public CompletableFuture launchTestsInternal(TestResult testResult, Service service,
         TestRunnerType runnerType, OAuth2ClientContext oAuth2Context) {
      // Found next build number for this test.
      List older = testResultRepository.findByServiceId(service.getId(),
            PageRequest.of(0, 2, Sort.Direction.DESC, "testNumber"));
      if (older != null && !older.isEmpty() && older.get(0).getTestNumber() != null) {
         testResult.setTestNumber(older.get(0).getTestNumber() + 1L);
      } else {
         testResult.setTestNumber(1L);
      }

      Secret secret = null;
      if (testResult.getSecretRef() != null) {
         secret = secretRepository.findById(testResult.getSecretRef().getSecretId()).orElse(null);
         log.debug("Using a secret to test endpoint? '{}'", secret != null ? secret.getName() : "none");
      }

      if (oAuth2Context != null) {
         log.debug("Applying OAuth2 grant before actually running the test '{}'", oAuth2Context.getGrantType());
         OAuth2AuthorizedClientProvider tokenProvider = new OAuth2AuthorizedClientProvider();
         try {
            OAuth2AuthorizedClient authorizedClient = tokenProvider.authorize(oAuth2Context);
            testResult.setAuthorizedClient(authorizedClient);
            if (secret != null) {
               log.debug("Updating the Secret with token from OAuth2 for '{}'", authorizedClient.getPrincipalName());
               secret.setToken(authorizedClient.getEncodedAccessToken());
               secret.setTokenHeader(null);
            } else {
               secret = new Secret();
               secret.setToken(authorizedClient.getEncodedAccessToken());
            }
         } catch (AuthorizationException authorizationException) {
            log.error("OAuth2 token flow '{}' failed with: {}", oAuth2Context.getGrantType(),
                  authorizationException.getMessage());
            log.error("Marking the test as a failure before cancelling it");
            // Set flags and add to results before exiting loop.
            testResult.setSuccess(false);
            testResult.setInProgress(false);
            testResult.setElapsedTime(0);
            testResultRepository.save(testResult);
            return CompletableFuture.completedFuture(testResult);
         }
      }

      // Initialize runner once as it is shared for each test.
      AbstractTestRunner testRunner = retrieveRunner(runnerType, secret, testResult.getTimeout(),
            service.getId());
      if (testRunner == null) {
         // Set failure and stopped flags and save before exiting.
         testResult.setSuccess(false);
         testResult.setInProgress(false);
         testResult.setElapsedTime(0);
         testResultRepository.save(testResult);
         return CompletableFuture.completedFuture(testResult);
      }

      for (TestCaseResult testCaseResult : testResult.getTestCaseResults()) {
         // Retrieve operation corresponding to testCase.
         Operation operation = service.getOperations().stream()
               .filter(o -> o.getName().equals(testCaseResult.getOperationName())).findFirst().get();
         String testCaseId = IdBuilder.buildTestCaseId(testResult, operation);

         // Prepare collection of requests to launch.
         List requests = requestRepository.findByOperationId(IdBuilder.buildOperationId(service, operation));
         requests = cloneRequestsForTestCase(requests, testCaseId);

         List results = new ArrayList<>();
         try {
            HttpMethod method = testRunner.buildMethod(operation.getMethod());
            results = testRunner.runTest(service, operation, testResult, requests, testResult.getTestedEndpoint(),
                  method);
         } catch (URISyntaxException use) {
            log.error("URISyntaxException on endpoint {}, aborting current tests", testResult.getTestedEndpoint(), use);
            // Set flags and add to results before exiting loop.
            testCaseResult.setSuccess(false);
            testCaseResult.setElapsedTime(0);
            testResultRepository.save(testResult);
            break;
         } catch (Throwable t) {
            log.error("Throwable while testing operation {}", operation.getName(), t);
         }

         // Update result if we got returns. If no returns, it means that there's no
         // sample request for that operation -> mark it as failed.
         if (results == null) {
            testCaseResult.setSuccess(false);
            testCaseResult.setElapsedTime(0);
            testResultRepository.save(testResult);
         } else if (!results.isEmpty()) {
            updateTestCaseResultWithReturns(testCaseResult, results, testCaseId);
            testResultRepository.save(testResult);
         }
      }

      // Update success, progress indicators and total time before saving and returning.
      updateTestResult(testResult);

      return CompletableFuture.completedFuture(testResult);
   }


   /**
    *
    */
   private void updateTestCaseResultWithReturns(TestCaseResult testCaseResult, List testReturns,
         String testCaseId) {
      // Prepare a bunch of flag we're going to complete.
      boolean successFlag = true;
      long caseElapsedTime = 0;
      List responses = new ArrayList<>();
      List actualRequests = new ArrayList<>();

      for (TestReturn testReturn : testReturns) {
         // Deal with elapsed time and success flag.
         caseElapsedTime += testReturn.getElapsedTime();
         TestStepResult testStepResult = testReturn.buildTestStepResult();
         if (!testStepResult.isSuccess()) {
            successFlag = false;
         }

         // Extract, complete and store response and request.
         testReturn.getResponse().setTestCaseId(testCaseId);
         testReturn.getRequest().setTestCaseId(testCaseId);
         responses.add(testReturn.getResponse());
         actualRequests.add(testReturn.getRequest());

         testCaseResult.getTestStepResults().add(testStepResult);
      }

      // Save the responses into repository to get their ids.
      log.debug("Saving {} responses with testCaseId {}", responses.size(), testCaseId);
      responseRepository.saveAll(responses);

      // Associate responses to requests before saving requests.
      for (int i = 0; i < actualRequests.size(); i++) {
         actualRequests.get(i).setResponseId(responses.get(i).getId());
      }
      log.debug("Saving {} requests with testCaseId {}", responses.size(), testCaseId);
      requestRepository.saveAll(actualRequests);

      // Update and save the completed TestCaseResult.
      // We cannot consider as success if we have no TestStepResults associated...
      if (!testCaseResult.getTestStepResults().isEmpty()) {
         testCaseResult.setSuccess(successFlag);
      }
      testCaseResult.setElapsedTime(caseElapsedTime);
   }

   /**
    *
    */
   private void updateTestResult(TestResult testResult) {
      log.debug("Updating total testResult");
      // Update success, progress indicators and total time before saving and returning.
      boolean globalSuccessFlag = true;
      boolean globalProgressFlag = false;
      long totalElapsedTime = 0;
      for (TestCaseResult testCaseResult : testResult.getTestCaseResults()) {
         totalElapsedTime += testCaseResult.getElapsedTime();
         if (!testCaseResult.isSuccess()) {
            globalSuccessFlag = false;
         }
         // -1 is default elapsed time for testcase so its mean that still in
         // progress because not updated yet.
         if (testCaseResult.getElapsedTime() == -1) {
            log.debug("testCaseResult.elapsedTime is -1, set globalProgressFlag to true");
            globalProgressFlag = true;
         }
      }

      // Update aggregated flags before saving whole testResult.
      testResult.setSuccess(globalSuccessFlag);
      testResult.setInProgress(globalProgressFlag);
      testResult.setElapsedTime(totalElapsedTime);

      testResultRepository.save(testResult);

      // If test is completed, publish a completion event.
      if (!testResult.isInProgress()) {
         publishTestCompletionEvent(testResult);
      }
   }

   /** Clone and prepare request for a test case usage. */
   private List cloneRequestsForTestCase(List requests, String testCaseId) {
      List result = new ArrayList<>();
      for (Request request : requests) {
         Request clone = new Request();
         clone.setName(request.getName());
         clone.setContent(request.getContent());
         clone.setHeaders(request.getHeaders());
         clone.setQueryParameters(request.getQueryParameters());
         clone.setResponseId(request.getResponseId());
         // Assign testCaseId.
         clone.setTestCaseId(testCaseId);
         result.add(clone);
      }
      return result;
   }

   /** Retrieve correct test runner according given type. */
   private AbstractTestRunner retrieveRunner(TestRunnerType runnerType, Secret secret, Long runnerTimeout,
         String serviceId) {
      // TODO: remove this ugly initialization later.
      // Initialize new HttpComponentsClientHttpRequestFactory that supports https connections.
      SSLContext sslContext = null;
      try {
         // Initialize trusting material depending on Secret content.
         if (secret != null && secret.getCaCertPem() != null) {
            log.debug("Test Secret contains a CA Cert, installing certificate into SSLContext");
            sslContext = SSLContexts.custom()
                  .loadTrustMaterial(buildCustomCaCertTruststore(secret.getCaCertPem()), null).build();
         } else {
            log.debug("No Test Secret or no CA Cert found, installing accept everything strategy");
            sslContext = SSLContexts.custom().loadTrustMaterial(null, (cert, authType) -> true).build();
         }
      } catch (Exception e) {
         log.error("Exception while building SSLContext with acceptingTrustStrategy", e);
         return null;
      }
      SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,
            new String[] { "TLSv1.2", "TLSv1.3" }, null, NoopHostnameVerifier.INSTANCE);

      // BasicHttpClientConnectionManager was facing issues detecting close connections and re-creating new ones.
      // Switching to PoolingHttpClientConnectionManager for HC 5.2 solves this issue.
      final PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
            .setSSLSocketFactory(sslsf).build();

      CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager)
            .setDefaultRequestConfig(
                  RequestConfig.custom().setResponseTimeout(Timeout.ofMilliseconds(runnerTimeout)).build())
            .build();

      HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
      factory.setConnectTimeout(200);

      switch (runnerType) {
         case SOAP_HTTP:
            SoapHttpTestRunner soapRunner = new SoapHttpTestRunner(resourceRepository);
            soapRunner.setClientHttpRequestFactory(factory);
            soapRunner.setResourceUrl(validationResourceUrl);
            soapRunner.setSecret(secret);
            return soapRunner;
         case OPEN_API_SCHEMA:
            OpenAPITestRunner openApiRunner = new OpenAPITestRunner(resourceRepository, responseRepository, true);
            openApiRunner.setClientHttpRequestFactory(factory);
            openApiRunner.setResourceUrl(validationResourceUrl);
            openApiRunner.setSecret(secret);
            return openApiRunner;
         case ASYNC_API_SCHEMA:
            AsyncAPITestRunner asyncApiRunner = new AsyncAPITestRunner(resourceRepository, secretRepository);
            asyncApiRunner.setClientHttpRequestFactory(factory);
            asyncApiRunner.setAsyncMinionUrl(asyncMinionUrl);
            return asyncApiRunner;
         case GRPC_PROTOBUF:
            GrpcTestRunner grpcRunner = new GrpcTestRunner(resourceRepository);
            grpcRunner.setSecret(secret);
            grpcRunner.setTimeout(runnerTimeout);
            return grpcRunner;
         case GRAPHQL_SCHEMA:
            GraphQLTestRunner graphqlRunner = new GraphQLTestRunner(resourceRepository);
            graphqlRunner.setClientHttpRequestFactory(factory);
            graphqlRunner.setSecret(secret);
            return graphqlRunner;
         case POSTMAN:
            PostmanTestStepsRunner postmanRunner = new PostmanTestStepsRunner(resourceRepository);
            postmanRunner.setClientHttpRequestFactory(factory);
            postmanRunner.setTestsCallbackUrl(testsCallbackUrl);
            postmanRunner.setPostmanRunnerUrl(postmanRunnerUrl);
            return postmanRunner;
         case SOAP_UI:
            SoapUIAssertionsTestRunner soapUIRunner = new SoapUIAssertionsTestRunner(resourceRepository);
            soapUIRunner.setClientHttpRequestFactory(factory);
            soapUIRunner.setResourceUrl(validationResourceUrl);
            soapUIRunner.setSecret(secret);
            return soapUIRunner;
         default:
            HttpTestRunner httpRunner = new HttpTestRunner();
            httpRunner.setClientHttpRequestFactory(factory);
            httpRunner.setSecret(secret);
            return httpRunner;
      }
   }

   /** Build a customer truststore with provided certificate in PEM format. */
   private KeyStore buildCustomCaCertTruststore(String caCertPem) throws Exception {
      // First compute a stripped PEM certificate and decode it from base64.
      String strippedPem = caCertPem.replace(BEGIN_CERTIFICATE, "").replace(END_CERTIFICATE, "");
      InputStream is = new ByteArrayInputStream(org.apache.commons.codec.binary.Base64.decodeBase64(strippedPem));

      // Generate a new x509 certificate from the stripped decoded pem.
      CertificateFactory cf = CertificateFactory.getInstance("X.509");
      X509Certificate caCert = (X509Certificate) cf.generateCertificate(is);

      KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
      ks.load(null); // You don't need the KeyStore instance to come from a file.
      ks.setCertificateEntry("caCert", caCert);

      return ks;
   }

   /** Publish a TestCompletionEvent towards asynchronous consumers. */
   private void publishTestCompletionEvent(TestResult testResult) {
      TestCompletionEvent event = new TestCompletionEvent(this, testResult);
      applicationContext.publishEvent(event);
      log.debug("Test completion event has been published");
   }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy