io.specto.hoverfly.junit.core.Hoverfly.java Source code

Java tutorial

Introduction

Here is the source code for io.specto.hoverfly.junit.core.Hoverfly.java

Source

/**
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this classpath except in compliance with
 * the License. You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.
 * <p>
 * Copyright 2016-2016 SpectoLabs Ltd.
 */
package io.specto.hoverfly.junit.core;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import io.specto.hoverfly.junit.api.HoverflyClient;
import io.specto.hoverfly.junit.api.HoverflyClientException;
import io.specto.hoverfly.junit.api.model.ModeArguments;
import io.specto.hoverfly.junit.api.view.HoverflyInfoView;
import io.specto.hoverfly.junit.core.config.HoverflyConfiguration;
import io.specto.hoverfly.junit.core.model.Journal;
import io.specto.hoverfly.junit.core.model.Request;
import io.specto.hoverfly.junit.core.model.RequestResponsePair;
import io.specto.hoverfly.junit.core.model.Simulation;
import io.specto.hoverfly.junit.dsl.RequestMatcherBuilder;
import io.specto.hoverfly.junit.dsl.StubServiceBuilder;
import io.specto.hoverfly.junit.verification.VerificationCriteria;
import io.specto.hoverfly.junit.verification.VerificationData;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.StartedProcess;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

import static io.specto.hoverfly.junit.core.HoverflyConfig.configs;
import static io.specto.hoverfly.junit.core.HoverflyMode.CAPTURE;
import static io.specto.hoverfly.junit.core.HoverflyUtils.checkPortInUse;
import static io.specto.hoverfly.junit.dsl.matchers.HoverflyMatchers.any;
import static io.specto.hoverfly.junit.verification.HoverflyVerifications.atLeastOnce;
import static io.specto.hoverfly.junit.verification.HoverflyVerifications.never;
import static io.specto.hoverfly.junit.verification.HoverflyVerifications.times;

/**
 * A wrapper class for the Hoverfly binary.  Manage the lifecycle of the processes, and then manage Hoverfly itself by using it's API endpoints.
 */
// TODO extract interface and create LocalHoverfly and RemoteHoverfly
public class Hoverfly implements AutoCloseable {

    private static final Logger LOGGER = LoggerFactory.getLogger(Hoverfly.class);
    private static final ObjectWriter JSON_PRETTY_PRINTER = new ObjectMapper().writerWithDefaultPrettyPrinter();
    private static final int BOOT_TIMEOUT_SECONDS = 10;
    private static final int RETRY_BACKOFF_INTERVAL_MS = 100;

    private final HoverflyConfiguration hoverflyConfig;
    private final HoverflyMode hoverflyMode;
    private final ProxyConfigurer proxyConfigurer;
    private final SslConfigurer sslConfigurer = new SslConfigurer();
    private final HoverflyClient hoverflyClient;

    private final TempFileManager tempFileManager = new TempFileManager();
    private StartedProcess startedProcess;
    private boolean useDefaultSslCert = true;

    /**
     * Instantiates {@link Hoverfly}
     *
     * @param hoverflyConfigBuilder the config
     * @param hoverflyMode   the mode
     */
    public Hoverfly(HoverflyConfig hoverflyConfigBuilder, HoverflyMode hoverflyMode) {
        hoverflyConfig = hoverflyConfigBuilder.build();
        this.proxyConfigurer = new ProxyConfigurer(hoverflyConfig);
        this.hoverflyClient = HoverflyClient.custom().scheme(hoverflyConfig.getScheme())
                .host(hoverflyConfig.getHost()).port(hoverflyConfig.getAdminPort()).withAuthToken().build();
        this.hoverflyMode = hoverflyMode;

    }

    /**
     * Instantiates {@link Hoverfly}
     *
     * @param hoverflyMode the mode
     */
    public Hoverfly(HoverflyMode hoverflyMode) {
        this(configs(), hoverflyMode);
    }

    /**
     * <ol>
     * <li>Adds Hoverfly SSL certificate to the trust store</li>
     * <li>Sets the proxy system properties to route through Hoverfly</li>
     * <li>Starts Hoverfly</li>
     * </ol>
     */
    public void start() {

        Runtime.getRuntime().addShutdownHook(new Thread(this::close));

        if (startedProcess != null) {
            LOGGER.warn("Local Hoverfly is already running.");
            return;
        }

        if (!hoverflyConfig.isRemoteInstance()) {
            startHoverflyProcess();
        } else {
            resetJournal();
        }

        waitForHoverflyToBecomeHealthy();

        if (StringUtils.isNotBlank(hoverflyConfig.getDestination())) {
            setDestination(hoverflyConfig.getDestination());
        }

        if (hoverflyMode == CAPTURE) {
            hoverflyClient.setMode(hoverflyMode, new ModeArguments(hoverflyConfig.getCaptureHeaders()));
        } else {
            hoverflyClient.setMode(hoverflyMode);
        }

        if (hoverflyConfig.getProxyCaCertificate().isPresent()) {
            sslConfigurer.setDefaultSslContext(hoverflyConfig.getProxyCaCertificate().get());
        } else if (useDefaultSslCert) {
            sslConfigurer.setDefaultSslContext();
        }

        proxyConfigurer.setProxySystemProperties();
    }

    private void startHoverflyProcess() {
        checkPortInUse(hoverflyConfig.getProxyPort());
        checkPortInUse(hoverflyConfig.getAdminPort());

        final SystemConfig systemConfig = new SystemConfigFactory().createSystemConfig();

        Path binaryPath = tempFileManager.copyHoverflyBinary(systemConfig);

        LOGGER.info("Executing binary at {}", binaryPath);
        final List<String> commands = new ArrayList<>();
        commands.add(binaryPath.toString());
        commands.add("-db");
        commands.add("memory");
        commands.add("-pp");
        commands.add(String.valueOf(hoverflyConfig.getProxyPort()));
        commands.add("-ap");
        commands.add(String.valueOf(hoverflyConfig.getAdminPort()));

        if (StringUtils.isNotBlank(hoverflyConfig.getSslCertificatePath())) {
            tempFileManager.copyClassPathResource(hoverflyConfig.getSslCertificatePath(), "ca.crt");
            commands.add("-cert");
            commands.add("ca.crt");
        }
        if (StringUtils.isNotBlank(hoverflyConfig.getSslKeyPath())) {
            tempFileManager.copyClassPathResource(hoverflyConfig.getSslKeyPath(), "ca.key");
            commands.add("-key");
            commands.add("ca.key");
            useDefaultSslCert = false;
        }

        try {
            startedProcess = new ProcessExecutor().command(commands).redirectOutput(System.out)
                    .directory(tempFileManager.getTempDirectory().toFile()).start();
        } catch (IOException e) {
            throw new IllegalStateException("Could not start Hoverfly process", e);
        }
    }

    /**
     * Stops the running {@link Hoverfly} process and clean up resources
     */
    @Override
    public void close() {
        cleanUp();
    }

    /**
     * Imports a simulation into {@link Hoverfly} from a {@link SimulationSource}
     *
     * @param simulationSource the simulation to import
     */
    public void importSimulation(SimulationSource simulationSource) {
        LOGGER.info("Importing simulation data to Hoverfly");

        final Simulation simulation = simulationSource.getSimulation();

        hoverflyClient.setSimulation(simulation);
    }

    /**
     * Delete existing simulations and journals
     */
    public void reset() {
        hoverflyClient.deleteSimulation();
        resetJournal();
    }

    /**
     * Delete journal logs
     */
    public void resetJournal() {
        try {
            hoverflyClient.deleteJournal();
        } catch (HoverflyClientException e) {
            LOGGER.warn("Older version of Hoverfly may not have a reset journal API", e);
        }
    }

    /**
     * Exports a simulation and stores it on the filesystem at the given path
     *
     * @param path the path on the filesystem to where the simulation should be stored
     */
    public void exportSimulation(Path path) {

        if (path == null) {
            throw new IllegalArgumentException("Export path cannot be null.");
        }

        LOGGER.info("Exporting simulation data from Hoverfly");
        try {
            Files.deleteIfExists(path);
            final Simulation simulation = hoverflyClient.getSimulation();
            persistSimulation(path, simulation);
        } catch (Exception e) {
            LOGGER.error("Failed to export simulation data", e);
        }
    }

    /**
     * Gets the simulation currently used by the running {@link Hoverfly} instance
     *
     * @return the simulation
     */
    public Simulation getSimulation() {
        return hoverflyClient.getSimulation();
    }

    /**
     * Gets configuration information from the running instance of Hoverfly.
     * @return the hoverfly info object
     */
    public HoverflyInfoView getHoverflyInfo() {
        return hoverflyClient.getConfigInfo();
    }

    /**
     * Sets a new destination for the running instance of Hoverfly, overwriting the existing destination setting.
     * @param destination the destination setting to override
     */
    public void setDestination(String destination) {
        hoverflyClient.setDestination(destination);
    }

    /**
     * Changes the mode of the running instance of Hoverfly.
     * @param mode hoverfly mode to change
     */
    public void setMode(HoverflyMode mode) {
        hoverflyClient.setMode(mode);
    }

    /**
     * Gets the validated {@link HoverflyConfig} object used by the current Hoverfly instance
     * @return the current Hoverfly configurations
     */
    public HoverflyConfiguration getHoverflyConfig() {
        return hoverflyConfig;
    }

    /**
     * Gets the currently activated Hoverfly mode
     * @return hoverfly mode
     */
    public HoverflyMode getMode() {
        return HoverflyMode.valueOf(hoverflyClient.getConfigInfo().getMode().toUpperCase());
    }

    public SslConfigurer getSslConfigurer() {
        return sslConfigurer;
    }

    public void verify(RequestMatcherBuilder requestMatcher, VerificationCriteria criteria) {
        verifyRequest(requestMatcher.build(), criteria);
    }

    public void verify(RequestMatcherBuilder requestMatcher) {
        verify(requestMatcher, times(1));
    }

    public void verifyZeroRequestTo(StubServiceBuilder requestedServiceBuilder) {
        verify(requestedServiceBuilder.anyMethod(any()), never());
    }

    public void verifyAll() {
        Simulation simulation = hoverflyClient.getSimulation();
        simulation.getHoverflyData().getPairs().stream().map(RequestResponsePair::getRequest)
                .forEach(request -> verifyRequest(request, atLeastOnce()));
    }

    private void verifyRequest(Request request, VerificationCriteria criteria) {
        Journal journal = hoverflyClient.searchJournal(request);

        criteria.verify(request, new VerificationData(journal));
    }

    private void persistSimulation(Path path, Simulation simulation) throws IOException {
        Files.createDirectories(path.getParent());
        JSON_PRETTY_PRINTER.writeValue(path.toFile(), simulation);
    }

    /**
     * Blocks until the Hoverfly process becomes healthy, otherwise time out
     */
    private void waitForHoverflyToBecomeHealthy() {
        final Instant now = Instant.now();

        while (Duration.between(now, Instant.now()).getSeconds() < BOOT_TIMEOUT_SECONDS) {
            if (hoverflyClient.getHealth())
                return;
            try {
                // TODO: prefer executors and tasks to threads
                Thread.sleep(RETRY_BACKOFF_INTERVAL_MS);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        throw new IllegalStateException("Hoverfly has not become healthy in " + BOOT_TIMEOUT_SECONDS + " seconds");
    }

    private void cleanUp() {
        LOGGER.info("Destroying hoverfly process");

        if (startedProcess != null) {
            Process process = startedProcess.getProcess();
            process.destroy();

            // Some platforms terminate process asynchronously, eg. Windows, and cannot guarantee that synchronous file deletion
            // can acquire file lock
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            Future<Integer> future = executorService.submit((Callable<Integer>) process::waitFor);
            try {
                future.get(5, TimeUnit.SECONDS);
            } catch (InterruptedException | ExecutionException | TimeoutException e) {
                LOGGER.warn("Timeout when waiting for hoverfly process to terminate.");
            }
            executorService.shutdownNow();
        }

        proxyConfigurer.restoreProxySystemProperties();
        // TODO: reset default SslContext?
        tempFileManager.purge();
    }
}