Add support for placeholders in Wiremock stubs (#1109)

* Generate wiremock stubs in restdocs with placeholders for port

* Add support for placeholders in Wiremock stubs

* Ensure response-template transformer is added if needed

fixes gh-1088
This commit is contained in:
Dave Syer
2019-06-14 15:15:25 +01:00
committed by Marcin Grzejszczak
parent 31f43648ac
commit 33c8cbfc87
10 changed files with 247 additions and 22 deletions

View File

@@ -24,24 +24,6 @@
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8-standalone</artifactId>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
@@ -85,6 +67,24 @@
<artifactId>spring-boot-starter-test</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8-standalone</artifactId>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>

View File

@@ -28,6 +28,7 @@ import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.common.Slf4jNotifier;
import com.github.tomakehurst.wiremock.core.Options;
import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -97,6 +98,9 @@ public class WireMockConfiguration implements SmartLifecycle {
}
registerFiles(factory);
factory.notifier(new Slf4jNotifier(true));
if (this.wireMock.getPlaceholders().isEnabled()) {
factory.extensions(new ResponseTemplateTransformer(false));
}
this.options = factory;
if (this.customizer != null) {
this.customizer.customize(factory);
@@ -245,6 +249,8 @@ class WireMockProperties {
private Server server = new Server();
private Placeholders placeholders = new Placeholders();
private boolean restTemplateSslEnabled;
public boolean isRestTemplateSslEnabled() {
@@ -263,6 +269,32 @@ class WireMockProperties {
this.server = server;
}
public Placeholders getPlaceholders() {
return this.placeholders;
}
public void setPlaceholders(Placeholders placeholders) {
this.placeholders = placeholders;
}
public class Placeholders {
/**
* Flag to indicate that http URLs in generated wiremock stubs should be filtered
* to add or resolve a placeholder for a dynamic port.
*/
private boolean enabled = true;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
public static class Server {
private int port = 8080;

View File

@@ -235,12 +235,14 @@ public final class WireMockRestServiceServer {
}
}
private RequestMatcher matchContents(final ContentPattern pattern) {
private RequestMatcher matchContents(
@SuppressWarnings("rawtypes") final ContentPattern pattern) {
return new RequestMatcher() {
@Override
public void match(ClientHttpRequest request)
throws IOException, AssertionError {
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
@SuppressWarnings("unchecked")
MatchResult result = pattern.match(mockRequest.getBodyAsString());
MatcherAssert.assertThat(
"Request as string [" + mockRequest.getBodyAsString() + "]",

View File

@@ -19,7 +19,12 @@ package org.springframework.cloud.contract.wiremock.restdocs;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationConfigurer;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
import org.springframework.restdocs.operation.preprocess.OperationPreprocessor;
/**
* Custom configuration for Spring RestDocs that adds a WireMock snippet (for generating
@@ -38,9 +43,42 @@ import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationConfigurer;
public class WireMockRestDocsConfiguration
implements RestDocsMockMvcConfigurationCustomizer {
private final Environment environment;
public WireMockRestDocsConfiguration(Environment environment) {
this.environment = environment;
}
@Override
public void customize(MockMvcRestDocumentationConfigurer configurer) {
if (this.environment.getProperty("wiremock.placeholders.enabled", "true")
.equals("true")) {
configurer.operationPreprocessors()
.withResponseDefaults(new ForwardHeaderPreprocessor());
}
configurer.snippets().withAdditionalDefaults(new WireMockSnippet());
}
static class ForwardHeaderPreprocessor implements OperationPreprocessor {
private final OperationResponseFactory responseFactory = new OperationResponseFactory();
@Override
public OperationRequest preprocess(OperationRequest request) {
return request;
}
@Override
public OperationResponse preprocess(OperationResponse response) {
String content = response.getContentAsString();
if (content.contains("localhost:8080")) {
content = content.replace("localhost:8080",
"localhost:{{request.requestLine.port}}");
response = this.responseFactory.createFrom(response, content.getBytes());
}
return response;
}
}
}

View File

@@ -146,9 +146,14 @@ public class WireMockSnippet implements Snippet {
}
private ResponseDefinitionBuilder response(Operation operation) {
return aResponse().withHeaders(responseHeaders(operation))
.withBody(operation.getResponse().getContentAsString())
.withStatus(operation.getResponse().getStatus().value());
String content = operation.getResponse().getContentAsString();
ResponseDefinitionBuilder response = aResponse()
.withHeaders(responseHeaders(operation)).withBody(content);
if (content != null
&& content.contains("localhost:{{request.requestLine.port}}")) {
response = response.withTransformers("response-template");
}
return response.withStatus(operation.getResponse().getStatus().value());
}
private MappingBuilder request(Operation operation) {

View File

@@ -20,6 +20,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.junit4.SpringRunner;
@@ -36,9 +37,17 @@ public class AutoConfigureWireMockStubsApplicationTests {
@Autowired
private Service service;
@Value("localhost:${wiremock.server.port}")
private String hostname;
@Test
public void contextLoads() throws Exception {
assertThat(this.service.go()).isEqualTo("Hello World");
}
@Test
public void link() throws Exception {
assertThat(this.service.link()).contains("http://" + this.hostname);
}
}

View File

@@ -20,6 +20,8 @@ import java.io.File;
import java.nio.file.Files;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.matching.MultiValuePattern;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
@@ -35,6 +37,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.WiremockServerRestDocsApplicationTests.TestConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@@ -43,7 +46,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import static org.hamcrest.CoreMatchers.containsString;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -93,6 +99,24 @@ public class WiremockServerRestDocsApplicationTests {
.isEqualTo(WireMock.equalTo("bar"));
}
@Test
public void stubsRenderLinksWithPlaceholder() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/link"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("link:")))
.andDo(document("link"));
File file = new File("target/snippets/stubs", "link.json");
BDDAssertions.then(file).exists();
StubMapping stubMapping = StubMapping
.buildFrom(new String(Files.readAllBytes(file.toPath())));
String body = stubMapping.getResponse().getBody();
BDDAssertions.then(body)
.contains("http://localhost:{{request.requestLine.port}}/link");
BDDAssertions.then(stubMapping.getResponse().getTransformers())
.contains("response-template");
}
@Configuration
@RestController
protected static class TestConfiguration {
@@ -103,6 +127,14 @@ public class WiremockServerRestDocsApplicationTests {
return "Hello World";
}
@ResponseBody
@RequestMapping("/link")
public String link(HttpServletRequest request) {
UriComponents uriComponents = UriComponentsBuilder
.fromHttpRequest(new ServletServerHttpRequest(request)).build();
return "link: " + uriComponents.toUriString();
}
@ResponseBody
@RequestMapping("/status")
public ResponseEntity<String> status() {

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.wiremock;
import java.io.File;
import java.nio.file.Files;
import javax.servlet.http.HttpServletRequest;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
import org.assertj.core.api.BDDAssertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.WiremockServerRestDocsHypermediaApplicationTests.TestConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import static org.hamcrest.CoreMatchers.containsString;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestConfiguration.class,
properties = "wiremock.placeholders.enabled=false")
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
public class WiremockServerRestDocsHypermediaApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
public void stubsRenderLinksWithoutPlaceholder() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/link"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("link:")))
.andDo(document("link"));
File file = new File("target/snippets/stubs", "link.json");
BDDAssertions.then(file).exists();
StubMapping stubMapping = StubMapping
.buildFrom(new String(Files.readAllBytes(file.toPath())));
String body = stubMapping.getResponse().getBody();
BDDAssertions.then(body).contains("http://localhost:8080/link");
}
@Configuration
@RestController
protected static class TestConfiguration {
@ResponseBody
@RequestMapping("/link")
public String resource(HttpServletRequest request) {
UriComponents uriComponents = UriComponentsBuilder
.fromHttpRequest(new ServletServerHttpRequest(request)).build();
return "link: " + uriComponents.toUriString();
}
}
}

View File

@@ -87,6 +87,12 @@ class Service {
return this.restTemplate.getForEntity(requestUrl, String.class).getBody();
}
public String link() {
String requestUrl = this.base + "/link";
log.info("Will send a request to [" + requestUrl + "]");
return this.restTemplate.getForEntity(requestUrl, String.class).getBody();
}
public String pom() {
String requestUrl = this.base + "/pom.xml";
log.info("Will send a request to [" + requestUrl + "]");

View File

@@ -0,0 +1,12 @@
{
"priority": 1,
"request": {
"urlPath": "/link",
"method": "GET"
},
"response": {
"status": 200,
"body": "{\"_links\": {\"markup\":{\"href\":\"http://localhost:{{request.requestLine.port}}/documents\"}}}",
"transformers": ["response-template"]
}
}