we are trying to provide a contract with the following characteristics:
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method(GET())
url("/v2/entity")
headers {
accept(applicationJson())
}
}
response {
status 200
body( """
{
"saveLink": "http://<requestedHost>:<requestedPort>/v2/entity/save"
}
)
}
}
If our client uses the stubrunner and chooses a different port, e.q. 9876, the "saveLink" should reflect this port in the response URL. We couldn'd find a simple API way to get the host and port information. fromRequest() or url() only return the relative part of the request URL. Is there an API method or a simple solution to this requirement? Any other suggestions?
To begin with I think that you're misuing the concept of contract testing. I wonder why is it crucial for you to have those concrete values there? In contract tests you would be only interested that the link contains some value of a URL and port. You wouldn't even make a call to that URL. So most likely you should change your approach before going any further.
If however you decide that this is the only way to go I present my opinion on how you could solve this (I haven't tested it though but it looks like it should work ;) ).
Currently making this all work is not easy. It will be easier in the upcoming Edgware release once this https://github.com/spring-cloud/spring-cloud-contract/pull/429 gets merged.
I'll try to think of a workaround though. What we will do is to add an transformation mechanism that will modify the response payload before sending it back from WireMock. We will need the transformation class and we'll need to extend the existing stub serving mechanism too.
First, let's create a custom WireMock extension that will analyze each mapping that is consumed by WireMock. We will want to modify only the one that has the saveLink
in it.
class CustomExtension extends ResponseTransformer {
@Override
String getName() {
return "url-transformer";
}
/**
* Transformer that converts the save the way we want it to look like
*/
@Override
Response transform(Request request, Response response, FileSource files, Parameters parameters) {
if (requestRelatedToMyParticularCase(response)) {
String body = "\"{\"saveLink\" : \"http://"+ url.host + ":" + url.port + "/v2/entity/save\"}\"";
return new Response(response.getStatus(), response.getStatusMessage(),
body, response.getHeaders(), response.wasConfigured(), response.getFault(), response.isFromProxy());
}
// if it's not related continue as usual
return response;
}
private boolean requestRelatedToMyParticularCase(Response response) {
// is it related to your particular scenario ?
return response.bodyAsString.contains("saveLink");
}
/**
* We want to apply this transformation for all mappings
*/
@Override
boolean applyGlobally() {
return true
}
}
Now, you can create a class that implements the HttpServerStub
and register it as presented here - http://cloud.spring.io/spring-cloud-static/Dalston.SR3/#_custom_stub_runner . It's basically a copy of WireMockHttpServerStub
with a change where we add the transformer manually
public class MyCustomWireMockHttpServerStub implements HttpServerStub {
private static final Logger log = LoggerFactory.getLogger(MyCustomWireMockHttpServerStub.class);
private static final int INVALID_PORT = -1;
private WireMockServer wireMockServer;
@Override
public HttpServerStub start(int port) {
this.wireMockServer = new WireMockServer(myConfig().port(port)
.notifier(new Slf4jNotifier(true)));
this.wireMockServer.start();
return this;
}
private WireMockConfiguration myConfig() {
if (ClassUtils.isPresent("org.springframework.cloud.contract.wiremock.WireMockSpring", null)) {
return WireMockSpring.options()
.extensions(responseTransformers());
}
return new WireMockConfiguration().extensions(responseTransformers());
}
private Extension[] responseTransformers() {
List<Extension> extensions = new ArrayList<>();
extensions.add(defaultResponseTemplateTransformer());
extensions.add(new CustomExtension());
return extensions.toArray(new Extension[extensions.size()]);
}
private ResponseTemplateTransformer defaultResponseTemplateTransformer() {
return new ResponseTemplateTransformer(false, helpers());
}
@Override
public int port() {
return isRunning() ? this.wireMockServer.port() : INVALID_PORT;
}
@Override
public boolean isRunning() {
return this.wireMockServer != null && this.wireMockServer.isRunning();
}
@Override
public HttpServerStub start() {
if (isRunning()) {
if (log.isDebugEnabled()) {
log.debug("The server is already running at port [" + port() + "]");
}
return this;
}
return start(SocketUtils.findAvailableTcpPort());
}
@Override
public HttpServerStub stop() {
if (!isRunning()) {
if (log.isDebugEnabled()) {
log.debug("Trying to stop a non started server!");
}
return this;
}
this.wireMockServer.stop();
return this;
}
@Override
public HttpServerStub registerMappings(Collection<File> stubFiles) {
if (!isRunning()) {
throw new IllegalStateException("Server not started!");
}
registerStubMappings(stubFiles);
return this;
}
@Override public String registeredMappings() {
Collection<String> mappings = new ArrayList<>();
for (StubMapping stubMapping : this.wireMockServer.getStubMappings()) {
mappings.add(stubMapping.toString());
}
return jsonArrayOfMappings(mappings);
}
private String jsonArrayOfMappings(Collection<String> mappings) {
return "[" + StringUtils.collectionToDelimitedString(mappings, ",\n") + "]";
}
@Override
public boolean isAccepted(File file) {
return file.getName().endsWith(".json");
}
StubMapping getMapping(File file) {
try (InputStream stream = Files.newInputStream(file.toPath())) {
return StubMapping.buildFrom(
StreamUtils.copyToString(stream, Charset.forName("UTF-8")));
}
catch (IOException e) {
throw new IllegalStateException("Cannot read file", e);
}
}
private void registerStubMappings(Collection<File> stubFiles) {
WireMock wireMock = new WireMock("localhost", port(), "");
registerDefaultHealthChecks(wireMock);
registerStubs(stubFiles, wireMock);
}
private void registerDefaultHealthChecks(WireMock wireMock) {
registerHealthCheck(wireMock, "/ping");
registerHealthCheck(wireMock, "/health");
}
private void registerStubs(Collection<File> sortedMappings, WireMock wireMock) {
for (File mappingDescriptor : sortedMappings) {
try {
wireMock.register(getMapping(mappingDescriptor));
if (log.isDebugEnabled()) {
log.debug("Registered stub mappings from [" + mappingDescriptor + "]");
}
}
catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("Failed to register the stub mapping [" + mappingDescriptor + "]", e);
}
}
}
}
private void registerHealthCheck(WireMock wireMock, String url) {
registerHealthCheck(wireMock, url, "OK");
}
private void registerHealthCheck(WireMock wireMock, String url, String body) {
wireMock.register(
WireMock.get(WireMock.urlEqualTo(url)).willReturn(WireMock.aResponse().withBody(body).withStatus(200)));
}
}