diff --git a/docs/src/main/asciidoc/spring-cloud-wiremock.adoc b/docs/src/main/asciidoc/spring-cloud-wiremock.adoc index fbdd7f9cab..a3a89f8fd9 100644 --- a/docs/src/main/asciidoc/spring-cloud-wiremock.adoc +++ b/docs/src/main/asciidoc/spring-cloud-wiremock.adoc @@ -157,6 +157,29 @@ is saying: any valid POST with an "id" field will get back an the same response as in this test. You can chain together calls to `.jsonPath()` to add additional matchers. +Instead of the `jsonPath` and `contentType` convenience methods, you +can also use WireMock to verify the request matches the created +stub. Example: + +[source,java,indent=0] +---- + @Test + public void contextLoads() throws Exception { + mockMvc.perform(post("/resource") + .content("{\"id\":\"123456\",\"message\":\"Hello World\"}")) + .andExpect(status.isOk()) + .andDo(verify() + .wiremock(WireMock.post( + urlPathEquals("/resource")) + .withRequestBody(matchingJsonPath("$.id")) + .stub("resource")); + } +---- + +The WireMock API is rich - you can match headers, query parameters, +and request body by regex for instance - so this can useful to create +stubs with a wider range of parameters. + On the consumer side, assuming the `resource.json` generated above is available on the classpath, you can create a stub using WireMock in a number of different ways, including as described above using diff --git a/samples/standalone/restdocs/http-client/build.gradle b/samples/standalone/restdocs/http-client/build.gradle index 4db1443b15..23424e685a 100644 --- a/samples/standalone/restdocs/http-client/build.gradle +++ b/samples/standalone/restdocs/http-client/build.gradle @@ -5,9 +5,6 @@ buildscript { maven { url "http://repo.spring.io/libs-snapshot-local" } maven { url "http://repo.spring.io/libs-release-local" } maven { url "http://repo.spring.io/libs-staging-local" } - maven { url 'http://repo.spring.io/plugins-snapshot' } - maven { url "http://repo.spring.io/plugins-release-local" } - maven { url "http://repo.spring.io/plugins-staging-local/" } } dependencies { classpath "org.springframework.boot:spring-boot-gradle-plugin:1.4.0.BUILD-SNAPSHOT" @@ -41,7 +38,8 @@ dependencies { compile("org.springframework.boot:spring-boot-starter-actuator") testCompile 'org.springframework.cloud:spring-cloud-contract-wiremock' - testCompile "org.springframework.cloud:spring-cloud-starter-contract-stub-runner" + testCompile "org.springframework.boot:spring-boot-starter-test" + testCompile "com.example:http-server-restdocs:0.0.1-SNAPSHOT:stubs" } test { diff --git a/samples/standalone/restdocs/http-client/pom.xml b/samples/standalone/restdocs/http-client/pom.xml index 62759e90b3..c4dc7be630 100644 --- a/samples/standalone/restdocs/http-client/pom.xml +++ b/samples/standalone/restdocs/http-client/pom.xml @@ -141,22 +141,6 @@ false - - spring-plugin-snapshots - Spring Snapshots - http://repo.spring.io/plugins-snapshot-local - - true - - - - spring-plugin-milestones - Spring Milestones - http://repo.spring.io/plugins-milestone-local - - false - - diff --git a/samples/standalone/runTests.sh b/samples/standalone/runTests.sh index f3fb7755d5..477a9899e8 100755 --- a/samples/standalone/runTests.sh +++ b/samples/standalone/runTests.sh @@ -25,6 +25,14 @@ echo -e "\n\nBuilding client (uses Spring Cloud Contract Stub Runner)" cd dsl/http-client ./gradlew clean build -PverifierVersion=${VERIFIER_VERSION} --stacktrace cd $ROOT +echo -e "Building server (uses Spring Cloud Contract Verifier Gradle Plugin)" +cd restdocs/http-server +./gradlew clean build publishToMavenLocal -PverifierVersion=${VERIFIER_VERSION} --stacktrace +cd $ROOT +echo -e "\n\nBuilding client (uses Spring Cloud Contract Stub Runner)" +cd restdocs/http-client +./gradlew clean build -PverifierVersion=${VERIFIER_VERSION} --stacktrace +cd $ROOT echo -e "\n\nClearing saved stubs" rm -rf $LOCAL_MAVEN_REPO/repository/org/springframework/cloud/contract/testprojects/ diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractRequestHandler.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractRequestHandler.java index 26dc16763a..e6f70b1ea9 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractRequestHandler.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractRequestHandler.java @@ -18,6 +18,7 @@ package org.springframework.cloud.contract.wiremock.restdocs; import static org.assertj.core.api.Assertions.assertThat; +import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.HashMap; import java.util.LinkedHashMap; @@ -25,6 +26,7 @@ import java.util.Map; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -32,6 +34,11 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; +import com.github.tomakehurst.wiremock.client.RemoteMappingBuilder; +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.matching.MatchResult; +import com.github.tomakehurst.wiremock.servlet.WireMockHttpServletRequestAdapter; +import com.github.tomakehurst.wiremock.stubbing.StubMapping; import com.jayway.jsonpath.JsonPath; public class ContractRequestHandler implements ResultHandler { @@ -42,6 +49,8 @@ public class ContractRequestHandler implements ResultHandler { private MediaType contentType; private String name; + private RemoteMappingBuilder builder; + public ContractRequestHandler() { } @@ -55,7 +64,7 @@ public class ContractRequestHandler implements ResultHandler { @Override public void handle(MvcResult result) throws Exception { MockHttpServletRequest request = result.getRequest(); - Map configuration = getConfiguration(result); + Map configuration = getConfiguration(result); String actual = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8")); for (JsonPath jsonPath : jsonPaths.values()) { @@ -69,19 +78,50 @@ public class ContractRequestHandler implements ResultHandler { assertThat(contentType.includes(MediaType.valueOf(resultType))).isTrue() .as("content type did not match"); } + if (builder != null) { + builder.willReturn(getResponseDefinition(result)); + StubMapping stubMapping = builder.build(); + MatchResult match = stubMapping.getRequest() + .match(new WireMockHttpServletRequestAdapter(request)); + assertThat(match.isExactMatch()).as("wiremock did not match request").isTrue(); + configuration.put("contract.stubMapping", stubMapping); + } MockMvcRestDocumentation.document(this.name).handle(result); } + private ResponseDefinitionBuilder getResponseDefinition(MvcResult result) + throws UnsupportedEncodingException { + MockHttpServletResponse response = result.getResponse(); + ResponseDefinitionBuilder definition = ResponseDefinitionBuilder + .responseDefinition().withBody(response.getContentAsString()) + .withStatus(response.getStatus()); + addResponseHeaders(definition, response); + return definition; + } + + private void addResponseHeaders(ResponseDefinitionBuilder definition, + MockHttpServletResponse input) { + for (String name : input.getHeaderNames()) { + definition.withHeader(name, input.getHeader(name)); + } + } + private Map getConfiguration(MvcResult result) { @SuppressWarnings("unchecked") - Map map = (Map) result.getRequest().getAttribute(ATTRIBUTE_NAME_CONFIGURATION); - if (map==null) { + Map map = (Map) result.getRequest() + .getAttribute(ATTRIBUTE_NAME_CONFIGURATION); + if (map == null) { map = new HashMap<>(); result.getRequest().setAttribute(ATTRIBUTE_NAME_CONFIGURATION, map); } return map; } + public ContractRequestHandler wiremock(RemoteMappingBuilder builder) { + this.builder = builder; + return this; + } + public ContractRequestHandler jsonPath(String expression, Object... args) { compile(expression, args); return this; diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockServerRestDocsMatcherApplicationTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockServerRestDocsMatcherApplicationTests.java new file mode 100644 index 0000000000..85f4bcae3f --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockServerRestDocsMatcherApplicationTests.java @@ -0,0 +1,78 @@ +package org.springframework.cloud.contract.wiremock; + +import org.junit.ComparisonFailure; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +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.WiremockServerRestDocsMatcherApplicationTests.TestConfiguration; +import org.springframework.cloud.contract.wiremock.restdocs.WireMockRestDocs; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.annotation.DirtiesContext; +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.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import com.github.tomakehurst.wiremock.client.WireMock; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestConfiguration.class) +@AutoConfigureRestDocs(outputDir = "target/snippets") +@AutoConfigureMockMvc +@DirtiesContext +public class WiremockServerRestDocsMatcherApplicationTests { + + @Autowired + private MockMvc mockMvc; + + @Rule + public ExpectedException expected = ExpectedException.none(); + + @Test + public void matchesRequest() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/resource").content("greeting") + .contentType(MediaType.TEXT_PLAIN)) + .andExpect(MockMvcResultMatchers.content().string("Hello World")) + .andDo(WireMockRestDocs.verify() + .wiremock(WireMock.post(WireMock.urlPathEqualTo("/resource")) + .withRequestBody(WireMock.matching("greeting.*"))) + .stub("posted")); + } + + @Test + public void doesNotMatch() throws Exception { + expected.expect(ComparisonFailure.class); + expected.expectMessage("wiremock did not match"); + mockMvc.perform(MockMvcRequestBuilders.post("/resource").content("greeting") + .contentType(MediaType.TEXT_PLAIN)) + .andExpect(MockMvcResultMatchers.content().string("Hello World")) + .andDo(WireMockRestDocs.verify() + .wiremock(WireMock.post(WireMock.urlPathEqualTo("/resource")) + .withRequestBody(WireMock.matching("garbage.*"))) + .stub("posted")); + } + + @Configuration + @RestController + protected static class TestConfiguration { + + @ResponseBody + @RequestMapping(value = "/resource", method = RequestMethod.POST) + public String resource(@RequestBody String body) { + return "Hello World"; + } + + } + +}