From 5843234fa1e1f7f7be27b730eb23cc4ba5033c98 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 14 Apr 2017 09:40:15 +0200 Subject: [PATCH] Fixed invalid escaping of characters for regex without this change some regex contained invalid characters. Even though the code compiles the regular expressions passed to json assert are invalid. with this change that gets fixed and tests are added. We're now checking if the code can be executed and not only compiled. We also check if WireMock fed with the stubs generated from these cases works fine and not only do we check if stub mappings are properly generated. fixes #250 --- .../internal/PatternValueDslProperty.groovy | 2 +- .../spec/internal/RegexPatterns.groovy | 48 ++++- .../spec/internal/RegexPatternsSpec.groovy | 69 ++++++- .../spring-cloud-contract-converters/pom.xml | 21 +++ .../DslToWireMockClientConverterSpec.groovy | 173 ++++++++++++++++-- .../util/DelegatingJsonVerifiable.java | 7 +- .../util/JsonToJsonPathsConverter.groovy | 3 +- .../verifier/util/MapConverter.groovy | 4 +- .../JaxRsClientMethodBuilderSpec.groovy | 15 +- .../MockMvcMethodBodyBuilderSpec.groovy | 17 +- .../util/JsonToJsonPathsConverterSpec.groovy | 8 + .../verifier/util/SyntaxChecker.groovy | 22 ++- 12 files changed, 349 insertions(+), 40 deletions(-) diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PatternValueDslProperty.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PatternValueDslProperty.groovy index 72a8e85df0..6441c546f6 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PatternValueDslProperty.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PatternValueDslProperty.groovy @@ -49,7 +49,7 @@ abstract class PatternValueDslProperty { } T anyIpAddress() { - return createAndValidateProperty(RegexPatterns.IP_ADDRESS) + return createAndValidateProperty(RegexPatterns.IP_ADDRESS, "192.168.0." + this.random.nextInt(10)) } T anyHostname() { diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy index 370c5c7449..c091803d45 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/RegexPatterns.groovy @@ -37,9 +37,9 @@ class RegexPatterns { protected static final Pattern ONLY_ALPHA_UNICODE = Pattern.compile(/[\p{L}]*/) protected static final Pattern NUMBER = Pattern.compile('-?\\d*(\\.\\d+)?') protected static final Pattern IP_ADDRESS = Pattern.compile('([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])') - protected static final Pattern HOSTNAME_PATTERN = Pattern.compile('((http[s]?|ftp):\\/)\\/?([^:\\/\\s]+)(:[0-9]{1,5})?') - protected static final Pattern EMAIL = Pattern.compile('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}'); - protected static final Pattern URL = Pattern.compile('((www\\.|(http|https|ftp|news|file)+\\:\\/\\/)[_.a-z0-9-]+\\.[a-z0-9\\/_:@=.+?,##%&~-]*[^.|\\\'|\\# |!|\\(|?|,| |>|<|;|\\)])') + protected static final Pattern HOSTNAME_PATTERN = Pattern.compile('((http[s]?|ftp):/)/?([^:/\\s]+)(:[0-9]{1,5})?') + protected static final Pattern EMAIL = Pattern.compile('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}') + protected static final Pattern URL = UrlHelper.URL protected static final Pattern UUID = Pattern.compile('[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}') protected static final Pattern ANY_DATE = Pattern.compile('(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])') protected static final Pattern ANY_DATE_TIME = Pattern.compile('([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])') @@ -118,3 +118,45 @@ class RegexPatterns { return ".*--(.*)\r\nContent-Disposition: form-data; name=\"$name\"; filename=\"$filename\"\r\n(Content-Type: .*\r\n)?(Content-Length: \\d+\r\n)?\r\n$content\r\n--\\1.*"; } } + +/** + * Taken from https://gist.github.com/skeller88/5eb73dc0090d4ff1249a + */ +class UrlHelper { + /** + * Example: "http". Also called 'protocol'. + * Scheme component is optional, even though the RFC doesn't make it optional. Since this regex is validating a + * submitted callback url, which determines where the browser will navigate to after a successful authentication, + * the browser will use http or https for the scheme by default. + * Not borrowed from dperini in order to allow any scheme type. + */ + private static final String REGEX_SCHEME = "[A-Za-z][+-.\\w^_]*:" + + // Example: "//". + private static final String REGEX_AUTHORATIVE_DECLARATION = "/{2}" + + // Optional component. Example: "suzie:abc123@". The use of the format "user:password" is deprecated. + private static final String REGEX_USERINFO = "(?:\\S+(?::\\S*)?@)?" + + // Examples: "fitbit.com", "22.231.113.64". + private static final String REGEX_HOST = "(?:" + + // @Author = http://www.regular-expressions.info/examples.html + // IP address + "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "|" + + // host name + "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" + + // domain name + "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" + + // TLD identifier must have >= 2 characters + "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))" + + // Example: ":8042". + private static final String REGEX_PORT = "(?::\\d{2,5})?" + + //Example: "/user/heartrate?foo=bar#element1". + private static final String REGEX_RESOURCE_PATH = "(?:/\\S*)?" + + protected static final Pattern URL = Pattern.compile("^(?:(?:" + REGEX_SCHEME + REGEX_AUTHORATIVE_DECLARATION + ")?" + + REGEX_USERINFO + REGEX_HOST + REGEX_PORT + REGEX_RESOURCE_PATH + ")\$") +} diff --git a/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/RegexPatternsSpec.groovy b/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/RegexPatternsSpec.groovy index 53255eb140..9eb48650ba 100644 --- a/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/RegexPatternsSpec.groovy +++ b/spring-cloud-contract-spec/src/test/groovy/org/springframework/cloud/contract/spec/internal/RegexPatternsSpec.groovy @@ -55,13 +55,76 @@ class RegexPatternsSpec extends Specification { 'a.b.' || false } + // @see http://formvalidation.io/validators/uri/ def "should generate a regex for url [#textToMatch] that is a match [#shouldMatch]"() { expect: shouldMatch == Pattern.compile(regexPatterns.url()).matcher(textToMatch).matches() where: - textToMatch || shouldMatch - 'ftp://asd.com:9090/asd/a?a=b' || true - 'a.b.' || false + textToMatch || shouldMatch + 'ftp://asd.com:9090/asd/a?a=b' || true + 'http://foo.com/blah_blah' || true + 'http://foo.com/blah_blah/' || true + 'http://foo.com/blah_blah_(wikipedia)' || true + 'http://foo.com/blah_blah_(wikipedia)_(again)' || true + 'http://www.example.com/wpstyle/?p=364' || true + 'https://www.example.com/foo/?bar=baz&inga=42&quux' || true + 'http://✪df.ws/123' || true + 'http://userid:password@example.com:8080' || true + 'http://userid:password@example.com:8080/' || true + 'http://userid@example.com' || true + 'http://userid@example.com/' || true + 'http://userid@example.com:8080' || true + 'http://userid@example.com:8080/' || true + 'http://userid:password@example.com' || true + 'http://userid:password@example.com/' || true + 'http://142.42.1.1/' || true + 'http://142.42.1.1:8080/' || true + 'http://⌘.ws' || true + 'http://⌘.ws/' || true + 'http://foo.com/blah_(wikipedia)#cite-1' || true + 'http://foo.com/blah_(wikipedia)_blah#cite-1' || true + 'http://foo.com/unicode_(✪)_in_parens' || true + 'http://foo.com/(something)?after=parens' || true + 'http://☺.damowmow.com/' || true + 'http://code.google.com/events/#&product=browser' || true + 'http://j.mp' || true + 'ftp://foo.bar/baz' || true + 'http://foo.bar/?q=Test%20URL-encoded%20stuff' || true + 'http://1337.net' || true + 'http://a.b-c.de' || true + 'http://223.255.255.254' || true + 'foo.com' || true + 'a.b.' || false + 'http://' || false + 'http://.' || false + 'http://..' || false + 'http://../' || false + 'http://?' || false + 'http://??' || false + 'http://??/' || false + 'http://#' || false + 'http://##' || false + 'http://##/' || false + 'http://foo.bar?q=Spaces should be encoded' || false + '//' || false + '//a' || false + '///a' || false + '///' || false + 'http:///a' || false + 'rdar://1234' || false + 'h://test' || false + 'http:// shouldfail.com' || false + ':// should fail' || false + 'http://foo.bar/foo(bar)baz quux' || false + 'http://-error-.invalid/' || false + 'http://-a.b.co' || false + 'http://a.b-.co' || false + 'http://1.1.1.1.1' || false + 'http://123.123.123' || false + 'http://3628126748' || false + 'http://.www.foo.bar/' || false + 'http://www.foo.bar./' || false + 'http://.www.foo.bar./' || false } def "should generate a regex for a number [#textToMatch] that is a match [#shouldMatch]"() { diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml index 9e84438079..09a81e7508 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml @@ -49,6 +49,27 @@ spock-global-unroll test + + org.springframework.cloud + spring-cloud-contract-wiremock + test + + + org.springframework.boot + spring-boot-starter-web + test + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-jetty + test + diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy index 7d38754aa5..e5bd1c9539 100755 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy @@ -16,13 +16,18 @@ package org.springframework.cloud.contract.verifier.wiremock +import com.github.tomakehurst.wiremock.junit.WireMockRule import com.github.tomakehurst.wiremock.matching.RegexPattern import com.github.tomakehurst.wiremock.stubbing.StubMapping +import groovy.json.JsonOutput import org.junit.Rule import org.junit.rules.TemporaryFolder import org.skyscreamer.jsonassert.JSONAssert +import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubMapping import org.springframework.cloud.contract.verifier.file.ContractMetadata +import org.springframework.http.RequestEntity +import org.springframework.util.SocketUtils import spock.lang.Issue import spock.lang.Specification @@ -30,8 +35,15 @@ import java.util.regex.Pattern class DslToWireMockClientConverterSpec extends Specification { - @Rule - public TemporaryFolder tmpFolder = new TemporaryFolder() + static int port = SocketUtils.findAvailableTcpPort() + @Rule public WireMockRule wireMockRule = new WireMockRule(port) + @Rule public TemporaryFolder tmpFolder = new TemporaryFolder() + TestRestTemplate restTemplate = new TestRestTemplate() + String url + + def setup() { + url = "http://localhost:${port}" + } def "should convert DSL file to WireMock JSON"() { given: @@ -56,7 +68,11 @@ class DslToWireMockClientConverterSpec extends Specification { {"request":{"method":"PUT","urlPattern":"/[0-9]{2}"},"response":{"status":200}} ''', json, false) and: - stubMappingIsValidWireMockStub(json) + StubMapping mapping = stubMappingIsValidWireMockStub(json) + and: + wireMockRule.addStubMapping(mapping) + and: + restTemplate.exchange(RequestEntity.put("${url}/12".toURI()).body(""), String) } @Issue("196") @@ -89,7 +105,11 @@ class DslToWireMockClientConverterSpec extends Specification { }} ''', json, false) and: - stubMappingIsValidWireMockStub(json) + StubMapping mapping = stubMappingIsValidWireMockStub(json) + and: + wireMockRule.addStubMapping(mapping) + and: + restTemplate.exchange(RequestEntity.get("${url}/foo".toURI()).build(), String) } def "should convert DSL file with a nested list to WireMock JSON"() { @@ -104,7 +124,6 @@ class DslToWireMockClientConverterSpec extends Specification { url '/api/12' headers { header 'Content-Type': 'application/vnd.org.springframework.cloud.contract.verifier.twitter-places-analyzer.v1+json' - } body ''' [{ @@ -195,10 +214,43 @@ class DslToWireMockClientConverterSpec extends Specification { } ''', json, false) and: - stubMappingIsValidWireMockStub(json) + StubMapping mapping = stubMappingIsValidWireMockStub(json) + and: + wireMockRule.addStubMapping(mapping) + and: + restTemplate.exchange(RequestEntity.put("${url}/api/12".toURI()) + .header('Content-Type', 'application/vnd.org.springframework.cloud.contract.verifier.twitter-places-analyzer.v1+json') + .body(''' + [{ + "created_at": "Sat Jul 26 09:38:57 +0000 2014", + "id": 492967299297845248, + "id_str": "492967299297845248", + "text": "Gonna see you at Warsaw", + "place": + { + "attributes":{}, + "bounding_box": + { + "coordinates": + [[ + [-77.119759,38.791645], + [-76.909393,38.791645], + [-76.909393,38.995548], + [-77.119759,38.995548] + ]], + "type":"Polygon" + }, + "country":"United States", + "country_code":"US", + "full_name":"Washington, DC", + "id":"01fbe706f872cb32", + "name":"Washington", + "place_type":"city", + "url": "http://api.twitter.com/1/geo/id/01fbe706f872cb32.json" + } + }]'''), String) } - @Issue("262") def "should create stub with map inside list"() { given: @@ -233,7 +285,13 @@ class DslToWireMockClientConverterSpec extends Specification { {"request":{"urlPath":"/foos","method":"GET"},"response":{"body":"[{\\"id\\":\\"123\\"},{\\"id\\":\\"567\\"}]"}} ''', json, false) and: - stubMappingIsValidWireMockStub(json) + StubMapping mapping = stubMappingIsValidWireMockStub(json) + and: + wireMockRule.addStubMapping(mapping) + and: + def response = restTemplate.exchange(RequestEntity.get("${url}/foos".toURI()).build(), String) + response.headers.get('Content-Type') == ['application/json'] + JSONAssert.assertEquals('''[ { "id":"123" }, { "id": "567" } ]''', response.body, false) } @@ -265,7 +323,12 @@ class DslToWireMockClientConverterSpec extends Specification { and: !json.contains('cursor') and: - stubMappingIsValidWireMockStub(json) + StubMapping mapping = stubMappingIsValidWireMockStub(json) + and: + wireMockRule.addStubMapping(mapping) + and: + def response = restTemplate.exchange(RequestEntity.get("${url}/foos".toURI()).build(), String) + response.body } def 'should convert dsl to wiremock to show it in the docs'() { @@ -332,7 +395,17 @@ class DslToWireMockClientConverterSpec extends Specification { // end::wiremock[] , json, false) and: - stubMappingIsValidWireMockStub(json) + StubMapping mapping = stubMappingIsValidWireMockStub(json) + and: + wireMockRule.addStubMapping(mapping) + and: + def response = restTemplate.exchange(RequestEntity.post("${url}/users/password".toURI()) + .header("Content-Type", "application/json") + .body('''{"email":"abc@abc.com", "callback_url":"http://partners.com"}''') + , String) + response.headers.get('Content-Type') == ['application/json'] + response.statusCodeValue == 404 + JSONAssert.assertEquals('''{"code":"123123","message":"User not found by email == [not.existing@user.com]"}"''', response.body, false) } def 'should convert dsl to wiremock with stub matchers'() { @@ -343,7 +416,7 @@ class DslToWireMockClientConverterSpec extends Specification { file.write(''' org.springframework.cloud.contract.spec.Contract.make { request { - method 'GET' + method 'POST' urlPath '/get' body([ duck: 123, @@ -451,7 +524,7 @@ class DslToWireMockClientConverterSpec extends Specification { { "request" : { "urlPath" : "/get", - "method" : "GET", + "method" : "POST", "headers" : { "Content-Type" : { "matches" : "application/json.*" @@ -501,7 +574,60 @@ class DslToWireMockClientConverterSpec extends Specification { //end::matchers[] , json, false) and: - stubMappingIsValidWireMockStub(json) + StubMapping mapping = stubMappingIsValidWireMockStub(json) + and: + wireMockRule.addStubMapping(mapping) + and: + def response = restTemplate.exchange(RequestEntity.post("${url}/get".toURI()) + .header("Content-Type", "application/json") + .body(JsonOutput.toJson([ + duck: 123, + alpha: "abc", + number: 123, + aBoolean: true, + date: "2017-01-01", + dateTime: "2017-01-01T01:23:45", + time: "01:02:34", + valueWithoutAMatcher: "foo", + valueWithTypeMatch: "string", + list: [ + some: [ + nested: [ + json: "with value", + anothervalue: 4 + ] + ], + someother: [ + nested: [ + json: "with value", + anothervalue: 4 + ] + ] + ] + ])) + , String) + response.headers.get('Content-Type') == ['application/json'] + response.statusCodeValue == 200 + JSONAssert.assertEquals(JsonOutput.toJson([ + duck: 123, + alpha: "abc", + number: 123, + aBoolean: true, + date: "2017-01-01", + dateTime: "2017-01-01T01:23:45", + time: "01:02:34", + valueWithoutAMatcher: "foo", + valueWithTypeMatch: "string", + valueWithMin: [ + 1,2,3 + ], + valueWithMax: [ + 1,2,3 + ], + valueWithMinMax: [ + 1,2,3 + ], + ]), response.body, false) } def 'should convert dsl to wiremock with stub matchers with docs example'() { @@ -514,7 +640,7 @@ class DslToWireMockClientConverterSpec extends Specification { priority 1 request { method 'POST' - url '/users/password' + url '/users/password2' headers { header 'Content-Type': 'application/json' } @@ -550,7 +676,7 @@ class DslToWireMockClientConverterSpec extends Specification { ''' { "request" : { - "url" : "/users/password", + "url" : "/users/password2", "method" : "POST", "bodyPatterns" : [ { "matchesJsonPath" : "$[?(@.email =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,4})/)]" @@ -575,15 +701,26 @@ class DslToWireMockClientConverterSpec extends Specification { ''' , json, false) and: - stubMappingIsValidWireMockStub(json) + StubMapping mapping = stubMappingIsValidWireMockStub(json) + and: + wireMockRule.addStubMapping(mapping) + and: + def response = restTemplate.exchange(RequestEntity.post("${url}/users/password2".toURI()) + .header("Content-Type", "application/json") + .body('''{"email":"abc@abc.com", "callback_url":"http://partners.com"}''') + , String) + response.headers.get('Content-Type') == ['application/json'] + response.statusCodeValue == 404 + JSONAssert.assertEquals('''{"code":"123123","message":"User not found by email == [not.existing@user.com]"}"''', response.body, false) } - - void stubMappingIsValidWireMockStub(String mappingDefinition) { + + StubMapping stubMappingIsValidWireMockStub(String mappingDefinition) { StubMapping stubMapping = WireMockStubMapping.buildFrom(mappingDefinition) stubMapping.request.bodyPatterns.findAll { it.isPresent() && it instanceof RegexPattern }.every { Pattern.compile(it.getValue()) } assert !mappingDefinition.contains('org.springframework.cloud.contract.spec.internal') + return stubMapping } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java index beba817b81..792ccb1d99 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/DelegatingJsonVerifiable.java @@ -16,13 +16,13 @@ package org.springframework.cloud.contract.verifier.util; +import static org.apache.commons.lang3.StringEscapeUtils.escapeJava; + import java.util.LinkedList; import java.util.regex.Pattern; import com.toomuchcoding.jsonassert.JsonVerifiable; -import static org.apache.commons.lang3.StringEscapeUtils.escapeJava; - /** * Implementation of the {@link MethodBufferingJsonVerifiable} that contains a list * of String method commands that need to be executed to assert JSONs. @@ -212,7 +212,8 @@ class DelegatingJsonVerifiable implements MethodBufferingJsonVerifiable { * an double escaped text. Related to https://github.com/spring-cloud/spring-cloud-contract/issues/169 */ private String escapedHackedJavaText(String value) { - return escapeJava(value).replace("\\\"", "\""); + return escapeJava(value) + .replace("\\\"", "\""); } @Override diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy index 5fd357735c..6b31ae36dd 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy @@ -133,7 +133,8 @@ class JsonToJsonPathsConverter { throw new IllegalStateException("Value [${bodyMatcher.path()}] not found in JSON [${JsonOutput.toJson(body)}]", e) } } else { - return "=~ /(${value})/" + String convertedValue = value.toString().replace('/', '\\\\/') + return "=~ /(${convertedValue})/" } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy index e8c38e1740..90465c2b3e 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy @@ -58,7 +58,7 @@ class MapConverter { } } catch (Exception ignore) { } - return extractValue(value, closure); + return extractValue(value, closure) } else if (value instanceof Map) { return convert(value as Map, closure) } else if (value instanceof List) { @@ -77,7 +77,7 @@ class MapConverter { if (newValue instanceof Map || newValue instanceof List || newValue instanceof String && value) { return transformValues(newValue, closure) } - return newValue; + return newValue }) } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy index 9a2fa71b7f..af7093e01d 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy @@ -1039,8 +1039,8 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub then: test.contains('assertThatJson(parsedJson).field("aBoolean").matches("(true|false)")') test.contains('assertThatJson(parsedJson).field("alpha").matches("[\\\\p{L}]*")') - test.contains('assertThatJson(parsedJson).field("hostname").matches("((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?")') - test.contains('assertThatJson(parsedJson).field("url").matches("((www\\\\.|(http|https|ftp|news|file)+\\\\:\\\\/\\\\/)[_.a-z0-9-]+\\\\.[a-z0-9\\\\/_:@=.+?,##%&~-]*[^.|\\\\\'|\\\\# |!|\\\\(|?|,| |>|<|;|\\\\)])")') + test.contains('assertThatJson(parsedJson).field("hostname").matches("((http[s]?|ftp):/)/?([^:/\\\\s]+)(:[0-9]{1,5})?")') + test.contains('assertThatJson(parsedJson).field("url").matches("^(?:(?:[A-Za-z][+-.\\\\w^_]*:/{2})?(?:\\\\S+(?::\\\\S*)?@)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,})))(?::\\\\d{2,5})?(?:/\\\\S*)?)') test.contains('assertThatJson(parsedJson).field("number").matches("-?\\\\d*(\\\\.\\\\d+)?")') test.contains('assertThatJson(parsedJson).field("email").matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,4}")') test.contains('assertThatJson(parsedJson).field("ip").matches("([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])")') @@ -1053,8 +1053,19 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub test.contains('assertThatJson(parsedJson).field("nonEmptyString").matches(".+")') test.contains('assertThatJson(parsedJson).field("anyOf").matches("^foo' + endOfLineRegexSymbol + '|^bar' + endOfLineRegexSymbol + '")') !test.contains('cursor') + !test.contains('REGEXP>>') and: SyntaxChecker.tryToCompile(methodBuilderName, blockBuilder.toString()) + and: + String jsonSample = '''\ + String json = "{\\"duck\\":\\"8\\",\\"alpha\\":\\"YAJEOWYGMFBEWPMEMAZI\\",\\"number\\":-2095030871,\\"aBoolean\\":true,\\"ip\\":\\"129.168.99.100\\",\\"hostname\\":\\"http://foo389886219.com\\",\\"email\\":\\"foo@bar1367573183.com\\",\\"url\\":\\"http://foo-597104692.com\\",\\"uuid\\":\\"e436b817-b764-49a2-908e-967f2f99eb9f\\",\\"date\\":\\"2014-04-14\\",\\"dateTime\\":\\"2011-01-11T12:23:34\\",\\"time\\":\\"12:20:30\\",\\"iso8601WithOffset\\":\\"2015-05-15T12:23:34.123Z\\",\\"nonBlankString\\":\\"EPZWVIRHSUAPBJMMQSFO\\",\\"nonEmptyString\\":\\"RVMFDSEQFHRQFVUVQPIA\\",\\"anyOf\\":\\"foo\\"}"; + DocumentContext parsedJson = JsonPath.parse(json); + ''' + and: + LinkedList lines = [] as LinkedList + test.eachLine { if (it.contains("assertThatJson")) lines << it else it } + lines.addFirst(jsonSample) + SyntaxChecker.tryToRun(methodBuilderName, lines.join("\n")) where: methodBuilderName | methodBuilder | endOfLineRegexSymbol "JaxRsClientSpockMethodRequestProcessingBodyBuilder"| { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$' diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy index f972024375..3a0c65a05f 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy @@ -1880,7 +1880,7 @@ World.'''""" } @Issue('#149') - def "should allow easier way of providing dynamic values for [#methodBuilderName]"() { + def "should allow easier way of providing dynamic values for [#methodBuilderName]"() { given: Contract contractDsl = Contract.make { request { @@ -1940,8 +1940,8 @@ World.'''""" then: test.contains('assertThatJson(parsedJson).field("aBoolean").matches("(true|false)")') test.contains('assertThatJson(parsedJson).field("alpha").matches("[\\\\p{L}]*")') - test.contains('assertThatJson(parsedJson).field("hostname").matches("((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?")') - test.contains('assertThatJson(parsedJson).field("url").matches("((www\\\\.|(http|https|ftp|news|file)+\\\\:\\\\/\\\\/)[_.a-z0-9-]+\\\\.[a-z0-9\\\\/_:@=.+?,##%&~-]*[^.|\\\\\'|\\\\# |!|\\\\(|?|,| |>|<|;|\\\\)])")') + test.contains('assertThatJson(parsedJson).field("hostname").matches("((http[s]?|ftp):/)/?([^:/\\\\s]+)(:[0-9]{1,5})?")') + test.contains('assertThatJson(parsedJson).field("url").matches("^(?:(?:[A-Za-z][+-.\\\\w^_]*:/{2})?(?:\\\\S+(?::\\\\S*)?@)?(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,})))(?::\\\\d{2,5})?(?:/\\\\S*)?)') test.contains('assertThatJson(parsedJson).field("number").matches("-?\\\\d*(\\\\.\\\\d+)?")') test.contains('assertThatJson(parsedJson).field("email").matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,4}")') test.contains('assertThatJson(parsedJson).field("ip").matches("([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])")') @@ -1954,8 +1954,19 @@ World.'''""" test.contains('assertThatJson(parsedJson).field("nonEmptyString").matches(".+")') test.contains('assertThatJson(parsedJson).field("anyOf").matches("^foo' + endOfLineRegExSymbol + '|^bar' + endOfLineRegExSymbol + '")') !test.contains('cursor') + !test.contains('REGEXP>>') and: SyntaxChecker.tryToCompile(methodBuilderName, blockBuilder.toString()) + and: + String jsonSample = '''\ +String json = "{\\"duck\\":\\"8\\",\\"alpha\\":\\"YAJEOWYGMFBEWPMEMAZI\\",\\"number\\":-2095030871,\\"aBoolean\\":true,\\"ip\\":\\"129.168.99.100\\",\\"hostname\\":\\"http://foo389886219.com\\",\\"email\\":\\"foo@bar1367573183.com\\",\\"url\\":\\"http://foo-597104692.com\\",\\"uuid\\":\\"e436b817-b764-49a2-908e-967f2f99eb9f\\",\\"date\\":\\"2014-04-14\\",\\"dateTime\\":\\"2011-01-11T12:23:34\\",\\"time\\":\\"12:20:30\\",\\"iso8601WithOffset\\":\\"2015-05-15T12:23:34.123Z\\",\\"nonBlankString\\":\\"EPZWVIRHSUAPBJMMQSFO\\",\\"nonEmptyString\\":\\"RVMFDSEQFHRQFVUVQPIA\\",\\"anyOf\\":\\"foo\\"}"; +DocumentContext parsedJson = JsonPath.parse(json); +''' + and: + LinkedList lines = [] as LinkedList + test.eachLine { if (it.contains("assertThatJson")) lines << it else it } + lines.addFirst(jsonSample) + SyntaxChecker.tryToRun(methodBuilderName, lines.join("\n")) where: methodBuilderName | methodBuilder | endOfLineRegExSymbol "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$' diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy index 82a63f0ba4..962626236f 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverterSpec.groovy @@ -763,6 +763,14 @@ class JsonToJsonPathsConverterSpec extends Specification { '$.a.b.c[?(@.d =~ /(.*)/)]' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.REGEX, jsonPath, regexPattern)) } + def "should convert a json path with regex to a regex checking json path that has a / in it"() { + given: + String jsonPath = '$.a.b.c.d' + String regexPattern = "/.*/" + expect: + '$.a.b.c[?(@.d =~ /(\\\\/.*\\\\/)/)]' == JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher(MatchingType.REGEX, jsonPath, regexPattern)) + } + def "should convert a json path with value to a equality checking json path without quotes for numbers"() { given: String jsonPath = '$.a.b.c.d' diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy index 8a71f78cf1..7d74ed37e8 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/SyntaxChecker.groovy @@ -5,6 +5,9 @@ import org.codehaus.groovy.control.CompilerConfiguration import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer import org.codehaus.groovy.control.customizers.ImportCustomizer import org.mdkt.compiler.InMemoryJavaCompiler +import org.springframework.util.ReflectionUtils + +import java.lang.reflect.Method /** * checking the syntax of produced scripts @@ -51,6 +54,17 @@ class SyntaxChecker { } } + static void tryToRun(String builderName, String test) { + if (builderName.toLowerCase().contains("spock")) { + Script script = tryToCompileGroovy(test) + script.run() + } else { + Class clazz = tryToCompileJava(test) + Method method = ReflectionUtils.findMethod(clazz, "method") + method.invoke(clazz.newInstance()) + } + } + // no static compilation due to bug in Groovy https://issues.apache.org/jira/browse/GROOVY-8055 static void tryToCompileWithoutCompileStatic(String builderName, String test) { if (builderName.toLowerCase().contains("spock")) { @@ -60,7 +74,7 @@ class SyntaxChecker { } } - static void tryToCompileGroovy(String test, boolean compileStatic = true) { + static Script tryToCompileGroovy(String test, boolean compileStatic = true) { def imports = new ImportCustomizer() CompilerConfiguration configuration = new CompilerConfiguration() if (compileStatic) { @@ -75,7 +89,7 @@ class SyntaxChecker { sourceCode.append("WebTarget webTarget") sourceCode.append("\n") sourceCode.append(test) - new GroovyShell(SyntaxChecker.classLoader, configuration).parse(sourceCode.toString()) + return new GroovyShell(SyntaxChecker.classLoader, configuration).parse(sourceCode.toString()) } static Class tryToCompileJava(String test) { @@ -89,11 +103,11 @@ class SyntaxChecker { sourceCode.append("${DEFAULT_IMPORTS_AS_STRING}\n") sourceCode.append("${STATIC_IMPORTS}\n") sourceCode.append("\n") - sourceCode.append("class ${className} {\n") + sourceCode.append("public class ${className} {\n") sourceCode.append("\n") sourceCode.append(" WebTarget webTarget;") sourceCode.append("\n") - sourceCode.append(" void method() {\n") + sourceCode.append(" public void method() {\n") sourceCode.append(" ${test}\n") sourceCode.append(" }\n") sourceCode.append("}")