From 4ceaff5b66f42ec9c29b24d9b9cb03305fee8d9e Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 29 Dec 2016 15:38:34 +0100 Subject: [PATCH] Added missing body pattern comparison without this change the eact contents of the body were mostly ignored with this change we support all WireMock body matching patterns fixes #135 --- README.adoc | 13 +- .../wiremock/WireMockRestServiceServer.java | 45 +++++ .../WiremockMockServerApplicationTests.java | 183 ++++++++++++++++++ .../mappings/body-matches-equaltojson.json | 13 ++ .../mappings/body-matches-equaltoxml.json | 13 ++ .../mappings/body-matches-jsonpath.json | 13 ++ .../mappings/body-matches-regex.json | 13 ++ .../mappings/body-matches-xpath.json | 13 ++ 8 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-equaltojson.json create mode 100644 spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-equaltoxml.json create mode 100644 spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-jsonpath.json create mode 100644 spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-regex.json create mode 100644 spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-xpath.json diff --git a/README.adoc b/README.adoc index a488936dcd..feb0e9a0f0 100644 --- a/README.adoc +++ b/README.adoc @@ -1,7 +1,7 @@ // Do not edit this file (e.g. go instead to src/main/asciidoc) image::https://badges.gitter.im/Join%20Chat.svg[Gitter, link="https://gitter.im/spring-cloud/spring-cloud-contract?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] -image::https://codecov.io/gh/spring-cloud/spring-cloud-contract/branch/master/graph/badge.svg["codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-contract"] +image::https://codecov.io/gh/spring-cloud/spring-cloud-contract/branch/1.0.x/graph/badge.svg["codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-contract"] image::https://circleci.com/gh/spring-cloud/spring-cloud-contract.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-contract"] == Spring Cloud Contract @@ -1318,7 +1318,7 @@ Example of a `pom.xml` inside the `server` folder. org.springframework.boot spring-boot-starter-parent - 1.4.2.BUILD-SNAPSHOT + 1.5.0.BUILD-SNAPSHOT @@ -1566,17 +1566,12 @@ follow the guidelines below. === Sign the Contributor License Agreement Before we accept a non-trivial patch or pull request we will need you to sign the -https://cla.pivotal.io/sign/spring[Contributor License Agreement]. +https://support.springsource.com/spring_committer_signup[contributor's agreement]. Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. Active contributors might be asked to join the core team, and given the ability to merge pull requests. -=== Code of Conduct -This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc[code of -conduct]. By participating, you are expected to uphold this code. Please report -unacceptable behavior to spring-code-of-conduct@pivotal.io. - === Code Conventions and Housekeeping None of these is essential for a pull request, but they will all help. They can also be added after the original pull request but before a merge. @@ -1584,7 +1579,7 @@ added after the original pull request but before a merge. * Use the Spring Framework code format conventions. If you use Eclipse you can import formatter settings using the `eclipse-code-formatter.xml` file from the - https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring + https://github.com/spring-cloud/build/tree/master/eclipse-coding-conventions.xml[Spring Cloud Build] project. If using IntelliJ, you can use the http://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter Plugin] to import the same file. diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockRestServiceServer.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockRestServiceServer.java index db83c5271a..f9a07ee0a9 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockRestServiceServer.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockRestServiceServer.java @@ -23,18 +23,24 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import javax.xml.xpath.XPathExpressionException; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; +import org.hamcrest.MatcherAssert; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.client.MockRestServiceServer.MockRestServiceServerBuilder; +import org.springframework.test.web.client.RequestMatcher; import org.springframework.test.web.client.ResponseActions; +import org.springframework.test.web.client.match.MockRestRequestMatchers; import org.springframework.test.web.client.response.DefaultResponseCreator; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; @@ -44,8 +50,12 @@ import com.github.tomakehurst.wiremock.common.Json; import com.github.tomakehurst.wiremock.http.HttpHeader; import com.github.tomakehurst.wiremock.http.MultiValue; import com.github.tomakehurst.wiremock.http.ResponseDefinition; +import com.github.tomakehurst.wiremock.matching.MatchResult; +import com.github.tomakehurst.wiremock.matching.MatchesJsonPathPattern; +import com.github.tomakehurst.wiremock.matching.MatchesXPathPattern; import com.github.tomakehurst.wiremock.matching.MultiValuePattern; import com.github.tomakehurst.wiremock.matching.RequestPattern; +import com.github.tomakehurst.wiremock.matching.StringValuePattern; import com.github.tomakehurst.wiremock.stubbing.StubMapping; import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; @@ -192,12 +202,47 @@ public class WireMockRestServiceServer { for (StubMapping mapping : mappings) { ResponseActions expect = server.expect(requestTo(request(mapping.getRequest()))); expect.andExpect(method(HttpMethod.valueOf(mapping.getRequest().getMethod().getName()))); + mapping.getRequest().getBodyPatterns(); + bodyPatterns(expect, mapping.getRequest()); requestHeaders(expect, mapping.getRequest()); expect.andRespond(response(mapping.getResponse())); } return server; } + private void bodyPatterns(ResponseActions expect, RequestPattern request) { + if (request.getBodyPatterns() == null) { + return; + } + for (final StringValuePattern pattern : request.getBodyPatterns()) { + if (pattern instanceof MatchesJsonPathPattern) { + expect.andExpect(MockRestRequestMatchers.jsonPath(((MatchesJsonPathPattern) pattern).getMatchesJsonPath()).exists()); + } else if (pattern instanceof MatchesXPathPattern) { + expect.andExpect(xpath((MatchesXPathPattern) pattern)); + } + expect.andExpect(matchContents(pattern)); + } + } + + private RequestMatcher matchContents(final StringValuePattern pattern) { + return new RequestMatcher() { + @Override public void match(ClientHttpRequest request) + throws IOException, AssertionError { + MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;; + MatchResult result = pattern.match(mockRequest.getBodyAsString()); + MatcherAssert.assertThat("Request as string [" + mockRequest.getBodyAsString() + "]", result.isExactMatch()); + } + }; + } + + private RequestMatcher xpath(MatchesXPathPattern pattern) { + try { + return MockRestRequestMatchers.xpath(pattern.getMatchesXPath()).exists(); + } catch (XPathExpressionException e) { + throw new IllegalStateException(e); + } + } + private String request(RequestPattern request) { return this.baseUrl + (request.getUrlPath() == null ? (request.getUrl() == null ? "/" : request.getUrl()) : request.getUrlPath()); diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockMockServerApplicationTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockMockServerApplicationTests.java index f61d2a4c31..8f57a61452 100644 --- a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockMockServerApplicationTests.java +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/WiremockMockServerApplicationTests.java @@ -1,6 +1,8 @@ package org.springframework.cloud.contract.wiremock; import java.net.URI; +import java.util.Collections; +import java.util.List; import org.junit.Test; import org.springframework.http.MediaType; @@ -9,6 +11,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; +import com.fasterxml.jackson.annotation.JsonCreator; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -217,4 +221,183 @@ public class WiremockMockServerApplicationTests { String.class)).isEqualTo("Hello World"); } + // Issue #135 + @Test + public void postWithBodyMatchingJsonPaths() throws Exception { + WireMockRestServiceServer.with(this.restTemplate) // + .baseUrl("http://example.org") // + .stubs("classpath:/mappings/body-matches-jsonpath.json").build(); + + assertThat(this.restTemplate.postForObject("http://example.org/body", + new Things(Collections.singletonList(new Thing("RequiredThing"))), String.class)) + .isEqualTo("Hello Body"); + } + + @Test + public void postWithRequestThatHasNonMatchingJsonPaths() throws Exception { + WireMockRestServiceServer.with(this.restTemplate) // + .baseUrl("http://example.org") // + .stubs("classpath:/mappings/body-matches-jsonpath.json").build(); + + String response; + try { + response = this.restTemplate.postForObject("http://example.org/body", + new Things(Collections.singletonList(new Thing("AbsentThing"))), String.class); + } catch (AssertionError e) { + response = null; + } + if (null != response) { + fail("There was a response for a request that shouldn't be matched : " + response); + } + } + + @Test + public void postWithBodyMatchingXPath() throws Exception { + WireMockRestServiceServer.with(this.restTemplate) // + .baseUrl("http://example.org") // + .stubs("classpath:/mappings/body-matches-xpath.json").build(); + + assertThat(this.restTemplate.exchange( + RequestEntity.post(URI.create("http://example.org/body")) + .contentType(MediaType.APPLICATION_XML) + .body("RequiredThing"), + String.class).getBody()).isEqualTo("Hello Body"); + } + + @Test + public void postWithRequestThatHasNonMatchingXPaths() throws Exception { + WireMockRestServiceServer.with(this.restTemplate) // + .baseUrl("http://example.org") // + .stubs("classpath:/mappings/body-matches-xpath.json").build(); + + ResponseEntity response; + try { + response = this.restTemplate.exchange( + RequestEntity.post(URI.create("http://example.org/body")) + .contentType(MediaType.APPLICATION_XML) + .body(""), + String.class); + } catch (AssertionError e) { + response = null; + } + if (null != response) { + fail("There was a response for a request that shouldn't be matched : " + response); + } + } + + @Test + public void postWithBodyMatchingJson() throws Exception { + WireMockRestServiceServer.with(this.restTemplate) // + .baseUrl("http://example.org") // + .stubs("classpath:/mappings/body-matches-equaltojson.json").build(); + + assertThat(this.restTemplate.postForObject("http://example.org/body", + new Things(Collections.singletonList(new Thing("RequiredThing"))), String.class)) + .isEqualTo("Hello Body"); + } + + @Test + public void postWithRequestThatHasNonMatchingJson() throws Exception { + WireMockRestServiceServer.with(this.restTemplate) // + .baseUrl("http://example.org") // + .stubs("classpath:/mappings/body-matches-equaltojson.json").build(); + + String response; + try { + response = this.restTemplate.postForObject("http://example.org/body", + new Things(Collections.singletonList(new Thing("AbsentThing"))), String.class); + } catch (AssertionError e) { + response = null; + } + if (null != response) { + fail("There was a response for a request that shouldn't be matched : " + response); + } + } + + @Test + public void postWithBodyMatchingXml() throws Exception { + WireMockRestServiceServer.with(this.restTemplate) // + .baseUrl("http://example.org") // + .stubs("classpath:/mappings/body-matches-equaltoxml.json").build(); + + assertThat(this.restTemplate.exchange( + RequestEntity.post(URI.create("http://example.org/body")) + .contentType(MediaType.APPLICATION_XML) + .body("RequiredThing"), + String.class).getBody()).isEqualTo("Hello Body"); + } + + @Test + public void postWithRequestThatHasNonMatchingXml() throws Exception { + WireMockRestServiceServer.with(this.restTemplate) // + .baseUrl("http://example.org") // + .stubs("classpath:/mappings/body-matches-equaltoxml.json").build(); + + ResponseEntity response; + try { + response = this.restTemplate.exchange( + RequestEntity.post(URI.create("http://example.org/body")) + .contentType(MediaType.APPLICATION_XML) + .body("AbsentThing"), + String.class); + } catch (AssertionError e) { + response = null; + } + if (null != response) { + fail("There was a response for a request that shouldn't be matched : " + response); + } + } + + @Test + public void postWithBodyMatchingRegex() throws Exception { + WireMockRestServiceServer.with(this.restTemplate) // + .baseUrl("http://example.org") // + .stubs("classpath:/mappings/body-matches-regex.json").build(); + + assertThat(this.restTemplate.exchange( + RequestEntity.post(URI.create("http://example.org/body")) + .contentType(MediaType.APPLICATION_XML) + .body("RequiredThing"), + String.class).getBody()).isEqualTo("Hello Body"); + } + + @Test + public void postWithRequestThatHasNonMatchingRegex() throws Exception { + WireMockRestServiceServer.with(this.restTemplate) // + .baseUrl("http://example.org") // + .stubs("classpath:/mappings/body-matches-regex.json").build(); + + ResponseEntity response; + try { + response = this.restTemplate.exchange( + RequestEntity.post(URI.create("http://example.org/body")) + .contentType(MediaType.APPLICATION_XML) + .body("AbsentThing"), + String.class); + } catch (AssertionError e) { + response = null; + } + if (null != response) { + fail("There was a response for a request that shouldn't be matched : " + response); + } + } + + public static class Things { + public List things; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Things(List things) { + this.things = things; + } + } + + public static class Thing { + public String name; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Thing(String name) { + this.name = name; + } + } + } diff --git a/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-equaltojson.json b/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-equaltojson.json new file mode 100644 index 0000000000..0a866bc1ad --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-equaltojson.json @@ -0,0 +1,13 @@ +{ + "request" : { + "urlPath" : "/body", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{ \"things\": [ { \"name\" : \"RequiredThing\" } ] }" + } ] + }, + "response" : { + "status" : 200, + "body" : "Hello Body" + } +} \ No newline at end of file diff --git a/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-equaltoxml.json b/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-equaltoxml.json new file mode 100644 index 0000000000..5e83f4d78d --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-equaltoxml.json @@ -0,0 +1,13 @@ +{ + "request" : { + "urlPath" : "/body", + "method" : "POST", + "bodyPatterns" : [ { + "equalToXml" : "RequiredThing" + } ] + }, + "response" : { + "status" : 200, + "body" : "Hello Body" + } +} \ No newline at end of file diff --git a/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-jsonpath.json b/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-jsonpath.json new file mode 100644 index 0000000000..78ba614859 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-jsonpath.json @@ -0,0 +1,13 @@ +{ + "request" : { + "urlPath" : "/body", + "method" : "POST", + "bodyPatterns" : [ { + "matchesJsonPath" : "$.things[?(@.name == 'RequiredThing')]" + } ] + }, + "response" : { + "status" : 200, + "body" : "Hello Body" + } +} \ No newline at end of file diff --git a/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-regex.json b/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-regex.json new file mode 100644 index 0000000000..bcc2c1df06 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-regex.json @@ -0,0 +1,13 @@ +{ + "request" : { + "urlPath" : "/body", + "method" : "POST", + "bodyPatterns" : [ { + "matches" : ".*RequiredThing.*" + } ] + }, + "response" : { + "status" : 200, + "body" : "Hello Body" + } +} \ No newline at end of file diff --git a/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-xpath.json b/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-xpath.json new file mode 100644 index 0000000000..4a8f2615c3 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/resources/mappings/body-matches-xpath.json @@ -0,0 +1,13 @@ +{ + "request" : { + "urlPath" : "/body", + "method" : "POST", + "bodyPatterns" : [ { + "matchesXPath" : "//things" + } ] + }, + "response" : { + "status" : 200, + "body" : "Hello Body" + } +} \ No newline at end of file