Search code examples
groovydesign-by-contractspring-cloud-contract

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?


Solution

  • 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)));
        }
    }