WireMock - CDCgov/prime-simplereport GitHub Wiki

What is WireMock?

WireMock is a service that allows us to record requests and responses to an external service and use these requests/responses (called "mappings" in WireMock lingo) to run tests. We currently use WireMock to tests some areas of the application that require Okta, such as the account creation flow or organization signup flow.

When running in record mode, WireMock acts as a proxy for all your requests to the third-party service and generates mappings based off the actual responses from the service. In later runnings of the test, WireMock will serve up the mappings instead of calling the third party service again. (This is particularly important with Okta because we're rate-limited and they don't have a test instance, so we can't just hit a real Okta endpoint every time we want to run a test.)

Generating new mappings

Using wiremock standalone

If you are writing a new Cypress test for a flow that uses an external service, you can generate new mappings by using Wiremock's recording feature. To use it, start Wiremock in standalone mode:

  • Download wiremock. There should be a link at the very bottom of that page to download the wiremock jar file.
  • Move the downloaded jar file to the root of the prime-simplereport directory
  • Start wiremock
    • the jar filename may be different based on the version you are using
java -jar ./wiremock-standalone-3.2.0.jar --local-response-templating --port 8088 --root-dir ./wiremock/stubs/$1
  • Then navigate to http://localhost:8088/__admin/recorder. That will open a page where you can point Wiremock at the external service you wish to record for. You'll then need to configure the app to point at Wiremock for the service, which will then proxy the requests to the real service through the recorder. For example, if you are generating mappings for Okta, you would set the following in application-local.yml:
okta:
  client:
    org-url: http://localhost:8088
  • Point the Wiremock recorder at https://hhs-prime.oktapreview.com. NOTE: since Okta requires https, you'll need to run the backend with the following environment variable OKTA_TESTING_DISABLEHTTPSCHECK=true (or serve Wiremock via https if you wanna get real fancy)

  • Start the backend and frontend with Okta local configurations

  • Then you can open up the app in your browser and take whatever actions you plan on doing in the test (or write the test first and actually run it). When you're done, click "Stop" on the Wiremock recording page. Your new mappings should then appear in frontend/cypress/stubs! You can then organize them into their own subfolder (see existing mappings for example).

You may need to make adjustments to make the mappings a little more broad so that they can cover more test cases, for example by using regex matching. See the doc pages on stubbing and request matching for more info on this.

Updating LiveOktaAuthentication

LiveOktaAuthentication is tested with WireMock. If you make changes to that file, you'll likely need to update the WireMock recordings.

Here are the steps to do so:

  1. delete all the existing mappings.
  2. create a new user in Okta (following the test fields, so name is TEST INTEGRATION and email is [email protected]) curl request:
curl -v -X POST \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: SSWS {$actualOktaApiToken}" \
-d '{
  "profile": {
    "firstName": "TEST",
    "lastName": "INTEGRATION",
    "email": "[email protected]",
    "login": "[email protected]",
    "mobilePhone": "999-999-9999"
  }
}' "https://hhs-prime.okta.com/api/v1/users?activate=false"

This should return a user id, you'll want to keep that.

  1. Next, you need to activate the user in Okta. (This returns the activation token; it's not the same as the programmatic activation we do in the test.) curl request:
curl -v -X POST \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: SSWS ${actualOktaAiToken}" \
"https://hhs-prime.okta.com/api/v1/users/${userIdFromStep3}/lifecycle/activate?sendEmail=false"

Get the activation token out of the response.

  1. Replace the userId and activationToken in LiveOktaAuthenticationTest with the new user id and activation token.

  2. Add a call to Thread.sleep(45000) in the resendActivationPasscode success case test. (If there's not a 30 second pause between requests, Okta will throw an error)

  3. Replace the static "passcode" in verifyActivationPasscodeSuccessful with the following code:

JSONObject embeddedJson = new JSONObject(factor.getEmbedded());
    String sharedSecret = embeddedJson.getJSONObject("activation").getString("sharedSecret");
    long timeStep = embeddedJson.getJSONObject("activation").getLong("timeStep");

    CodeGenerator codeGenerator = new DefaultCodeGenerator();
    long millisSinceEpoch = Instant.now().toEpochMilli();
    long counter = millisSinceEpoch / 1000 / timeStep;
    String passcode = codeGenerator.generate(sharedSecret, counter);

You will likely also need to add the library to build.gradle and add the imports back in. The gradle build is:

testCompile 'dev.samstevens.totp:totp:1.7.1'

The imports are:

import dev.samstevens.totp.code.CodeGenerator;
import dev.samstevens.totp.code.DefaultCodeGenerator;
import java.time.Instant;
  1. Add the Wiremock recording lines back in.
@BeforeAll
   public void startMockServer() throws IOException {
     WireMock.startRecording("https://hhs-prime.okta.com");
   }

 @AfterAll
   public void stopMockServer() throws IOException {
     List<StubMapping> recordedMappings = WireMock.stopRecording().getStubMappings();
   }

The Wiremock imports are as follows:

import com.github.tomakehurst.wiremock.stubbing.StubMapping;
import com.github.tomakehurst.wiremock.client.WireMock;
  1. Add the actual okta token to application-default.yaml
  2. Run the test! This should pass and generate all the recordings. (There might be a failure around removing/deactivating the user in Okta, you can ignore this. It'll present as some kind of executionException, and it happens because there's no response body for the final user deletion call and WireMock doesn't know how to handle it.)
  3. Search in the /mappings folder for the request/response for verifying an activation passcode. Find the success case, and copy/paste that passcode to verifyActivationPasscodeSuccessful (replacing the CodeGenerator from step 7.)
  4. Remove the Wiremock recording code, remove the totp import from build.gradle, and remove the call to Thread.sleep.
  5. If user deactivation/deletion did fail, delete the user. This can either be done directly in the Okta admin console, or with the following curl requests: deactivate:
curl -v -X POST \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: SSWS ${actualOktaApiToken} \
"https://hhs-prime.okta.com/api/v1/users/${userId}/lifecycle/deactivate?sendEmail=false"

delete (you probably have to run this twice, once to soft-delete and once to hard-delete):

curl -v -X DELETE \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: SSWS ${actualOktaApiToken}" \
"https://hhs-prime.okta.com/api/v1/users/${userId}?sendEmail=false"
  1. Re-run the test and it should pass using the recordings!
  2. If you're feeling ambitious and like you have a couple hours to kill, you can categorize and rename the mappings (also possibly strip some of the unnecessary info, like headers in the response.) If you're not feeling ambitious and/or have better things to do, you can apologize to your reviewers for the massive file dump ¯_(ツ)_/¯

The reason for the nonsense with creating/activating/deleting a user on every run of the recording is that there's a test which verifies that a user's password has never been set before. If you just de-activate the user, their previous setPassword information will be kept, and the test will fail.

Troubleshooting Wiremock

When using wiremock, the response will usually be given with what wiremock is attempting to match image When the Cypress tests are running, you may not be able to see the wiremock logs because Cypress will restart wiremock each time. In order to see those logs, you must capture the output in the startWireMock method in frontend/cypress/plugins/index.js

let scriptOutput = "";
wm.stdout.setEncoding("utf8");
wm.stdout.on("data", function (data) {
  scriptOutput += data.toString();
});
wm.stderr.setEncoding("utf8");
wm.stderr.on("data", function (data) {
  scriptOutput += data.toString();
});
wm.on("close", function (code) {
  console.log(scriptOutput);
});