Mocking HTTP Services with WireMock – Part 2

By | March 3, 2018
Reading Time: 11 minutes

In this second article on HTTP testing with WireMock and REST Assured I will show how to set up a HTTPS mock service, with and without mutual authentication, using WireMock and then use REST Assured as a HTTP client to send requests to the service.

I highly recommend reading the first article if you haven’t already.

Prerequisites

I am assuming that you have the example project from the first article ready. If not, you can also find the completed example on GitHub.

Creating a Keystore and Truststore

If your example project does not contain keystores and truststores for the client and server (located in the client and client/server directories in the project root directory) you need to create these keystores and truststores using the script below before being able to proceed with the HTTPS examples.
If you are using an operating system that does not have a Bash shell, open a terminal window, go to the root directory of the example project created above and run the commands in the script one by one. Some commands may need to be adapted to the environment in question.

  • In the project root directory, create a file named “create-keystore.sh” with the following contents:
#!/bin/bash

# This script creates client and server keystores and truststores
# for the WireMock example project.
# All certificates are self-signed.
#
# Author: Ivan Krizsan

rm -r client

mkdir -p client/server

# Create the client keystore containing the client public and private keys.
keytool -genkey -keyalg RSA -keysize 2048 -alias client \
    -keypass secret -storepass secret -keystore client/client_keystore.jks \
    -dname "CN=client.ivankrizsan.se,OU=Client Company,O=Client Organization,L=Client City,ST=Client State,C=SE"

# Export the client certificate (public key).
keytool -export -alias client -keystore client/client_keystore.jks -storepass secret -file client/client.cer

# Create the server truststore containing the client certificate (public key).
keytool -importcert -v -trustcacerts -alias client -keystore client/server/server_cacerts.jks -keypass secret -file client/client.cer

# Create the server keystore containing the server public and private keys.
keytool -genkey -keyalg RSA -keysize 2048 -alias server \
    -keypass secret -storepass secret -keystore client/server/server_keystore.jks \
    -dname "CN=server.ivankrizsan.se,OU=Server Company,O=Server Organization,L=Server City,ST=Server State,C=SE"

# Export the server certificate (public key).
keytool -export -alias server -keystore client/server/server_keystore.jks -storepass secret -file client/server/server.cer

# Create the client truststore containing the server certificate (public key).
keytool -importcert -v -trustcacerts -alias server -keystore client/client_cacerts.jks -storepass secret -keypass secret -file client/server/server.cer
  • Open a terminal window.
  • Go to the example project’s root directory.
    In my case this means that I am standing inside the “wiremock-test” directory.
  • Make the script executable using the following command:
    chmod +x create-keystore.sh
  • Launch the script using the following command:
    ./create-keystores.sh
    When asked to enter and re-enter the keystore password, enter “secret” without quotes.
    When asked whether to trust this certificate, enter “yes” also without quotes.

HTTPS without Client Authentication

Using HTTPS without client authentication is very straightforward with both WireMock and REST Assured. In the example below I have used a JUnit rule to create the WireMock service.

package se.ivankrizsan.wiremocktest;

import com.github.tomakehurst.wiremock.junit.WireMockRule;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import org.hamcrest.core.StringContains;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;

/**
 * Examples on how to use HTTPS without client authentication with WireMock
 * and REST Assured.
 * To debug the SSL handshake, launch with this property: -Djavax.net.debug=ssl:handshake
 *
 * @author Ivan Krizsan
 */
public class WireMockHttpsNoClientAuthTests extends AbstractTestBase {
    /* Constant(s): */

    /* Instance variable(s): */
    @Rule
    public WireMockRule mWireMockRule = new WireMockRule(wireMockConfig()
        .httpsPort(HTTPS_ENDPOINT_PORT)
        .keystorePath(SERVER_KEYSTORE_PATH)
        .keystorePassword(SERVER_KEYSTORE_PASSWORD));

    /**
     * Performs preparations before each test.
     */
    @Before
    public void setup() {
        initializeRestAssuredHttp();
    }

    /**
     * Test sending a HTTPS request to the mock server that matches the request that
     * the mock server expects. Client authentication is not used.
     *
     * Expected result: A response containing a greeting should be received.
     */
    @Test
    public void successfulNoClientAuthTest() {
        /*
         * Setup test HTTPS mock as to expect one request to /wiremock/test with an Accept
         * header that has the value "text/plain".
         * When having received such a request, the mock will return a response with
         * the HTTP status 200 and the header Content-Type with the value "text/plain".
         * The body of the response will be a string containing a greeting.
         */
        stubFor(
            get(urlEqualTo(BASE_PATH))
                .withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.TEXT_PLAIN_VALUE))
                .willReturn(
                    aResponse()
                        .withStatus(HttpStatus.OK.value())
                        .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
                        .withBody("Hello client, this is the response body.")
                )
        );

        /*
         * Send the test-request and save the response so we can log information from it.
         * Set the client keystore to be used with this request.
         * Since a self-signed certificate is used, HTTPS validation need to be relaxed.
         * This is needed if your certificate is not signed by a CA or if the
         * name in the certificate does not match the DNS name of the host.
         */
        final Response theResponse = RestAssured
            .given()
            .relaxedHTTPSValidation()
            .keyStore(CLIENT_KEYSTORE_PATH, CLIENT_KEYSTORE_PASSWORD)
            .contentType(ContentType.TEXT)
            .accept(ContentType.TEXT)
            .when()
            .get(BASE_HTTPS_URL);
        theResponse
            .then()
            .statusCode(HttpStatus.OK.value())
            .contentType(ContentType.TEXT);

        /* Log information from the response for the sake of this being an example. */
        logResponseStatusHeadersAndBody(theResponse);

        /* Verify the result. */
        Assert.assertThat("Response message should contain greeting",
            theResponse.asString(), new StringContains("Hello"));
    }
}

Note that:

  • There is a public instance variable named mWireMockRule of the type WireMockRule and annotated with the @Rule annotation.
    As before, this is the JUnit rule that will setup and start the WireMock server before each test method and also stop it after each test method.
  • When setting up the WireMock server, a HTTPS port is configured using the httpsPort method.
  • Again when setting up the WireMock server, a keystore and a keystore password are configured using the keystorePath and keystorePassword methods.
    When using HTTPS without mutual authentication, configuring a keystore is sufficient on the server side.
  • In the method successfulNoClientAuthTest the WireMock server is configured to expect a GET request to the standard path used in these examples that contain the HTTP header Accept with the text/plain value.
    Having received such a request, WireMock will return a plaintext response containing a greeting.
  • When preparing the request to be sent by REST Assured, the method relaxedHTTPSValidation is invoked.
    Using relaxed HTTPS validation, REST Assured will trust all hosts regardless of whether their certificates are valid or not. In fact, this method removes the need to configure a keystore.
  • When preparing the request, the method keyStore is used to set the client keystore and keystore password.
    As before, this is not strictly necessary when having relaxed HTTPS validation, nevertheless I wanted to include this in my example since I most often have valid test-certificates I can use in my tests.
  • Finally, the response is validated and logged as we have seen in the earlier examples.

When run, the example/test should pass yielding the following lines of log output in the console:

12:42:55.113 [INFO ] s.i.w.AbstractTestBase - Response status: 200
12:42:55.115 [INFO ] s.i.w.AbstractTestBase - Response headers: Content-Type=text/plain
Content-Encoding=gzip
Vary=Accept-Encoding, User-Agent
Transfer-Encoding=chunked
Server=Jetty(9.2.z-SNAPSHOT)
12:42:55.122 [INFO ] s.i.w.AbstractTestBase - Response body: Hello client, this is the response body.

If you want to debug the SSL handshake or just look at the details of a successful SSL handshake, add the following flag to the Java VM options of your test launch-configuration:

-Djavax.net.debug=ssl:handshake

HTTPS with Mutual Authentication

HTTPS with mutual authentication seemed impossible at first, until I realized that REST Assured probably contain a bug related to keystore/truststore configuration. I have not investigated this further but I did find a way to work around the issue by using a custom SSL socket factory.
Regretfully, a side-effect of the workaround is an additional 200 lines of code.

package se.ivankrizsan.wiremocktest;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.verification.LoggedRequest;
import io.restassured.RestAssured;
import io.restassured.config.SSLConfig;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.hamcrest.core.StringContains;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.security.KeyStore;
import java.util.List;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static io.restassured.config.RestAssuredConfig.newConfig;

/**
 * Examples on how to use HTTPS with client authentication with WireMock
 * and REST Assured.
 * To debug the SSL handshake, launch with this property: -Djavax.net.debug=ssl:handshake
 *
 * @author Ivan Krizsan
 */
public class WireMockHttpsWithClientAuthTests extends AbstractTestBase {
    /* Constant(s): */
    private static final Logger LOGGER = LoggerFactory.getLogger(WireMockHttpsWithClientAuthTests.class);

    /* Instance variable(s): */
    protected WireMockServer mWireMockServer;

    /**
     * Performs preparations before each test.
     */
    @Before
    public void initializeRestAssuredHttps() {
        initializeRestAssuredHttp();

        /*
         * Create the WireMock server to be used by a test.
         * This also ensures that the records of received requests kept by the WireMock
         * server and expected scenarios etc are cleared prior to each test.
         * An alternative is to create the WireMock server once before all the tests in
         * a test-class and call {@code resetAll} before each test.
         */
        final WireMockConfiguration theWireMockConfiguration = wireMockConfig()
            .httpsPort(HTTPS_ENDPOINT_PORT)
            .needClientAuth(true)
            .keystorePath(SERVER_KEYSTORE_PATH)
            .keystorePassword(SERVER_KEYSTORE_PASSWORD)
            .trustStorePath(SERVER_TRUSTSTORE_PATH)
            .trustStorePassword(SERVER_TRUSTSTORE_PASSWORD);
        mWireMockServer = new WireMockServer(theWireMockConfiguration);
        mWireMockServer.start();
    }

    /**
     * Performs cleanup after each test.
     */
    @After
    public void tearDown() {
        /* Stop the WireMock server. */
        mWireMockServer.stop();

        /*
         * Find all requests that were expected by the WireMock server but that were
         * not matched by any request actually made to the server.
         * Logs any such requests as errors.
         */
        final List<LoggedRequest> theUnmatchedRequests = mWireMockServer.findAllUnmatchedRequests();
        if (!theUnmatchedRequests.isEmpty()) {
            LOGGER.error("Unmatched requests: {}", theUnmatchedRequests);
        }
    }

    /**
     * Test sending a HTTPS request using REST Assured to the mock server that matches the request that
     * the mock server expects.
     * NOTE!
     * REST Assured seems to be unable to do HTTPS with mutual authentication if the client
     * keystore and truststore is configured using the {@code keyStore} and {@code trustStore}
     * methods. The problem can be worked around by creating a custom SSL socket factory configured
     * with the client keystore and truststore.
     *
     * Expected result: A response containing a greeting should be received.
     *
     * @throws Exception If error occurs setting up the client SSL socket factory for REST Assured.
     */
    @Test
    public void successfulWithClientAuthRestAssuredTest() throws Exception {
        /*
         * Setup test HTTPS mock as to expect one request to /wiremock/test with an Accept
         * header that has the value "text/plain".
         * When having received such a request, the mock will return a response with
         * the HTTP status 200 and the header Content-Type with the value "text/plain".
         * The body of the response will be a string containing a greeting.
         */
        mWireMockServer.stubFor(
            get(urlEqualTo(BASE_PATH))
                .withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.TEXT_PLAIN_VALUE))
                .willReturn(
                    aResponse()
                        .withStatus(HttpStatus.OK.value())
                        .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
                        .withBody("Hello client, this is the response body.")
                )
        );

        /*
         * Setting up REST Assured using the keyStore and trustStore methods does not work
         * so a custom SSL socket factory need to be created.
         * If some day REST Assured fixes the problem, the client keystore and truststore
         * could be configured using the following after the {@code given()} method call:
         * {@code
         *     .relaxedHTTPSValidation()
         *     .keyStore(CLIENT_KEYSTORE_PATH, CLIENT_KEYSTORE_PASSWORD)
         *     .trustStore(CLIENT_TRUSTSTORE_PATH, CLIENT_TRUSTSTORE_PASSWORD)
         * }
         * In such a case, the call to the {@code config} method should be removed.
         *
         * In addition, REST Assured only allows for using the deprecated {@code SSLSocketFactory}
         * instead of the recommended {@code SSLConnectionSocketFactory}.
         * This class contains a method to create the latter, in the case REST Assured is
         * some day updated - please see {@code createNewClientSSLSocketFactory}.
         */
        @SuppressWarnings("deprecation")
        final SSLSocketFactory theClientSSLSocketFactory = createOldClientSSLSocketFactory();
        /* Send the test-request and save the response so we can log information from it. */
        final Response theResponse = RestAssured
            .given()
            .config(
                newConfig()
                .sslConfig(new SSLConfig().sslSocketFactory(theClientSSLSocketFactory))
            )
            .contentType(ContentType.TEXT)
            .accept(ContentType.TEXT)
            .when()
            .get(BASE_HTTPS_URL);
        theResponse
            .then()
            .statusCode(HttpStatus.OK.value())
            .contentType(ContentType.TEXT);

        /* Log information from the response for the sake of this being an example. */
        logResponseStatusHeadersAndBody(theResponse);

        /* Verify the result. */
        Assert.assertThat("Response message should contain greeting",
            theResponse.asString(), new StringContains("Hello"));
    }

    /**
     * Creates the client SSL connection socket factory configured to use the client keystore
     * and truststores.
     * This method creates the newer type of factory that is not deprecated.
     * Regretfully, the current version of REST Assured does not accept this type of factory,
     * but only the old deprecated type. See {@code createOldClientSSLSocketFactory} for the
     * method that create the type of factory that REST Assured is able to use.
     *
     * @return Client SSL connection socket factory.
     * @throws Exception If error occurs creating the factory.
     */
    private SSLConnectionSocketFactory createNewClientSSLSocketFactory() throws Exception {
        final KeyManager[] theClientKeyManagers = createClientKeyManagers();

        final TrustManager[] theClientTrustManagers = createClientTrustManagers();

        /*
         * The client SSL context contains both client key and trust managers
         * since mutual server requires HTTPS mutual authentication.
         */
        SSLContext theClientSSLContext = SSLContext.getInstance("TLS");
        theClientSSLContext.init(theClientKeyManagers, theClientTrustManagers, null);

        /*
         * Create a client SSL connection socket factory using the client's SSL context and
         * a hostname verifier that disables hostname verification.
         * The NOOP hostname verifier is used since we are using self-signed certificates
         * with a CN that does not match the hostname.
         */
        return new SSLConnectionSocketFactory(theClientSSLContext, new NoopHostnameVerifier());
    }

    /**
     * Creates the client SSL connection socket factory configured to use the client keystore
     * and truststores.
     * This method creates the old type of factory that is deprecated.
     *
     * @return Client SSL connection socket factory.
     * @throws Exception If error occurs creating the factory.
     */
    private SSLSocketFactory createOldClientSSLSocketFactory() throws Exception {
        final KeyManager[] theClientKeyManagers = createClientKeyManagers();

        final TrustManager[] theClientTrustManagers = createClientTrustManagers();

        /*
         * The client SSL context contains both client key and trust managers
         * since mutual server requires HTTPS mutual authentication.
         */
        SSLContext theClientSSLContext = SSLContext.getInstance("TLS");
        theClientSSLContext.init(theClientKeyManagers, theClientTrustManagers, null);

        /*
         * Create a client SSL connection socket factory using the client's SSL context and
         * a hostname verifier that disables hostname verification.
         * The NOOP hostname verifier is used since we are using self-signed certificates
         * with a CN that does not match the hostname.
         */
        final SSLSocketFactory theClientSSLSocketFactory = new SSLSocketFactory(theClientSSLContext);
        theClientSSLSocketFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

        return theClientSSLSocketFactory;
    }

    /**
     * Creates the client trust managers using the client truststore.
     *
     * @return Array of client trust managers.
     * @throws Exception If error occurs creating key managers.
     */
    private TrustManager[] createClientTrustManagers() throws Exception {
        final KeyStore theClientTruststore = getClientTruststore();

        /* Create the trust manager factory which is responsible for creating the client trust managers. */
        final TrustManagerFactory theClientTrustManagerFactory =
            TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        theClientTrustManagerFactory.init(theClientTruststore);

        return theClientTrustManagerFactory.getTrustManagers();
    }

    /**
     * Creates the client key managers using the client keystore.
     * Loads the client keystore from the file system.
     *
     * @return Array of client key managers.
     * @throws Exception If error occurs creating key managers.
     */
    private KeyManager[] createClientKeyManagers() throws Exception {
        final KeyStore theClientKeystore = getClientKeystore();

        /* Create the key manager factory which is responsible for creating the client key managers. */
        final KeyManagerFactory theClientKeyManagerFactory =
            KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        theClientKeyManagerFactory.init(theClientKeystore, CLIENT_KEYSTORE_PASSWORD.toCharArray());

        return theClientKeyManagerFactory.getKeyManagers();
    }

    /**
     * Creates and loads the client truststore.
     * Truststore is loaded from the file system.
     *
     * @return Client truststore.
     * @throws Exception If error occurs loading truststore.
     */
    private KeyStore getClientTruststore() throws Exception {
        final KeyStore theClientTruststore = KeyStore.getInstance("JKS");
        theClientTruststore.load(
            new FileInputStream(CLIENT_TRUSTSTORE_PATH), CLIENT_TRUSTSTORE_PASSWORD.toCharArray());

        return theClientTruststore;
    }

    /**
     * Creates and loads the client keystore.
     * Keystore is loaded from the file system.
     *
     * @return Keystore object containing client keystore.
     * @throws Exception If error occurs loading keystore.
     */
    private KeyStore getClientKeystore() throws Exception {
        final KeyStore theClientKeystore = KeyStore.getInstance("JKS");
        theClientKeystore.load(
            new FileInputStream(CLIENT_KEYSTORE_PATH), CLIENT_KEYSTORE_PASSWORD.toCharArray());

        return theClientKeystore;
    }
}

Note that:

  • The WireMock server is created, started and stopped programmatically.
    This is not necessary, you could just as well use a JUnit rule like in the previous example but I wanted to show that it is a feasible option.
  • When the WireMock server configuration is created, the needClientAuth method is called with a boolean true as parameter.
    This enables mutual authentication with HTTPS on the server side.
  • When the WireMock server configuration is created, a keystore and truststore and their corresponding passwords are set.
    HTTPS mutual authentication require both a keystore and a truststore.
  • In the test/example method successfulWithClientAuthRestAssuredTest the WireMock server is configured to expect a GET request to the usual path, with the Accept header having the text/plain value.
    As in the previous example, WireMock will respond with a plaintext body containing a greeting.
  • Before using REST Assured to send the test-request to the server, a SSL socket factory is created.
    We’ll get back to the SSL socket factory creation in a little while, let me just finish examining the test-method.
  • The class SSLSocketFactory is deprecated.
    SSLConnectionSocketFactory should be used instead. Regretfully REST Assured only accepts a socket factory of the former type.
  • REST Assured is used to send a request to the WireMock server.
    If REST Assured had worked as advertised, we should have used the following code to send the test-request. Instead we have to configure REST Assured to use the SSL socket factory just created.
/* If REST Assured had worked as expected, this code could have been used. */
        final Response theResponse = RestAssured
            .given()
            .relaxedHTTPSValidation()
            .keyStore(CLIENT_KEYSTORE_PATH, CLIENT_KEYSTORE_PASSWORD)
            .trustStore(CLIENT_TRUSTSTORE_PATH, CLIENT_TRUSTSTORE_PASSWORD)
            .contentType(ContentType.TEXT)
            .accept(ContentType.TEXT)
            .when()
            .get(BASE_HTTPS_URL);
  • The remainder of the test-method is spent logging and verifying the response as we have seen in the previous examples.
  • There is a method named createNewClientSSLSocketFactory which creates a SSL socket factory.
    I included this method in the case REST Assured is updated to use the newer socket factory.
  • There is a method named createOldClientSSLSocketFactory which creates a SSL socket factory of the old, deprecated, type.
    Both methods that create SSL socket factories configure the factory to have a keystore and a truststore and not to verify the name in the certificate against the hostname.
  • Finally there are methods to create client key and trust managers and methods that loads the client keystore and truststore.

When run, the test should pass and a few lines similar to what we have seen in earlier examples should be logged to the console:

17:13:29.547 [INFO ] s.i.w.AbstractTestBase - Response status: 200
17:13:29.549 [INFO ] s.i.w.AbstractTestBase - Response headers: Content-Type=text/plain
Content-Encoding=gzip
Vary=Accept-Encoding, User-Agent
Transfer-Encoding=chunked
Server=Jetty(9.2.z-SNAPSHOT)
17:13:29.556 [INFO ] s.i.w.AbstractTestBase - Response body: Hello client, this is the response body.

Final Words

The WireMock project was started back in 2011 and I do get the feeling that it is a mature piece of software. WireMock is very nice and seem to have all the features that I could wish for in a HTTP mock server. The one drawback I have found with WireMock is the total lack of documentation in the source-code and the fact that I haven’t been able to find any JavaDoc, which given the otherwise high standard, I find very surprising.
The examples in this article have but scratched on the surface, there are many more features available in WireMock!

Happy coding!

 

One thought on “Mocking HTTP Services with WireMock – Part 2

Leave a Reply

Your email address will not be published. Required fields are marked *