From b1175ff9bd37f77175e15c3eddd7e0541974cd52 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 18 Nov 2016 15:38:35 +0100 Subject: [PATCH] Simplification of the dsl (#151) with this change to DSL we're adding some helper methods and DSL simplifications - c(...) / p(...) - for consumer / producer - for regex - e.g. $(anyUrl()) - insead of $(consumer(regex(url())) - Added helper headers and media types - Added helper http methods fixes #149 --- .../spec/internal/ClientDslProperty.groovy | 4 + .../contract/spec/internal/Common.groovy | 11 + .../contract/spec/internal/DslProperty.groovy | 6 + .../contract/spec/internal/Headers.groovy | 21 + .../contract/spec/internal/HttpHeaders.groovy | 480 ++++++++++++++++++ .../contract/spec/internal/HttpMethods.groovy | 53 ++ .../contract/spec/internal/MediaTypes.groovy | 85 ++++ .../internal/PatternValueDslProperty.groovy | 82 +++ .../spec/internal/RegexPatterns.groovy | 17 +- .../contract/spec/internal/Request.groovy | 72 ++- .../contract/spec/internal/Response.groovy | 68 ++- .../spec/internal/ServerDslProperty.groovy | 4 + .../contract/spec/util/RegexpUtils.groovy | 41 ++ .../repository/contracts/contract1.groovy | 6 +- .../verifier/util/RegexpBuilders.groovy | 5 +- .../builder/ContractHttpDocsSpec.groovy | 11 +- .../JaxRsClientMethodBuilderSpec.groovy | 147 +++++- .../MockMvcMethodBodyBuilderSpec.groovy | 145 ++++-- .../test/resources/dsl/basic/sampleDsl.groovy | 4 +- .../CamelMessagingApplicationSpec.groovy | 2 +- 20 files changed, 1164 insertions(+), 100 deletions(-) create mode 100644 spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HttpHeaders.groovy create mode 100644 spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HttpMethods.groovy create mode 100644 spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MediaTypes.groovy create mode 100644 spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PatternValueDslProperty.groovy create mode 100644 spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/util/RegexpUtils.groovy diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ClientDslProperty.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ClientDslProperty.groovy index e16afaffb5..b9f1906ab9 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ClientDslProperty.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ClientDslProperty.groovy @@ -29,4 +29,8 @@ class ClientDslProperty extends DslProperty { ClientDslProperty(Object singleValue) { super(singleValue) } + + ClientDslProperty(Object client, Object server) { + super(client, server) + } } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy index c8704b79b0..398fb4c8e2 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy @@ -144,6 +144,17 @@ class Common { return new ServerDslProperty(serverValue) } + /** + * Helper method to provide a better name for the consumer side + */ + ClientDslProperty c(Object clientValue) { + return new ClientDslProperty(clientValue) + } + + ServerDslProperty p(Object serverValue) { + return new ServerDslProperty(serverValue) + } + /** * Helper method to provide a better name for the producer side */ diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/DslProperty.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/DslProperty.groovy index 4be6c853ae..ef3b9758fa 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/DslProperty.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/DslProperty.groovy @@ -42,4 +42,10 @@ class DslProperty { this.clientValue = singleValue this.serverValue = singleValue } + + boolean isSingleValue() { + return this.clientValue == this.serverValue || + (this.clientValue != null && this.serverValue == null ) || + (this.serverValue != null && this.clientValue == null ) + } } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy index 036a23598e..cb8ab772b8 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy @@ -30,6 +30,9 @@ import groovy.transform.TypeChecked @TypeChecked class Headers { + @Delegate MediaTypes mediaTypes = new MediaTypes() + @Delegate HttpHeaders httpHeaders = new HttpHeaders() + Set
entries = [] void header(Map singleHeader) { @@ -47,6 +50,24 @@ class Headers { } } + void accept(String contentType) { + header(accept(), matching(contentType)) + } + + void contentType(String contentType) { + header(httpHeaders.contentType(), matching(contentType)) + } + + /** + * If for the consumer / producer you want to match exactly only + * the root of content type. I.e. {@code application/json;charset=UTF8} + * you care only about {@code application/json} then you should + * use this method + */ + DslProperty matching(String value) { + return new DslProperty(value) + } + /** * Converts the headers into their stub side representations and returns as * a map of String key => Object value. diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HttpHeaders.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HttpHeaders.groovy new file mode 100644 index 0000000000..dfed48b49d --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HttpHeaders.groovy @@ -0,0 +1,480 @@ +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Contains most commonly used http headers + * + * @author Marcin Grzejszczak + * @since 1.0.2 + */ +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class HttpHeaders { + + /** + * The HTTP {@code Accept} header field name. + * @see Section 5.3.2 of RFC 7231 + */ + String accept() { + return "Accept" + } + /** + * The HTTP {@code Accept-Charset} header field name. + * @see Section 5.3.3 of RFC 7231 + */ + String acceptCharset() { + return "Accept-Charset" + } + /** + * The HTTP {@code Accept-Encoding} header field name. + * @see Section 5.3.4 of RFC 7231 + */ + String acceptEncoding() { + return "Accept-Encoding" + } + /** + * The HTTP {@code Accept-Language} header field name. + * @see Section 5.3.5 of RFC 7231 + */ + String acceptLanguage() { + return "Accept-Language" + } + /** + * The HTTP {@code Accept-Ranges} header field name. + * @see Section 5.3.5 of RFC 7233 + */ + String acceptRanges() { + return "Accept-Ranges" + } + /** + * The CORS {@code Access-Control-Allow-Credentials} response header field name. + * @see CORS W3C recommendation + */ + String accessControlAllowCredentials() { + return "Access-Control-Allow-Credentials" + } + /** + * The CORS {@code Access-Control-Allow-Headers} response header field name. + * @see CORS W3C recommendation + */ + String accessControlAllowHeaders() { + return "Access-Control-Allow-Headers" + } + /** + * The CORS {@code Access-Control-Allow-Methods} response header field name. + * @see CORS W3C recommendation + */ + String accessControlAllowMethods() { + return "Access-Control-Allow-Methods" + } + /** + * The CORS {@code Access-Control-Allow-Origin} response header field name. + * @see CORS W3C recommendation + */ + String accessControlAllowOrigin() { + return "Access-Control-Allow-Origin" + } + /** + * The CORS {@code Access-Control-Expose-Headers} response header field name. + * @see CORS W3C recommendation + */ + String accessControlExposeHeaders() { + return "Access-Control-Expose-Headers" + } + /** + * The CORS {@code Access-Control-Max-Age} response header field name. + * @see CORS W3C recommendation + */ + String accessControlMaxAge() { + return "Access-Control-Max-Age" + } + /** + * The CORS {@code Access-Control-Request-Headers} request header field name. + * @see CORS W3C recommendation + */ + String accessControlRequestHeaders() { + return "Access-Control-Request-Headers" + } + /** + * The CORS {@code Access-Control-Request-Method} request header field name. + * @see CORS W3C recommendation + */ + String accessControlRequestMethod() { + return "Access-Control-Request-Method" + } + /** + * The HTTP {@code Age} header field name. + * @see Section 5.1 of RFC 7234 + */ + String age() { + return "Age" + } + /** + * The HTTP {@code Allow} header field name. + * @see Section 7.4.1 of RFC 7231 + */ + String allow() { + return "Allow" + } + /** + * The HTTP {@code Authorization} header field name. + * @see Section 4.2 of RFC 7235 + */ + String authorization() { + return "Authorization" + } + /** + * The HTTP {@code Cache-Control} header field name. + * @see Section 5.2 of RFC 7234 + */ + String cacheControl() { + return "Cache-Control" + } + + /** + * The HTTP {@code Connection} header field name. + * @see Section 6.1 of RFC 7230 + */ + String connection() { + return "Connection" + } + + /** + * The HTTP {@code Content-Encoding} header field name. + * @see Section 3.1.2.2 of RFC 7231 + */ + String contentEncoding() { + return "Content-Encoding" + } + + /** + * The HTTP {@code Content-Disposition} header field name + * @see RFC 6266 + */ + String contentDisposition() { + return "Content-Disposition" + } + + /** + * The HTTP {@code Content-Language} header field name. + * @see Section 3.1.3.2 of RFC 7231 + */ + String contentLanguage() { + return "Content-Language" + } + + /** + * The HTTP {@code Content-Length} header field name. + * @see Section 3.3.2 of RFC 7230 + */ + String contentLength() { + return "Content-Length" + } + + /** + * The HTTP {@code Content-Location} header field name. + * @see Section 3.1.4.2 of RFC 7231 + */ + String contentLocation() { + return "Content-Location" + } + + /** + * The HTTP {@code Content-Range} header field name. + * @see Section 4.2 of RFC 7233 + */ + String contentRange() { + return "Content-Range" + } + + /** + * The HTTP {@code Content-Type} header field name. + * @see Section 3.1.1.5 of RFC 7231 + */ + String contentType() { + return "Content-Type" + } + + /** + * The HTTP {@code Cookie} header field name. + * @see Section 4.3.4 of RFC 2109 + */ + String cookie() { + return "Cookie" + } + + /** + * The HTTP {@code Date} header field name. + * @see Section 7.1.1.2 of RFC 7231 + */ + String date() { + return "Date" + } + + /** + * The HTTP {@code ETag} header field name. + * @see Section 2.3 of RFC 7232 + */ + String etag() { + return "ETag" + } + + /** + * The HTTP {@code Expect} header field name. + * @see Section 5.1.1 of RFC 7231 + */ + String expect() { + return "Expect" + } + + /** + * The HTTP {@code Expires} header field name. + * @see Section 5.3 of RFC 7234 + */ + String expires() { + return "Expires" + } + + /** + * The HTTP {@code From} header field name. + * @see Section 5.5.1 of RFC 7231 + */ + String from() { + return "From" + } + + /** + * The HTTP {@code Host} header field name. + * @see Section 5.4 of RFC 7230 + */ + String host() { + return "Host" + } + + /** + * The HTTP {@code If-Match} header field name. + * @see Section 3.1 of RFC 7232 + */ + String ifMatch() { + return "If-Match" + } + + /** + * The HTTP {@code If-Modified-Since} header field name. + * @see Section 3.3 of RFC 7232 + */ + String ifModifiedSince() { + return "If-Modified-Since" + } + + /** + * The HTTP {@code If-None-Match} header field name. + * @see Section 3.2 of RFC 7232 + */ + String ifNoneMatch() { + return "If-None-Match" + } + + /** + * The HTTP {@code If-Range} header field name. + * @see Section 3.2 of RFC 7233 + */ + String ifRange() { + return "If-Range" + } + + /** + * The HTTP {@code If-Unmodified-Since} header field name. + * @see Section 3.4 of RFC 7232 + */ + String ifUnmodifiedSince() { + return "If-Unmodified-Since" + } + + /** + * The HTTP {@code Last-Modified} header field name. + * @see Section 2.2 of RFC 7232 + */ + String lastModified() { + return "Last-Modified" + } + + /** + * The HTTP {@code Link} header field name. + * @see RFC 5988 + */ + String link() { + return "Link" + } + + /** + * The HTTP {@code Location} header field name. + * @see Section 7.1.2 of RFC 7231 + */ + String location() { + return "Location" + } + + /** + * The HTTP {@code Max-Forwards} header field name. + * @see Section 5.1.2 of RFC 7231 + */ + String max_forwards() { + return "Max-Forwards" + } + /** + * The HTTP {@code Origin} header field name. + * @see RFC 6454 + */ + String origin() { + return "Origin" + } + + /** + * The HTTP {@code Pragma} header field name. + * @see Section 5.4 of RFC 7234 + */ + String pragma() { + return "Pragma" + } + + /** + * The HTTP {@code Proxy-Authenticate} header field name. + * @see Section 4.3 of RFC 7235 + */ + String proxyAuthenticate() { + return "Proxy-Authenticate" + } + + /** + * The HTTP {@code Proxy-Authorization} header field name. + * @see Section 4.4 of RFC 7235 + */ + String proxyAuthorization() { + return "Proxy-Authorization" + } + + /** + * The HTTP {@code Range} header field name. + * @see Section 3.1 of RFC 7233 + */ + String range() { + return "Range" + } + + /** + * The HTTP {@code Referer} header field name. + * @see Section 5.5.2 of RFC 7231 + */ + String referer() { + return "Referer" + } + + /** + * The HTTP {@code Retry-After} header field name. + * @see Section 7.1.3 of RFC 7231 + */ + String retryAfter() { + return "Retry-After" + } + + /** + * The HTTP {@code Server} header field name. + * @see Section 7.4.2 of RFC 7231 + */ + String server() { + return "Server" + } + + /** + * The HTTP {@code Set-Cookie} header field name. + * @see Section 4.2.2 of RFC 2109 + */ + String setCookie() { + return "Set-Cookie" + } + + /** + * The HTTP {@code Set-Cookie2} header field name. + * @see RFC 2965 + */ + String setCookie2() { + return "Set-Cookie2" + } + + /** + * The HTTP {@code TE} header field name. + * @see Section 4.3 of RFC 7230 + */ + String te() { + return "TE" + } + + /** + * The HTTP {@code Trailer} header field name. + * @see Section 4.4 of RFC 7230 + */ + String trailer() { + return "Trailer" + } + + /** + * The HTTP {@code Transfer-Encoding} header field name. + * @see Section 3.3.1 of RFC 7230 + */ + String transferEncoding() { + return "Transfer-Encoding" + } + + /** + * The HTTP {@code Upgrade} header field name. + * @see Section 6.7 of RFC 7230 + */ + String upgrade() { + return "Upgrade" + } + + /** + * The HTTP {@code User-Agent} header field name. + * @see Section 5.5.3 of RFC 7231 + */ + String user_agent() { + return "User-Agent" + } + + /** + * The HTTP {@code Vary} header field name. + * @see Section 7.1.4 of RFC 7231 + */ + String vary() { + return "Vary" + } + + /** + * The HTTP {@code Via} header field name. + * @see Section 5.7.1 of RFC 7230 + */ + String via() { + return "Via" + } + + /** + * The HTTP {@code Warning} header field name. + * @see Section 5.5 of RFC 7234 + */ + String warning() { + return "Warning" + } + + /** + * The HTTP {@code WWW-Authenticate} header field name. + * @see Section 4.1 of RFC 7235 + */ + String wwwAuthenticate() { + return "WWW-Authenticate" + } +} diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HttpMethods.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HttpMethods.groovy new file mode 100644 index 0000000000..c2316c4bdc --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HttpMethods.groovy @@ -0,0 +1,53 @@ +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Contains Http Methods + * + * @author Marcin Grzejszczak + * @since 1.0.2 + */ +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class HttpMethods { + + HttpMethod GET() { + return HttpMethod.GET + } + + HttpMethod HEAD() { + return HttpMethod.HEAD + } + + HttpMethod POST() { + return HttpMethod.POST + } + + HttpMethod PUT() { + return HttpMethod.PUT + } + + HttpMethod PATCH() { + return HttpMethod.PATCH + } + + HttpMethod DELETE() { + return HttpMethod.DELETE + } + + HttpMethod OPTIONS() { + return HttpMethod.OPTIONS + } + + HttpMethod TRACE() { + return HttpMethod.TRACE + } + + enum HttpMethod { + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE + } +} diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MediaTypes.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MediaTypes.groovy new file mode 100644 index 0000000000..8f10f64ce3 --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/MediaTypes.groovy @@ -0,0 +1,85 @@ +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Contains most commonly used media types + * + * @author Marcin Grzejszczak + * @since 1.0.2 + */ +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false) +class MediaTypes { + + String allValue() { + return "*/*" + } + + String applicationAtomXml() { + return "application/atom+xml" + } + + String applicationFormUrlencoded() { + return "application/x-www-form-urlencoded" + } + + String applicationJson() { + return "application/json" + } + + String applicationJsonUtf8() { + return applicationJson() + ";charset=UTF-8" + } + + String applicationOctetStream() { + return "application/octet-stream" + } + + String applicationPdf() { + return "application/pdf" + } + + String applicationXhtmlXml() { + return "application/xhtml+xml" + } + + String applicationXml() { + return "application/xml" + } + + String imageGif() { + return "image/gif" + } + + String imageJpeg() { + return "image/jpeg" + } + + String imagePng() { + return "image/png" + } + + String multipartFormData() { + return "multipart/form-data" + } + + String textHtml() { + return "text/html" + } + + String textMarkdown() { + return "text/markdown" + } + + String textPlain() { + return "text/plain" + } + + String textXml() { + return "text/xml" + } +} 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 new file mode 100644 index 0000000000..1338b2f69a --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/PatternValueDslProperty.groovy @@ -0,0 +1,82 @@ +package org.springframework.cloud.contract.spec.internal + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope + +import java.util.regex.Pattern + +/** + * @author Marcin Grzejszczak + */ +@PackageScope +@CompileStatic +abstract class PatternValueDslProperty { + + private final Random random = new Random() + + protected T createAndValidateProperty(Pattern pattern, Object object = null) { + if (object) { + String generatedValue = object as String + boolean matches = pattern.matcher(generatedValue).matches() + if (!matches) { + throw new IllegalStateException("The generated value [${generatedValue}] doesn't match the pattern [${pattern.pattern()}]") + } + return createProperty(pattern, generatedValue) + } + return createProperty(pattern, object) + } + + /** + * Method to generate the PatternValue. The resulting implementation + * will create either a Client or a Server side impl. + * + * @param pattern - pattern for which the value will be generated or reused + * @param generatedValue - Nullable - potential generated value to be reused + * @return {@link DslProperty} wrapping a pattern and generated value + */ + protected abstract T createProperty(Pattern pattern, Object generatedValue) + + T anyAlphaUnicode() { + return createAndValidateProperty(RegexPatterns.ONLY_ALPHA_UNICODE, randomString(20)) + } + + T anyNumber() { + return createAndValidateProperty(RegexPatterns.NUMBER, this.random.nextInt()) + } + + T aBoolean() { + return createAndValidateProperty(RegexPatterns.TRUE_OR_FALSE) + } + + T anyIpAddress() { + return createAndValidateProperty(RegexPatterns.IP_ADDRESS) + } + + T anyHostname() { + return createAndValidateProperty(RegexPatterns.HOSTNAME_PATTERN, "http://foo" + this.random.nextInt() + ".com") + } + + T anyEmail() { + return createAndValidateProperty(RegexPatterns.EMAIL, "foo@bar" + this.random.nextInt() + ".com") + } + + T anyUrl() { + return createAndValidateProperty(RegexPatterns.URL, "http://foo" + this.random.nextInt() + ".com") + } + + T anyUuid(){ + return createAndValidateProperty(RegexPatterns.UUID, UUID.randomUUID().toString()) + } + + private static String randomString(int length) { + char[] characterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray() + Random random = new Random() + char[] result = new char[length] + for (int i = 0; i < result.length; i++) { + // picks a random index out of character set > random character + int randomCharIndex = random.nextInt(characterSet.length) + result[i] = characterSet[randomCharIndex] + } + return new String(result) + } +} 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 f2225db4f1..1cf37a9d7b 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 @@ -19,7 +19,6 @@ package org.springframework.cloud.contract.spec.internal import groovy.transform.CompileStatic import java.util.regex.Pattern - /** * Contains most common regular expression patterns * @@ -29,14 +28,14 @@ import java.util.regex.Pattern class RegexPatterns { // tag::regexps[] - private static final Pattern TRUE_OR_FALSE = Pattern.compile(/(true|false)/) - private static final Pattern ONLY_ALPHA_UNICODE = Pattern.compile(/[\p{L}]*/) - private static final Pattern NUMBER = Pattern.compile('-?\\d*(\\.\\d+)?') - private 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])'); - private static final Pattern HOSTNAME_PATTERN = Pattern.compile('((http[s]?|ftp):\\/)\\/?([^:\\/\\s]+)(:[0-9]{1,5})?'); - private static final Pattern EMAIL = Pattern.compile('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}'); - private static final Pattern URL = Pattern.compile('((www\\.|(http|https|ftp|news|file)+\\:\\/\\/)[_.a-z0-9-]+\\.[a-z0-9\\/_:@=.+?,##%&~-]*[^.|\\\'|\\# |!|\\(|?|,| |>|<|;|\\)])') - private 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 TRUE_OR_FALSE = Pattern.compile(/(true|false)/) + 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 UUID = Pattern.compile('[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}') String onlyAlphaUnicode() { return ONLY_ALPHA_UNICODE.pattern() diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy index c7f3321c01..372728945e 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Request.groovy @@ -20,6 +20,7 @@ import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.util.RegexpUtils import repackaged.nl.flotsam.xeger.Xeger import java.util.regex.Pattern @@ -33,10 +34,13 @@ import java.util.regex.Pattern @ToString(includePackage = false, includeNames = true) class Request extends Common { + @Delegate ClientPatternValueDslProperty property = new ClientPatternValueDslProperty() + @Delegate HttpMethods httpMethods = new HttpMethods() + DslProperty method Url url UrlPath urlPath - Headers headers + RequestHeaders headers Body body Multipart multipart @@ -56,6 +60,10 @@ class Request extends Common { this.method = toDslProperty(method) } + void method(HttpMethods.HttpMethod httpMethod) { + this.method = toDslProperty(httpMethod.toString()) + } + void method(DslProperty method) { this.method = toDslProperty(method) } @@ -100,8 +108,8 @@ class Request extends Common { closure() } - void headers(@DelegatesTo(Headers) Closure closure) { - this.headers = new Headers() + void headers(@DelegatesTo(RequestHeaders) Closure closure) { + this.headers = new RequestHeaders() closure.delegate = headers closure() } @@ -176,8 +184,10 @@ class Request extends Common { DslProperty value(ClientDslProperty client) { Object clientValue = client.clientValue - if (client.clientValue instanceof Pattern) { + if (client.clientValue instanceof Pattern && client.isSingleValue()) { clientValue = new Xeger(((Pattern)client.clientValue).pattern()).generate() + } else if (client.clientValue instanceof Pattern && !client.isSingleValue()) { + clientValue = client.serverValue } return new DslProperty(client.clientValue, clientValue) } @@ -202,22 +212,44 @@ class Request extends Common { return super.value(server, client) } -} - -@CompileStatic -@EqualsAndHashCode -@ToString(includePackage = false) -class ServerRequest extends Request { - ServerRequest(Request request) { - super(request) + @CompileStatic + @EqualsAndHashCode + @ToString(includePackage = false) + private class ServerRequest extends Request { + ServerRequest(Request request) { + super(request) + } } -} -@CompileStatic -@EqualsAndHashCode -@ToString(includePackage = false) -class ClientRequest extends Request { - ClientRequest(Request request) { - super(request) + @CompileStatic + @EqualsAndHashCode + @ToString(includePackage = false) + private class ClientRequest extends Request { + ClientRequest(Request request) { + super(request) + } } -} + + @CompileStatic + @EqualsAndHashCode + @ToString(includePackage = false) + private class RequestHeaders extends Headers { + + @Override + DslProperty matching(String value) { + return $(c(regex("${RegexpUtils.escapeSpecialRegexWithSingleEscape(value)}.*")), + p(value)) + } + } + + @CompileStatic + @EqualsAndHashCode + @ToString(includePackage = false) + private class ClientPatternValueDslProperty extends PatternValueDslProperty { + + @Override + protected ClientDslProperty createProperty(Pattern pattern, Object generatedValue) { + return new ClientDslProperty(pattern, generatedValue) + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy index 32ce645245..f2d1846a98 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy @@ -20,10 +20,10 @@ import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import groovy.transform.TypeChecked +import org.springframework.cloud.contract.spec.util.RegexpUtils import repackaged.nl.flotsam.xeger.Xeger import java.util.regex.Pattern - /** * Represents the response side of the HTTP communication * @@ -34,9 +34,11 @@ import java.util.regex.Pattern @ToString(includePackage = false, includeFields = true) class Response extends Common { + @Delegate ServerPatternValueDslProperty property = new ServerPatternValueDslProperty() + DslProperty status DslProperty delay - Headers headers + ResponseHeaders headers Body body boolean async @@ -57,8 +59,8 @@ class Response extends Common { this.status = toDslProperty(status) } - void headers(@DelegatesTo(Headers) Closure closure) { - this.headers = new Headers() + void headers(@DelegatesTo(ResponseHeaders) Closure closure) { + this.headers = new ResponseHeaders() closure.delegate = headers closure() } @@ -89,8 +91,10 @@ class Response extends Common { DslProperty value(ServerDslProperty server) { Object value = server.clientValue - if (server.clientValue instanceof Pattern) { + if (server.clientValue instanceof Pattern && server.isSingleValue()) { value = new Xeger(((Pattern)server.clientValue).pattern()).generate() + } else if (server.clientValue instanceof Pattern && !server.isSingleValue()) { + value = server.serverValue } return new DslProperty(value, server.serverValue) } @@ -114,22 +118,46 @@ class Response extends Common { } return super.value(server, client) } -} -@CompileStatic -@EqualsAndHashCode -@ToString(includePackage = false) -class ServerResponse extends Response { - ServerResponse(Response request) { - super(request) + @CompileStatic + @EqualsAndHashCode + @ToString(includePackage = false) + private class ServerResponse extends Response { + ServerResponse(Response request) { + super(request) + } + } + + @CompileStatic + @EqualsAndHashCode + @ToString(includePackage = false) + private class ClientResponse extends Response { + ClientResponse(Response request) { + super(request) + } + } + + @CompileStatic + @EqualsAndHashCode + @ToString(includePackage = false) + private class ResponseHeaders extends Headers { + + @Override + DslProperty matching(String value) { + return $(p(Pattern.compile("${RegexpUtils.escapeSpecialRegexWithSingleEscape(value)}.*")), + c(value)) + } + } + + @CompileStatic + @EqualsAndHashCode + @ToString(includePackage = false) + private class ServerPatternValueDslProperty extends PatternValueDslProperty { + + @Override + protected ServerDslProperty createProperty(Pattern pattern, Object generatedValue) { + return new ServerDslProperty(pattern, generatedValue) + } } } -@CompileStatic -@EqualsAndHashCode -@ToString(includePackage = false) -class ClientResponse extends Response { - ClientResponse(Response request) { - super(request) - } -} diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ServerDslProperty.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ServerDslProperty.groovy index 3cf7ec93cc..5efbe09f8b 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ServerDslProperty.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ServerDslProperty.groovy @@ -33,4 +33,8 @@ class ServerDslProperty extends DslProperty { ServerDslProperty(Object singleValue) { super(singleValue) } + + ServerDslProperty(Object server, Object client) { + super(client, server) + } } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/util/RegexpUtils.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/util/RegexpUtils.groovy new file mode 100644 index 0000000000..da5a711ad8 --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/util/RegexpUtils.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.spec.util + +import groovy.transform.CompileStatic + +import java.util.regex.Pattern + +/** + * Useful utility methods to work with regular expresisons + * + * @since 1.0.2 + */ +@CompileStatic +class RegexpUtils { + + private final static Pattern SPECIAL_REGEX_CHARS = Pattern.compile('[{}()\\[\\].+*?^$\\\\|]') + + static String escapeSpecialRegexChars(String str) { + return SPECIAL_REGEX_CHARS.matcher(str).replaceAll('\\\\\\\\$0') + } + + static String escapeSpecialRegexWithSingleEscape(String str) { + return SPECIAL_REGEX_CHARS.matcher(str).replaceAll('\\\\$0') + } + +} diff --git a/spring-cloud-contract-stub-runner/src/test/resources/repository/contracts/contract1.groovy b/spring-cloud-contract-stub-runner/src/test/resources/repository/contracts/contract1.groovy index c47a0a6fc8..80e123ae4d 100644 --- a/spring-cloud-contract-stub-runner/src/test/resources/repository/contracts/contract1.groovy +++ b/spring-cloud-contract-stub-runner/src/test/resources/repository/contracts/contract1.groovy @@ -9,18 +9,18 @@ org.springframework.cloud.contract.spec.Contract.make { """ ) headers { - header("""Content-Type""", """application/vnd.fraud.v1+json""") + contentType("application/vnd.fraud.v1+json") } } response { status 200 body( """{ - "fraudCheckStatus": "${value(consumer('FRAUD'), producer(regex('[A-Z]{5}')))}", + "fraudCheckStatus": "${value(c('FRAUD'), p(regex('[A-Z]{5}')))}", "rejectionReason": "Amount too high" }""") headers { - header('Content-Type': value(producer(regex('application/vnd.fraud.v1.json.*')), consumer('application/vnd.fraud.v1+json'))) + contentType("application/vnd.fraud.v1+json") } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy index 8e8fc9108f..a07700d914 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy @@ -19,6 +19,7 @@ package org.springframework.cloud.contract.verifier.util import groovy.transform.TypeChecked import org.codehaus.groovy.runtime.GStringImpl import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.spec.util.RegexpUtils import java.util.regex.Pattern @@ -97,10 +98,8 @@ class RegexpBuilders { return o.toString().replaceAll('\\\\', '\\\\\\\\') } - private final static Pattern SPECIAL_REGEX_CHARS = Pattern.compile('[{}()\\[\\].+*?^$\\\\|]') - static String escapeSpecialRegexChars(String str) { - return SPECIAL_REGEX_CHARS.matcher(str).replaceAll('\\\\\\\\$0') + return RegexpUtils.escapeSpecialRegexChars(str) } private final static String WS = /\s*/ diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy index d76f0152e8..590aacb482 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy @@ -138,8 +138,10 @@ class ContractHttpDocsSpec extends Specification { //... // Each header is added in form `'Header-Name' : 'Header-Value'`. + // there are also some helper methods headers { - header 'Content-Type': 'application/json' + header 'key': 'value' + contentType(applicationJson()) } //... @@ -210,10 +212,7 @@ class ContractHttpDocsSpec extends Specification { response { status 200 body( - id: value( - consumer('123'), - producer(regex('[0-9]+')) - ), + id: $(anyNumber()), surname: $( consumer('Kowalsky'), producer(regex('[a-zA-Z]+')) @@ -239,7 +238,7 @@ class ContractHttpDocsSpec extends Specification { method 'POST' url '/users/password' headers { - header 'Content-Type': 'application/json' + contentType(applicationJson()) } body( email: $(consumer(optional(regex(email()))), producer('abc@abc.com')), 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 c855907ab5..ed65535af0 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 @@ -339,7 +339,6 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub ) headers { header('Content-Type': 'application/json') - } } @@ -371,9 +370,7 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub body("""{"property1":"a","property2":"${value(consumer('123'), producer(regex('[0-9]{3}')))}"}""") headers { header('Content-Type': 'application/json') - } - } } MethodBodyBuilder builder = methodBuilder(contractDsl) @@ -628,7 +625,7 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub response { status 200 headers { - header('Content-Type': 'application/json;charset=UTF-8') + contentType(applicationJson()) } body """ {"id":"789fgh","other_data":1268} @@ -766,6 +763,29 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub test.contains("assertThat(responseBody).matches(\".*\");") } + @Issue('#150') + def "should support body matching in response in Spock"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + url '/get' + } + response { + status 200 + status 200 + body(value(stub("HELLO FROM STUB"), server(regex(".*")))) + } + } + MethodBodyBuilder builder = new JaxRsClientSpockMethodRequestProcessingBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.then(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains("responseBody ==~ java.util.regex.Pattern.compile('.*')") + } + @Issue('#150') def "should support custom method execution in response"() { given: @@ -789,6 +809,125 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub test.contains("foo(responseBody);") } + @Issue('#150') + def "should support custom method execution in response in Spock"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + url '/get' + } + response { + status 200 + status 200 + body(value(stub("HELLO FROM STUB"), server(execute('foo($it)')))) + } + } + MethodBodyBuilder builder = new JaxRsClientSpockMethodRequestProcessingBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.then(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains("foo(responseBody)") + } + + def "should allow c/p version of consumer producer"() { + given: + Contract contractDsl = Contract.make { + request { + method "GET" + url "test" + } + response { + status 200 + body( + property1: "a", + property2: $( + c('123'), + p(regex('[0-9]{3}')) + ) + ) + headers { + header('Content-Type': 'application/json') + } + + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property2").matches("[0-9]{3}")""") + blockBuilder.toString().contains("""assertThatJson(parsedJson).field("property1").isEqualTo("a")""") + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } + } + + @Issue('#149') + def "should allow easier way of providing dynamic values"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/get' + body([ + alpha: $(anyAlphaUnicode()), + number: $(anyNumber()), + aBoolean: $(aBoolean()), + ip: $(anyIpAddress()), + hostname: $(anyHostname()), + email: $(anyEmail()), + url: $(anyUrl()), + uuid: $(anyUuid()) + ]) + headers { + contentType(applicationJson()) + } + } + response { + status 200 + body([ + alpha: $(anyAlphaUnicode()), + number: $(anyNumber()), + aBoolean: $(aBoolean()), + ip: $(anyIpAddress()), + hostname: $(anyHostname()), + email: $(anyEmail()), + url: $(anyUrl()), + uuid: $(anyUuid()) + ]) + headers { + contentType(applicationJson()) + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + 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("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])")') + test.contains('assertThatJson(parsedJson).field("uuid").matches("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}")') + !test.contains('cursor') + where: + methodBuilderName | methodBuilder + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } + } + private String stripped(String string) { return string.stripMargin().stripIndent().replace('\t', '').replace('\n', '').replace(' ','') } 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 903b05cbe4..9f27a1a285 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 @@ -40,7 +40,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub method 'POST' url '/users/password' headers { - header 'Content-Type': 'application/json' + contentType(applicationJson()) } body( email: $(consumer(optional(regex(email()))), producer('abc@abc.com')), @@ -50,7 +50,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub response { status 404 headers { - header 'Content-Type': 'application/json' + contentType(applicationJson()) } body( code: value(consumer("123123"), producer(optional("123123"))), @@ -67,7 +67,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub method 'POST' url '/users/password' headers { - header 'Content-Type': 'application/json' + contentType(applicationJson()) } body( """ { @@ -84,7 +84,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub response { status 404 headers { - header 'Content-Type': 'application/json' + contentType(applicationJson()) } body( """ { @@ -102,7 +102,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub given: Contract contractDsl = Contract.make { request { - method "GET" + method GET() url "test" } response { @@ -133,7 +133,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub given: Contract contractDsl = Contract.make { request { - method "GET" + method GET() url "test" } response { @@ -439,7 +439,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub ) ) headers { - header('Content-Type': 'application/json') + contentType(applicationJson()) } } } @@ -471,7 +471,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub value(consumer('123'), producer(regex('[0-9]{3}'))) }"}""") headers { - header('Content-Type': 'application/json') + contentType(applicationJson()) } } } @@ -504,7 +504,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub value(consumer('123'), producer(regex('\\d+'))) }"}""") headers { - header('Content-Type': 'application/json') + contentType(applicationJson()) } } } @@ -621,7 +621,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub given: Contract contractDsl = Contract.make { request { - method('POST') + method(POST()) url("/ws/payments") body("") } @@ -679,7 +679,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub request { method 'POST' url $(consumer(regex('/partners/[0-9]+/users')), producer('/partners/1000/users')) - headers { header 'Content-Type': 'application/json' } + headers { contentType(applicationJson()) } body( first_name: 'John', last_name: 'Smith', @@ -718,7 +718,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub request { method 'POST' url $(consumer(regex('/partners/[0-9]+/users')), producer('/partners/1000/users')) - headers { header 'Content-Type': 'application/json' } + headers { contentType(applicationJson()) } body( first_name: 'John', last_name: 'Smith', @@ -758,7 +758,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub method 'POST' url '/validation/client' headers { - header 'Content-Type': 'application/json' + contentType(applicationJson()) } body( bank_account_number: '0014282912345698765432161182', @@ -796,10 +796,10 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub Contract contractDsl = Contract.make { request { - method 'PUT' + method PUT() url "/partners/${value(consumer(regex('^[0-9]*$')), producer('11'))}/agents/11/customers/09665703Z" headers { - header 'Content-Type': 'application/json' + contentType(applicationJson()) } body( first_name: 'Josef', @@ -829,10 +829,10 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub Contract contractDsl = Contract.make { priority 1 request { - method 'POST' + method POST() url '/users/password' headers { - header 'Content-Type': 'application/json' + contentType(applicationJson()) } body( email: $(consumer(regex(email())), producer('not.existing@user.com')), @@ -842,7 +842,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub response { status 404 headers { - header 'Content-Type': 'application/json' + contentType(applicationJson()) } body( code: 4, @@ -965,7 +965,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub method "PUT" url "/v1/payments/e86df6f693de4b35ae648464c5b0dc09/client_data" headers { - header('Content-Type': 'application/json') + contentType(applicationJson()) } body( client: [ @@ -1000,7 +1000,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub response { status 200 headers { - header('Content-Type': 'application/json') + contentType(applicationJson()) } } } @@ -1029,7 +1029,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub method "PUT" url "/v1/payments/e86df6f693de4b35ae648464c5b0dc09/енев" headers { - header('Content-Type': 'application/json') + contentType(applicationJson()) } body( client: [ @@ -1041,7 +1041,7 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub response { status 200 headers { - header('Content-Type': 'application/json') + contentType(applicationJson()) } } } @@ -1094,7 +1094,7 @@ World.'''""" method "PUT" url "/multipart" headers { - header('content-type', 'multipart/form-data;boundary=AaB03x') + contentType('multipart/form-data;boundary=AaB03x') } multipart( formParameter: value(consumer(regex('.+')), producer('"formParameterValue"')), @@ -1275,7 +1275,7 @@ World.'''""" producer(regex('[0-9]+')) )]]) headers { - header('Content-Type': 'application/json;charset=UTF-8') + contentType(applicationJsonUtf8()) } } } @@ -1525,22 +1525,19 @@ World.'''""" method 'PUT' url value(consumer(regex('/foo/[0-9]{5}'))) body([ - requestElement: value(consumer(regex('[0-9]{5}'))) + requestElement: $(consumer(regex('[0-9]{5}'))) ]) headers { - header('header', value(consumer(regex('application\\/vnd\\.fraud\\.v1\\+json;.*')))) + header('header', $(consumer(regex('application\\/vnd\\.fraud\\.v1\\+json;.*')))) } } response { status 200 body([ - responseElement: value(producer(regex('[0-9]{7}'))) + responseElement: $(producer(regex('[0-9]{7}'))) ]) headers { - header('Content-Type': value( - producer(regex('application/vnd.fraud.v1.json.*')), - consumer('application/vnd.fraud.v1+json')) - ) + contentType("application/vnd.fraud.v1.json") } } } @@ -1560,7 +1557,7 @@ World.'''""" strippedTest.matches(""".*header\\("header", "application\\/vnd\\.fraud\\.v1\\+json;.*"\\).*""") strippedTest.matches(""".*body\\('''\\{"requestElement":"[0-9]{5}"\\}'''\\).*""") strippedTest.matches(""".*put\\("/foo/[0-9]{5}"\\).*""") - strippedTest.contains("""response.header('Content-Type') ==~ java.util.regex.Pattern.compile('application/vnd.fraud.v1.json.*')""") + strippedTest.contains("""response.header('Content-Type') ==~ java.util.regex.Pattern.compile('application/vnd\\.fraud\\.v1\\.json.*')""") "application/vnd.fraud.v1+json;charset=UTF-8".matches('application/vnd.fraud.v1.json.*') strippedTest.contains("""assertThatJson(parsedJson).field("responseElement").matches("[0-9]{7}")""") } @@ -1668,4 +1665,88 @@ World.'''""" test.contains("foo(responseBody)") } + @Issue('#149') + def "should allow c/p version of consumer producer"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/get' + headers { + header('authorization', $(c('Bearer token'), p(execute('getOAuthTokenHeader()')))) + } + } + response { + status 200 + body([ + fraudCheckStatus: "OK", + rejectionReason : [ + title: $(c(null), p(execute('assertThatRejectionReasonIsNull($it)'))) + ] + ]) + } + } + MethodBodyBuilder builder = new MockMvcSpockMethodRequestProcessingBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.given(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('.header("authorization", getOAuthTokenHeader())') + } + + @Issue('#149') + def "should allow easier way of providing dynamic values"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/get' + body([ + alpha: $(anyAlphaUnicode()), + number: $(anyNumber()), + aBoolean: $(aBoolean()), + ip: $(anyIpAddress()), + hostname: $(anyHostname()), + email: $(anyEmail()), + url: $(anyUrl()), + uuid: $(anyUuid()) + ]) + headers { + contentType(applicationJson()) + } + } + response { + status 200 + body([ + alpha: $(anyAlphaUnicode()), + number: $(anyNumber()), + aBoolean: $(aBoolean()), + ip: $(anyIpAddress()), + hostname: $(anyHostname()), + email: $(anyEmail()), + url: $(anyUrl()), + uuid: $(anyUuid()) + ]) + headers { + contentType(applicationJson()) + } + } + } + MethodBodyBuilder builder = new MockMvcSpockMethodRequestProcessingBodyBuilder(contractDsl, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + 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("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])")') + test.contains('assertThatJson(parsedJson).field("uuid").matches("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}")') + !test.contains('cursor') + } } diff --git a/spring-cloud-contract-verifier/src/test/resources/dsl/basic/sampleDsl.groovy b/spring-cloud-contract-verifier/src/test/resources/dsl/basic/sampleDsl.groovy index 7cf9bdc0e6..067e0f7d19 100644 --- a/spring-cloud-contract-verifier/src/test/resources/dsl/basic/sampleDsl.groovy +++ b/spring-cloud-contract-verifier/src/test/resources/dsl/basic/sampleDsl.groovy @@ -20,7 +20,7 @@ Contract.make { request { method('PUT') headers { - header 'Content-Type': 'application/json' + contentType(applicationJson()) } body("""\ { @@ -42,7 +42,7 @@ Contract.make { """ ) headers { - header 'Content-Type': 'text/plain' + contentType(textPlain()) } } } diff --git a/tests/samples-messaging-camel/src/test/groovy/com/example/CamelMessagingApplicationSpec.groovy b/tests/samples-messaging-camel/src/test/groovy/com/example/CamelMessagingApplicationSpec.groovy index 3358f2063d..ae3e1b46dc 100644 --- a/tests/samples-messaging-camel/src/test/groovy/com/example/CamelMessagingApplicationSpec.groovy +++ b/tests/samples-messaging-camel/src/test/groovy/com/example/CamelMessagingApplicationSpec.groovy @@ -94,7 +94,7 @@ class CamelMessagingApplicationSpec extends Specification { ]) messageHeaders { header('sample', 'header') - header('Content-Type', 'application/json') + } } outputMessage {