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 3948d3d4a5..d8054ca59d 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 @@ -48,7 +48,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 81026683d8..b17f1e928c 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml @@ -57,6 +57,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 064abbdac2..7e9daab7b2 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,14 +16,19 @@ 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.spec.Contract 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 @@ -31,8 +36,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: @@ -57,7 +69,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) } def "should convert DSL file with list of contracts to WireMock JSONs"() { @@ -143,7 +159,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"() { @@ -158,7 +178,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 ''' [{ @@ -249,10 +268,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: @@ -287,7 +339,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) } @@ -319,7 +377,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'() { @@ -386,7 +449,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'() { @@ -397,7 +470,7 @@ class DslToWireMockClientConverterSpec extends Specification { file.write(''' org.springframework.cloud.contract.spec.Contract.make { request { - method 'GET' + method 'POST' urlPath '/get' body([ duck: 123, @@ -505,7 +578,7 @@ class DslToWireMockClientConverterSpec extends Specification { { "request" : { "urlPath" : "/get", - "method" : "GET", + "method" : "POST", "headers" : { "Content-Type" : { "matches" : "application/json.*" @@ -555,7 +628,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'() { @@ -568,7 +694,7 @@ class DslToWireMockClientConverterSpec extends Specification { priority 1 request { method 'POST' - url '/users/password' + url '/users/password2' headers { header 'Content-Type': 'application/json' } @@ -604,7 +730,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})/)]" @@ -629,15 +755,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 6b220f3b70..abc3598f63 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 @@ -136,7 +136,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/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 346e8cc5c1..d90d9e95c2 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 0d88f81f22..ec78f42b53 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 @@ -1879,7 +1879,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 { @@ -1939,8 +1939,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])")') @@ -1953,8 +1953,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 69a63d006d..97e56b7e81 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("}")