swinkler swinkler - 3 years ago 223
Groovy Question

Spring Cloud Contract: Access hostname and port in contract response for URL generation

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?

Answer Source

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)));
    }
}
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download