Callback-Simulator
Request and response value referencing
The callback data acts as a template where match patterns may be defined and which will be replaced by matching JsonPaths from the initial request and response. The general syntax follows the same rules as described in json-body-transformer and keywords documentation.
Keywords can be used exactly in the way as described in the documentation, but in contrast to the body transformer references to JSON of the initial request/response have to be prefixed.
Imagine following JSON request
{
"name": "John Doe",
"age": 35,
"appeared": "2016-11-23T11:10:00Z"
}
and the related JSON response
{
"id": "$(UUID)",
"found_age": "$(age)",
"composed_string": "$(name) is $(age) years old."
}
To reference the name
of the request and the id
of the response in a callback definition one has to prefix the match patterns in the callback data JSON like so
{
"reference_id": "$(response.id)",
"referenced_name": "$(request.name)",
"timestamp": "$(!Instant)"
}
Note the additional timestamp
property which uses the !Instant
keyword to populate the related value.
The data type handling is the same as describe for the json-body-transformer.
Also for the callback URL request and response values may be referenced. If the replacement value is contained in a query string part it will be URL encoded.
Callback processing
Internally the callback simulator utilizes Java's ScheduledExecutorService
with thread pool size of 50 to perform the callback requests. The thread pool size can be customized by specifying SCHEDULED_THREAD_POOL_SIZE
environment variable with the desired size. Note that if the value is less than the default of 50 the default is used.
Callback requests errors will be logged but note that retry handling is disabled by default. If a callback fails it fails...
Retry handling
Enabling retry handling for callbacks which may be useful during load testing depending on the service under test is as simple as specifying MAX_RETRIES
with some positive value depending on the number of desired retries that should be performed. The retry handling uses a back off period of 5 seconds by default that can be configured by specifying RETRY_BACKOFF
(default 5_000 milliseconds). This value is multiplied with the invocation count to reschedule the callback.
So with MAX_RETRIES
set to 3
retries will happen after 5, 10 and 15 seconds thus the callback will be retried for 30 seconds in total.
Common Callback Model
The properties shared by all callbacks are the delay
and the data
properties. The delay defines the wait time in milliseconds the callback will be scheduled for, when the URL defined in the mapping stub was requested. The data allows arbitrary JSON.
{
"delay": 1000,
"data": {
"json_representation": "of MyCallbackPayload"
}
}
SNS/SQS Callbacks
The configuration for the AWS SNS and SQS clients requires the AWS_REGION
environment variable to be provided. For testing purposes it is possible to specify the AWS_SQS_ENDPOINT
and AWS_SNS_ENDPOINT
but depending of the test stack (elasticMQ / localstack) access id and secret key might be required as well.
:warning: elasticMQ doesn't support SNS messaging
When running as a kubernetes pod in a larger test environment with real AWS queues the endpoint should be empty so that the default AWS endpoint is used. The credentials should be set up through the container.
:warning: if the configured AWS account is not authorized to perform: SNS:ListTopics a full qualified SNS topic arn must be configured
The only additional property for SQS callbacks is the queue
and for SNS callbacks is the topic
property to provide the queue or topic name to publish messages to. The queue or topic property may contain placeholders like request and response references or an environment variable.
SQS Callback example JSON
{
"queue": "my-sqs-queue-name",
"delay": 1000,
"data": {
"json_representation": "of MyCallbackPayload"
}
}
{
"queue": "$(!ENV[SQS_QUEUE_NAME])",
"delay": 1000,
"data": {
"json_representation": "of MyCallbackPayload"
}
}
SNS Callback example JSON
{
"topic": "my-sns-topic-name",
"delay": 1000,
"data": {
"json_representation": "of MyCallbackPayload"
}
}
{
"queue": "$(!ENV[SNS_TOPIC_NAME])",
"delay": 1000,
"data": {
"json_representation": "of MyCallbackPayload"
}
}
:bulb: see ElasticMQ and localstack for details on testing AWS SNS / SQS messaging.
HTTP Callbacks
HTTP provides some more properties compared to SQS. Beside the obvious required url
also the optional support for Basic and Bearer authentication
as well as request identification using a traceId
is implemented. Similar to the topic or queue property for AWS SNS/SQS callbacks the url
and authentication
properties may contain placeholders and keywords.
Request identification
The callback requests emitted by the callback-simulator will contain the X-Rps-TraceId
header populated with a random value. Services which evaluate this header may add this identifier to their logging context. It is possible to specify a custom value as trace id as outlined below.
HTTP callback example
{
"delay": 10000,
"url": "http://localhost:8080/my/listening/callback/url",
"authentication": {
"type": "BASIC",
"username": "user",
"password": "pass"
},
"traceId": "my-trace-identifier",
"data": {
"json representation": "of MyCallbackPayload"
}
}
{
"delay": 10000,
"url": "$(request.callbackUrl)",
"authentication": {
"type": "BEARER",
"token": "$(!ENV[BEARER_TOKEN])"
},
"traceId": "my-trace-identifier",
"expectedHttpStatus": 400,
"data": {
"json representation": "of invalid MyCallbackPayload"
}
}
Verification
In contrast to SNS/SQS callbacks HTTP implementation get's a synchronous response status. By default a 2xx HTTP status result is considered successful for a callback request, but for use case specific expectations, e.g. duplicate callback request to the same resource, it is possible to specify the optional expectedHttpStatus
to define the HTTP status value to indicates success.
Successful execution of a callback is recorded in the WireMock journal with URL /callback/result
and the report payload provides the absolute callback request URL as well as response status and body as shown by the example:
{
"result" : "success",
"target" : "http://localhost:8080/my/listening/callback/url",
"response" : {
"status" : 204,
"body" : "null"
}
}
{
"result" : "success",
"target" : "http://localhost:8080/my/listening/callback/url",
"response" : {
"status" : 400,
"body" : "{\"error_code\":\"my-fancy-error\"}"
}
}
Using WireMocks built-in verification mechanism a successful HTTP callback results can be verified as follows:
verify(1, postRequestedFor(urlPathEqualTo("/callback/result"))
.withRequestBody(matchingJsonPath("$.[?(@.target == 'http://localhost:8080/my/listening/callback/url')]"))
.withRequestBody(matchingJsonPath("$.[?(@.response.status == 400)]"))
.withRequestBody(matchingJsonPath("$.[?(@.response.body =~ /.*my-fancy-error.*/i)]")));
Stubbing
Instantiating the WireMock server with CallbackSimulator
extension instance
new WireMockServer(wireMockConfig().extensions(new CallbackSimulator()));
The examples assume that you are familiar with WireMocks stubbing technique.
1. Example - mixed callbacks
Specify the callback simulator with three callbacks. One is an HTTP request the second an SQS message and the last an SNS message callback.
// arbitrary JSON object that represents the payload to POST
MyCallbackPayload callbackData = new MyCallbackPayload();
String callbackUrl = "http://localhost:8080/my/listening/callback/url";
int httpCallbackDelay = 10000;
// arbitrary JSON object that represents the SQS message to sent
MyCallbackSqsMessage callbackSqsMessage = new MyCallbackSqsMessage();
String queueName = "callback-queue-name";
int sqsCallbackDelay = 11000;
// arbitrary JSON object that represents the SNS message to sent
MyCallbackSnsMessage callbackSqsMessage = new MyCallbackSnsMessage();
String topicName = "callback-topic-name";
int snsCallbackDelay = 12000;
// Note the usage of the com.ninecookies.wiremock.extensions.api.Callbacks and Callback classes
wireMockServer.stubFor(post(urlEqualTo("/url/to/post/to"))
.withPostServeAction("callback-simulator", Callbacks.of(
Callback.of(httpCallbackDelay, callbackUrl, callbackData),
Callback.ofQueueMessage(sqsCallbackDelay, queueName, callbackSqsMessage),
Callback.ofTopicMessage(snsCallbackDelay, topicName, callbackSnsMessage)
))
.willReturn(aResponse()
.withHeader("content-type", "application/json")
.withBody("{\"id\":\"$(!UUID)\"}")
.withTransformers("json-body-transformer")
.withStatus(201)));
Similar in JSON
{
"request": {
"url": "/url/to/post/to",
"method": "POST"
},
"response": {
"status": 201,
"body": "{\"id\":\"$(!UUID)\"}",
"headers": {
"content-type": "application/json"
},
"transformers": [
"json-body-transformer"
]
},
"postServeActions": {
"callback-simulator": {
"callbacks": [
{
"delay": 10000,
"url": "http://localhost:8080/my/listening/callback/url",
"data": {
"json representation": "of MyCallbackPayload"
}
},
{
"delay": 11000,
"queue": "callback-queue-name",
"data": {
"json representation": "of MyCallbackSqsMessage"
}
}
{
"delay": 12000,
"queue": "callback-topic-name",
"data": {
"json representation": "of MyCallbackSnsMessage"
}
}
]
}
}
}
2. Example - authenticated HTTP callback
Convenient usage of Callbacks.of()
overload with Basic authentication and custom trace identifier for a single callback. Same method is provided by Callback
.
// arbitrary JSON object that represents the payload to POST
MyCallbackPayload callbackData = new MyCallbackPayload();
String callbackUrl = "http://localhost:8080/my/listening/callback/url"
int callbackDelay = 10000;
// Note the usage of the com.ninecookies.wiremock.extensions.api.Callbacks class
wireMockServer.stubFor(post(urlEqualTo("/url/to/post/to"))
.withPostServeAction("callback-simulator", Callbacks.of(callbackDelay, callbackUrl, "user", "pass", "my-trace-identifier", callbackData))
.willReturn(aResponse()
.withHeader("content-type", "application/json")
.withBody("{\"id\":\"$(!UUID)\"}")
.withTransformers("json-body-transformer")
.withStatus(201)));
Similar in JSON
{
"request": {
"url": "/url/to/post/to",
"method": "POST"
},
"response": {
"status": 201,
"body": "{\"id\":\"$(!UUID)\"}",
"headers": {
"content-type": "application/json"
},
"transformers": [
"json-body-transformer"
]
},
"postServeActions": {
"callback-simulator": {
"callbacks": [
{
"delay": 10000,
"url": "http://localhost:8080/my/listening/callback/url",
"authentication": {
"type": "BASIC",
"username": "user",
"password": "pass"
},
"traceId": "my-trace-identifier",
"data": {
"json representation": "of MyCallbackPayload"
}
}
]
}
}
}
3. Example - environment variables
Callback configuration based on environment variables instead of having confidential information in a plain JSON mapping file.
// arbitrary JSON object that represents the payload to POST
MyCallbackPayload callbackData = new MyCallbackPayload();
int httpCallbackDelay = 10000;
// arbitrary JSON object that represents the message to sent
MyCallbackMessage callbackMessage = new MyCallbackMessage();
int sqsCallbackDelay = 11000;
// Note the usage of the com.ninecookies.wiremock.extensions.api.Callbacks and Callback classes
wireMockServer.stubFor(post(urlEqualTo("/url/to/post/to"))
.withPostServeAction("callback-simulator", Callbacks.of(
Callback.of(httpCallbackDelay, "$(!ENV[CBURL])", "$(!ENV[CBUSER])", "$(!ENV[CBPASS])", callbackData),
Callback.ofQueueMessage(sqsCallbackDelay, "$(!ENV[CBQUEUE]), callbackMessage)
))
.willReturn(aResponse()
.withHeader("content-type", "application/json")
.withBody("{\"id\":\"$(!UUID)\"}")
.withTransformers("json-body-transformer")
.withStatus(201)));
Similar in JSON
{
"request": {
"url": "/url/to/post/to",
"method": "POST"
},
"response": {
"status": 201,
"body": "{\"id\":\"$(!UUID)\"}",
"headers": {
"content-type": "application/json"
},
"transformers": [
"json-body-transformer"
]
},
"postServeActions": {
"callback-simulator": {
"callbacks": [
{
"delay": 10000,
"url": "$(!ENV[CBURL])",
"authentication": {
"type": "BASIC",
"username": "$(!ENV[CBUSER])",
"password": "$(!ENV[CBPASS])"
},
"data": {
"json representation": "of MyCallbackPayload"
}
},
{
"delay": 11000,
"queue": "$(!ENV[CBQUEUE])",
"data": {
"json representation": "of MyCallbackMessage"
}
}
]
}
}
}