Outbound REST Requests

The following tutorial uses the example project codebase. Make sure you have the repository cloned so you can follow along and run the tests.

This example creates a REST endpoint on the test server that makes a follow-up POST request to an external endpoint. This would be a common setup if you rely on third-party APIs or have a microservice architecture.

Creating the Flow

Open the Kotlin file located at main/kotlin/flowexamples/e02/E02HttpRequestFlow.kt. Like the previous example, the following flow uses the restApi processor to create an inbound REST endpoint:

val httpRequestSpec = flowConfig {
    id = "http-request"
    description = "HTTP Request Flow"
    ownerId = OWNER_ID
    exchangePattern = RequestResponse

    restApi {
        id = "echo-api"
        apiSpecId = httpRequestResourceKey.toResourceIdentifier()
    }

    ...
}

Remember that the restApi processor passes its payload to the next processor. In this case, we want that next processor to be a REST endpoint located on another server. The following restRequest processor will make an outbound POST request using the incoming payload for the body:

restRequest {
    id = "rest-request"
    defaultMethod = POST
    address = URL("https://OVERRIDE_ME/echo")
    authenticationConfigKey = BACKEND_AUTHENTICATION_KEY
}

You can assign a real URL to the address property, but we plan to mock this request in the functional test, so it doesn’t matter what we put for now. Keep in mind, however, that you would typically assign these external URLs at the environment level during deployment.

Optional credentials can be provided with an authenticationConfigKey property. In a live deployment, these credentials would be looked up externally. When testing, they are manually added to the test context. The authenticationConfigKey property in this example gets its value from the following variable in the same E02HttpRequestFlow object:

const val BACKEND_AUTHENTICATION_KEY = "backendAuth"

Assuming backendAuth is assigned a username and password during the test, the POST request will encode those values in the HTTP authorization header.

For more information on how the flow-server uses these authentication secrets, check out the Resource Registry documentation.

Testing the Flow

Open the Kotlin class located at test/kotlin/flowexamples/e02/E02HttpRequestFlowTest.kt. This uses the same concurrent testing framework as the previous example and contains the following @Test function:

@Test
fun `E02 GIVEN wiremock backend stub WHEN sending a value THEN an authenticated request is forwarded to the stub flow and echoed back`(
    ctx: ConcurrentTestContext
) {
    val inputValue = "testValue"
    val user = "backendUser"
    val pwd = "backendPwd"

    ...
}

These first few variables hold the value for the POST request and the credentials for the backendAuth configuration key.

Mocking the Request

A good functional test should always mock the responses from any external requests. The Utilihive SDK uses the WireMock library for this purpose. WireMock is already included as a Maven dependency in the example project, but for new projects that want to take advantage of these features, you would need to add the following <dependency> element (with the latest version) to your pom.xml file:

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8-standalone</artifactId>
    <version>LATEST_VERSION</version>
    <scope>test</scope>
</dependency>

With the WireMock dependency in place, the next line in the @Test function is able to call the following SDK function:

withWireMock { wireMockServer ->

}

The withWireMock() function uses the WireMock library internally to create a WireMockServer instance and then run the provided callback function. The mock server is automatically shut down by the SDK and can only be used within the scope of the callback, so the remaining test logic will take place here.

The first thing we do with the mock server is create the following stub:

wireMockServer.stubFor(
    post(urlPathEqualTo("/echo"))
        .withRequestBody(equalToJson(""" { "value": "$inputValue" } """))
        .withBasicAuth(user, pwd)
        .willReturn(okJson(""" { "message": "$inputValue" } """))
)

This informs the mock server how to respond when a POST request is made to /echo that meets the following requirements:

  • The request body is equal to { "value": "testValue" }

  • The authorization header matches the values of user and pwd

If these conditions are met, the stub will respond with { "message": "testValue" }.

Creating the Resources

The next step is to set up the resources for the test server, which includes the following variables:

val frontendApiResource = Resource(key = httpRequestResourceKey, content = httpRequestOpenApiDefinition)
val frontendFlow = httpRequestSpec.map<RestRequestConfig> { restRequestConfig ->
    ...
}

The frontendApiResource variable represents the OpenAPI specification for the inbound endpoint. The frontendFlow variable is a slightly edited version of the flow we want to test. Recall that the restRequest processor on the flow was hardcoded to https://OVERRIDE_ME/echo, so we need to swap out the URL with the mock server. We use the SDK’s own map() method to find the RestRequestConfig object on the flow spec and update it with the following code:

val backendAddress = URL("http://localhost:${wireMockServer.port()}/echo")
restRequestConfig.copy(address = backendAddress)

The backendAddress will become something like http://localhost:54420/echo, depending on which port the mock server is using. Next, we use the copy() method to create and return a new RestRequestConfig object, replacing only the properties that are specified as arguments.

Now the OpenAPI resource and modified flow can be added to the test context. We assign the credentials for backendAuth at the same time, as the following code demonstrates:

ctx.addFlowTestConfig {
    authConfig(
        BACKEND_AUTHENTICATION_KEY, mapOf(
            "userName" to user,
            "password" to pwd
        )
    )

    resource(frontendApiResource)
    flow(frontendFlow)
}

Running the Test

The flowTest() function starts the test server with the provided context and then executes the subsequent block of code. The test logic uses the restApiEndpoint() function again to make a POST request and assert the response, as the following shows:

flowTest(ctx) {
    val inputValue = "testValue"
    val responseMessage = restApiEndpoint(frontendFlow)
        .path("echo")
        .request()
        .basicAuth()
        .post(json(SimpleValue(inputValue)), SimpleMessage::class.java)
    assertThat(responseMessage.message).isEqualTo(inputValue)
}

This part of the test is the same as the first example’s test. The test still passes, though, because our new flow internally makes a POST request to the mocked /echo endpoint, which sets the response to {"message":"testValue"}.

Make sure to run the test yourself so you can see that the mock works. Next, you can try mocking a GET request instead of a POST request, adding a different set of authorization credentials, or move on to the distributed flows tutorial.