diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy index 86a6ab914b..47aa1c269c 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy @@ -64,6 +64,12 @@ interface ContractTemplate { */ String header(String key, int index) + /** + * Retruns the tempalte for retrieving the first value of a cookie with certain key + * @param key + */ + String cookie(String key) + /** * Request body text (avoid for non-text bodies) e.g. {{{ request.body }}} . The body will not be escaped * so you won't be able to directly embed it in a JSON for example. diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Cookie.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Cookie.groovy new file mode 100644 index 0000000000..a62e939da4 --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Cookie.groovy @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2017 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.internal +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Represents a http cookie + * + * @author Alex Xandra Albert Sim + * @since 1.3.8 + */ +@EqualsAndHashCode(includeFields = true, callSuper = true) +@ToString(includePackage = false, includeFields = true, ignoreNulls = true, includeNames = true, includeSuper = true) +@CompileStatic +class Cookie extends DslProperty { + + String key + + Cookie(String key, DslProperty dslProperty) { + super(dslProperty.clientValue, dslProperty.serverValue) + this.key = key + } + + Cookie(String key, MatchingStrategy value) { + super(value) + this.key = key + } + + Cookie(String key, Object value) { + super(value) + this.key = key + } +} diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Cookies.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Cookies.groovy new file mode 100644 index 0000000000..120b25c0c1 --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Cookies.groovy @@ -0,0 +1,66 @@ +/* + * Copyright 2013-2017 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.internal + +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import groovy.transform.TypeChecked + +/** + * Represents a set of http cookies + * + * @author Alex Xandra Albert Sim + * @since 1.3.8 + */ +@EqualsAndHashCode(includeFields = true) +@ToString(includePackage = false, includeFields = true, ignoreNulls = true, includeNames = true) +@TypeChecked +class Cookies { + + Set cookies = [] + + void cookie(Map singleCookie) { + Map.Entry first = singleCookie.entrySet().first() + cookies << new Cookie(first?.key, first?.value) + } + + void cookie(String cookieKey, Object cookieValue) { + cookies << new Cookie(cookieKey, cookieValue) + } + + void executeForEachCookie(Closure closure) { + cookies?.each { + cookie -> closure(cookie) + } + } + + DslProperty matching(String value) { + return new DslProperty(value) + } + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + Cookies cookies = (Cookies) o + if (cookies != cookies.cookies) return false + return true + } + + int hashCode() { + return cookies.hashCode() + } +} diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy index 27a37534f9..b56add4ee6 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy @@ -77,6 +77,14 @@ class FromRequest { return new DslProperty(template.header(key, index)) } + /** + * Retruns the tempalte for retrieving the first value of a cookie with certain key + * @param key + */ + DslProperty cookie(String key) { + return new DslProperty(template.cookie(key)) + } + /** * Request body text (avoid for non-text bodies) */ diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy index 5e223642bf..9a5acb6614 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy @@ -58,6 +58,11 @@ class HandlebarsContractTemplate implements ContractTemplate { return wrapped("request.headers.${key}.[${index}]") } + @Override + String cookie(String key) { + return wrapped("request.cookies.${key}") + } + @Override String body() { return wrapped("request.body") diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy index 9f0baa32bf..f369cc1761 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OutputMessage.groovy @@ -34,6 +34,7 @@ class OutputMessage extends Common { DslProperty sentTo Headers headers + Cookies cookies DslProperty body ExecutionProperty assertThat ResponseBodyMatchers matchers @@ -43,6 +44,7 @@ class OutputMessage extends Common { OutputMessage(OutputMessage outputMessage) { this.sentTo = outputMessage.sentTo this.headers = outputMessage.headers + this.cookies = outputMessage.cookies this.body = outputMessage.body } @@ -68,6 +70,12 @@ class OutputMessage extends Common { closure() } + void cookies(@DelegatesTo(Cookies) Closure closure) { + this.cookies = new Cookies() + closure.delegate = cookies + closure() + } + void assertThat(String assertThat) { this.assertThat = new ExecutionProperty(assertThat) } 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 ffdc9ba666..f70123c628 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 @@ -43,6 +43,7 @@ class Request extends Common { Url url UrlPath urlPath Headers headers + Cookies cookies Body body Multipart multipart BodyMatchers matchers @@ -55,6 +56,7 @@ class Request extends Common { this.url = request.url this.urlPath = request.urlPath this.headers = request.headers + this.cookies = request.cookies this.body = request.body this.multipart = request.multipart } @@ -117,6 +119,12 @@ class Request extends Common { closure() } + void cookies(@DelegatesTo(RequestCookies) Closure closure) { + this.cookies = new RequestCookies() + closure.delegate = cookies + closure() + } + void body(Map body) { this.body = new Body(convertObjectsToDslProperties(body)) } @@ -259,6 +267,18 @@ class Request extends Common { } } + @CompileStatic + @EqualsAndHashCode(includeFields = true) + @ToString(includePackage = false) + private class RequestCookies extends Cookies { + + @Override + DslProperty matching(String value) { + return $(c(regex("${RegexpUtils.escapeSpecialRegexWithSingleEscape(value)}.*")), + p(value)) + } + } + @CompileStatic @EqualsAndHashCode(includeFields = true) @ToString(includePackage = false) 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 7fa887a448..d7753bc1fe 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 @@ -40,6 +40,7 @@ class Response extends Common { DslProperty status DslProperty delay Headers headers + Cookies cookies Body body boolean async ResponseBodyMatchers matchers @@ -50,6 +51,7 @@ class Response extends Common { Response(Response response) { this.status = response.status this.headers = response.headers + this.cookies = response.cookies this.body = response.body } @@ -67,6 +69,12 @@ class Response extends Common { closure() } + void cookies(@DelegatesTo(ResponseCookies) Closure closure) { + this.cookies = new ResponseCookies() + closure.delegate = cookies + closure() + } + void body(Map body) { this.body = new Body(convertObjectsToDslProperties(body)) } @@ -169,6 +177,17 @@ class Response extends Common { } } + @CompileStatic + @EqualsAndHashCode(includeFields = true) + @ToString(includePackage = false) + private class ResponseCookies extends Cookies { + + @Override + DslProperty matching(String value) { + return $(p(regex("${RegexpUtils.escapeSpecialRegexWithSingleEscape(value)}.*")), c(value)) + } + } + @CompileStatic @EqualsAndHashCode(includeFields = true) @ToString(includePackage = false) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy index 8d74842edb..aa1b555819 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMessagingMethodBodyBuilder.groovy @@ -20,6 +20,7 @@ import groovy.json.StringEscapeUtils import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Cookie import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.Input @@ -113,6 +114,19 @@ class JUnitMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { blockBuilder.addLine("${exec.insertValue("response.getHeader(\"$property\").toString()")};") } + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, String value) { + } + + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, GString value) { + + } + + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, Pattern pattern) { + } + @Override protected void validateResponseCodeBlock(BlockBuilder bb) { @@ -129,6 +143,10 @@ class JUnitMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { } } + @Override + protected void validateResponseCookiesBlock(BlockBuilder bb) { + } + private String sentToValue(Object sentTo) { if (sentTo instanceof ExecutionProperty) { return ((ExecutionProperty) sentTo).executionCommand @@ -192,6 +210,11 @@ class JUnitMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { return ".header(${getTestSideValue(header.name)}, ${getTestSideValue(header.serverValue)})" } + @Override + protected String getCookieString(Cookie cookie) { + return "" + } + @Override protected String getBodyString(Object body) { return "" diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy index 0079442d2d..0d1dbd64fb 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JUnitMethodBodyBuilder.groovy @@ -20,6 +20,7 @@ import groovy.json.StringEscapeUtils import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Cookie import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.NamedProperty @@ -147,6 +148,11 @@ abstract class JUnitMethodBodyBuilder extends RequestProcessingMethodBodyBuilder return ".header(${getTestSideValue(header.name)}, ${getTestSideValue(header.serverValue)})" } + @Override + protected String getCookieString(Cookie cookie) { + return ".cookie(${getTestSideValue(cookie.key)}, ${getTestSideValue(cookie.serverValue)})" + } + @Override protected String getBodyString(Object body) { String value @@ -177,6 +183,15 @@ abstract class JUnitMethodBodyBuilder extends RequestProcessingMethodBodyBuilder return buildEscapedMatchesMethod(headerValue) + ";" } + protected String createCookieComparison(Object cookieValue) { + String escapedCookie = convertUnicodeEscapesIfRequired("$cookieValue") + return "isEqualTo(\"$escapedCookie\");" + } + + protected String createCookieComparison(Pattern cookieValue) { + return buildEscapedMatchesMethod(cookieValue) + ";" + } + private String buildEscapedMatchesMethod(Pattern escapedValue) { String escapedHeader = convertUnicodeEscapesIfRequired("$escapedValue") return createMatchesMethod(escapedHeader) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientJUnitMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientJUnitMethodBodyBuilder.groovy index e765e147f4..47bd2eda22 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientJUnitMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientJUnitMethodBodyBuilder.groovy @@ -19,6 +19,7 @@ package org.springframework.cloud.contract.verifier.builder import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Cookie import org.springframework.cloud.contract.spec.internal.DslProperty import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.NotToEscapePattern @@ -65,6 +66,7 @@ class JaxRsClientJUnitMethodBodyBuilder extends JUnitMethodBodyBuilder { appendUrlPathAndQueryParameters(bb) appendRequestWithRequiredResponseContentType(bb) appendHeaders(bb) + appendCookies(bb) appendMethodAndBody(bb) bb.addAtTheEnd(JUNIT.lineSuffix) @@ -123,6 +125,16 @@ class JaxRsClientJUnitMethodBodyBuilder extends JUnitMethodBodyBuilder { } } + protected appendCookies(BlockBuilder bb) { + request.cookies?.executeForEachCookie { Cookie cookie -> + if (cookieOfAbsentType(cookie)) { + return + } + + bb.addLine(".cookie(\"${cookie.key}\", \"${cookie.serverValue}\")") + } + } + protected void appendRequestWithRequiredResponseContentType(BlockBuilder bb) { String acceptHeader = getHeader("Accept") if (acceptHeader) { @@ -146,6 +158,15 @@ class JaxRsClientJUnitMethodBodyBuilder extends JUnitMethodBodyBuilder { } } + @Override + protected void validateResponseCookiesBlock(BlockBuilder bb) { + response.cookies?.executeForEachCookie { Cookie cookie -> + processCookieElement(bb, cookie.key, cookie.serverValue instanceof NotToEscapePattern ? + cookie.serverValue : + MapConverter.getTestSideValues(cookie.serverValue)) + } + } + protected String getHeader(String name) { return request.headers?.entries.find { it.name == name }?.serverValue } @@ -185,4 +206,16 @@ class JaxRsClientJUnitMethodBodyBuilder extends JUnitMethodBodyBuilder { blockBuilder.addLine("${exec.insertValue("response.getHeaderString(\"$property\")")};") } + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, Pattern pattern) { + blockBuilder.addLine("assertThat(response.getCookies().get(\"$key\")).isNotNull();") + blockBuilder.addLine("assertThat(response.getCookies().get(\"$key\").getValue()).${createCookieComparison(pattern)}") + } + + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, String value) { + blockBuilder.addLine("assertThat(response.getCookies().get(\"$key\")).isNotNull();") + blockBuilder.addLine("assertThat(response.getCookies().get(\"$key\").getValue()).${createCookieComparison(value)}") + } + } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy index 6dafd53011..a6e7327a20 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy @@ -19,6 +19,7 @@ package org.springframework.cloud.contract.verifier.builder import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Cookie import org.springframework.cloud.contract.spec.internal.DslProperty import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.NotToEscapePattern @@ -62,6 +63,7 @@ class JaxRsClientSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequ appendUrlPathAndQueryParameters(bb) appendRequestWithRequiredResponseContentType(bb) appendHeaders(bb) + appendCookies(bb) appendMethodAndBody(bb) bb.unindent() @@ -128,6 +130,16 @@ class JaxRsClientSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequ } } + protected appendCookies(BlockBuilder bb) { + request.cookies?.executeForEachCookie { Cookie cookie -> + if (cookieOfAbsentType(cookie)) { + return + } + + bb.addLine(".cookie('${cookie.key}', '${cookie.serverValue}')") + } + } + protected String getHeader(String name) { return request.headers?.entries?.find { it.name == name }?.serverValue } @@ -146,6 +158,15 @@ class JaxRsClientSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequ } } + @Override + protected void validateResponseCookiesBlock(BlockBuilder bb) { + response.cookies?.executeForEachCookie { Cookie cookie -> + processCookieElement(bb, cookie.key, cookie.serverValue instanceof NotToEscapePattern ? + cookie.serverValue : + MapConverter.getTestSideValues(cookie.serverValue)) + } + } + @Override protected String getResponseAsString() { return 'responseAsString' @@ -181,6 +202,18 @@ class JaxRsClientSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequ blockBuilder.addLine("response.getHeaderString('$property') ${convertHeaderComparison(value)}") } + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, Pattern pattern) { + blockBuilder.addLine("response.getCookies().get('$key') != null") + blockBuilder.addLine("response.getCookies().get('$key').getValue() ${convertCookieComparison(pattern)}") + } + + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, String value) { + blockBuilder.addLine("response.getCookies().get('$key') != null") + blockBuilder.addLine("response.getCookies().get('$key').getValue() ${convertCookieComparison(value)}") + } + @Override protected String postProcessJsonPathCall(String jsonPath) { if (templateProcessor.containsTemplateEntry(jsonPath)) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy index 8fef0d7fdb..8efea0c042 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy @@ -16,6 +16,8 @@ package org.springframework.cloud.contract.verifier.builder +import org.springframework.cloud.contract.spec.internal.Cookie + import java.util.regex.Pattern import com.jayway.jsonpath.DocumentContext @@ -98,6 +100,11 @@ abstract class MethodBodyBuilder { */ protected abstract void validateResponseHeadersBlock(BlockBuilder bb) + /** + * Builds the response cookies validation code block + */ + protected abstract void validateResponseCookiesBlock(BlockBuilder bb) + /** * Builds the code that returns response in the string format */ @@ -172,6 +179,21 @@ abstract class MethodBodyBuilder { */ protected abstract void processHeaderElement(BlockBuilder blockBuilder, String property, Number value) + /** + * Appends to the {@link BlockBuilder} the assertion for the given cookie path + */ + protected abstract void processCookieElement(BlockBuilder blockBuilder, String key, Pattern pattern) + + /** + * Appends to the {@link BlockBuilder} the assertion for the given cookie path + */ + protected abstract void processCookieElement(BlockBuilder blockBuilder, String key, String value) + + /** + * Appends to the {@link BlockBuilder} the assertion for the given cookie path + */ + protected abstract void processCookieElement(BlockBuilder blockBuilder, String key, GString value) + /** * Appends to the {@link BlockBuilder} the code to retrieve a value for a property * from the list with the given index @@ -201,6 +223,11 @@ abstract class MethodBodyBuilder { */ protected abstract String getHeaderString(Header header) + /** + * Builds the code to append a cookie to the request / message + */ + protected abstract String getCookieString(Cookie cookie) + /** * Builds the code to append body to the request / message */ @@ -596,6 +623,12 @@ abstract class MethodBodyBuilder { protected void processHeaderElement(BlockBuilder blockBuilder, String property, Object value) { } + /** + * Appends to the {@link BlockBuilder} the assertion for the given cookie + */ + protected void processCookieElement(BlockBuilder blockBuilder, String key, Object value) { + } + /** * Appends to the {@link BlockBuilder} the assertion for the given body element */ diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy index 1a35744196..45f475d49e 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy @@ -19,6 +19,7 @@ package org.springframework.cloud.contract.verifier.builder import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Cookie import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.NotToEscapePattern @@ -53,6 +54,15 @@ class MockMvcSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequestP } } + @Override + protected void validateResponseCookiesBlock(BlockBuilder bb) { + response.cookies?.executeForEachCookie { Cookie cookie -> + processCookieElement(bb, cookie.key, cookie.serverValue instanceof NotToEscapePattern ? + cookie.serverValue : + MapConverter.getTestSideValues(cookie.serverValue)) + } + } + @Override protected String getResponseAsString() { return 'response.body.asString()' @@ -69,6 +79,16 @@ class MockMvcSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequestP } } + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, Object value) { + if (value instanceof NotToEscapePattern) { + blockBuilder.addLine("response.cookie('$key') " + + "${patternComparison(((NotToEscapePattern) value).serverValue.pattern().replace("\\", "\\\\"))}") + } else { + processCookieElement(blockBuilder, key, value.toString()) + } + } + @Override protected void processHeaderElement(BlockBuilder blockBuilder, String property, Number number) { blockBuilder.addLine("response.header('$property') == ${number}") @@ -89,6 +109,18 @@ class MockMvcSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequestP blockBuilder.addLine("response.header('$property') ${convertHeaderComparison(value)}") } + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, Pattern pattern) { + blockBuilder.addLine("response.cookie('$key') != null") + blockBuilder.addLine("response.cookie('$key') ${convertCookieComparison(pattern)}") + } + + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, String value) { + blockBuilder.addLine("response.cookie('$key') != null") + blockBuilder.addLine("response.cookie('$key') ${convertCookieComparison(value)}") + } + // #273 - should escape $ for Groovy since it will try to make it a GString @Override protected String postProcessJsonPathCall(String jsonPath) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy index b4af946ecd..35a7c0ca46 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy @@ -22,6 +22,7 @@ import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.internal.BodyMatchers +import org.springframework.cloud.contract.spec.internal.Cookie import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.MatchingStrategy @@ -100,6 +101,14 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { } bb.addLine(getHeaderString(header)) } + + request.cookies?.executeForEachCookie { Cookie cookie -> + if (cookieOfAbsentType(cookie)) { + return + } + bb.addLine(getCookieString(cookie)) + } + if (request.body) { Object body = request.body?.serverValue instanceof ExecutionProperty ? request.body?.serverValue : bodyAsString @@ -115,6 +124,11 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { ((MatchingStrategy) header.serverValue).type == MatchingStrategy.Type.ABSENT } + protected boolean cookieOfAbsentType(Cookie cookie) { + return cookie.serverValue instanceof MatchingStrategy && + ((MatchingStrategy) cookie.serverValue).type == MatchingStrategy.Type.ABSENT + } + @Override protected void when(BlockBuilder bb) { bb.addLine(getInputString(request)) @@ -168,6 +182,9 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { if (response.headers) { validateResponseHeadersBlock(bb) } + if (response.cookies) { + validateResponseCookiesBlock(bb) + } if (response.body) { bb.endBlock() bb.addLine(addCommentSignIfRequired('and:')).startBlock() @@ -188,6 +205,12 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder { processHeaderElement(blockBuilder, property, gstringValue) } + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, GString value) { + String gStringValue = ContentUtils.extractValueForGString(value, ContentUtils.GET_TEST_SIDE).toString() + processCookieElement(blockBuilder, key, gStringValue) + } + @Override protected ContentType getResponseContentType() { ContentType contentType = recognizeContentTypeFromHeader(response.headers) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RestAssuredJUnitMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RestAssuredJUnitMethodBodyBuilder.groovy index f8efde8959..51294f1160 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RestAssuredJUnitMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RestAssuredJUnitMethodBodyBuilder.groovy @@ -19,6 +19,7 @@ package org.springframework.cloud.contract.verifier.builder import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Cookie import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.NotToEscapePattern @@ -55,6 +56,15 @@ class RestAssuredJUnitMethodBodyBuilder extends JUnitMethodBodyBuilder { } } + @Override + protected void validateResponseCookiesBlock(BlockBuilder bb) { + response.cookies?.executeForEachCookie { Cookie cookie -> + processCookieElement(bb, cookie.key, cookie.serverValue instanceof NotToEscapePattern ? + cookie.serverValue : + MapConverter.getTestSideValues(cookie.serverValue)) + } + } + @Override protected String getResponseBodyPropertyComparisonString(String property, Object value) { return null @@ -96,4 +106,15 @@ class RestAssuredJUnitMethodBodyBuilder extends JUnitMethodBodyBuilder { blockBuilder.addLine("${exec.insertValue("response.header(\"$property\")")};") } + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, Pattern pattern) { + blockBuilder.addLine("assertThat(response.getCookie(\"$key\")).isNotNull();") + blockBuilder.addLine("assertThat(response.getCookie(\"$key\")).${createCookieComparison(pattern)}") + } + + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, String value) { + blockBuilder.addLine("assertThat(response.getCookie(\"$key\")).isNotNull();") + blockBuilder.addLine("assertThat(response.getCookie(\"$key\")).${createCookieComparison(value)}") + } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy index d68ed46744..2d461b7c5e 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy @@ -20,6 +20,7 @@ import groovy.json.StringEscapeUtils import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Cookie import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.Input @@ -96,6 +97,18 @@ class SpockMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { blockBuilder.addLine("response.getHeader('$property')?.toString() ${convertHeaderComparison(value)}") } + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, Pattern pattern) { + } + + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, String value) { + } + + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, GString value) { + } + @Override protected void validateResponseCodeBlock(BlockBuilder bb) { if (outputMessage) { @@ -122,6 +135,10 @@ class SpockMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { } } + @Override + protected void validateResponseCookiesBlock(BlockBuilder bb) { + } + @Override protected String getResponseAsString() { return 'contractVerifierObjectMapper.writeValueAsString(response.payload)' @@ -187,6 +204,11 @@ class SpockMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder { return "${getTestSideValue(header.name)}: ${getTestSideValue(header.serverValue)}" } + @Override + protected String getCookieString(Cookie cookie) { + return '' + } + @Override protected String getBodyString(Object body) { return '' diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy index a8e26b52c4..0e4f0022e6 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy @@ -20,6 +20,7 @@ import groovy.json.StringEscapeUtils import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.internal.Cookie import org.springframework.cloud.contract.spec.internal.Header import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.Request @@ -122,6 +123,11 @@ abstract class SpockMethodRequestProcessingBodyBuilder extends RequestProcessing return ".header(${getTestSideValue(header.name)}, ${getTestSideValue(header.serverValue)})" } + @Override + protected String getCookieString(Cookie cookie) { + return ".cookie(${getTestSideValue(cookie.key)}, ${getTestSideValue(cookie.serverValue)})" + } + @Override protected String getBodyString(Object body) { String value @@ -149,6 +155,12 @@ abstract class SpockMethodRequestProcessingBodyBuilder extends RequestProcessing processHeaderElement(blockBuilder, property, gstringValue) } + @Override + protected void processCookieElement(BlockBuilder blockBuilder, String key, GString value) { + String gStringValue = ContentUtils.extractValueForGString(value, ContentUtils.GET_TEST_SIDE).toString() + processCookieElement(blockBuilder, key, gStringValue) + } + protected String convertHeaderComparison(String headerValue) { return " == '$headerValue'" } @@ -157,6 +169,14 @@ abstract class SpockMethodRequestProcessingBodyBuilder extends RequestProcessing return patternComparison(headerValue) } + protected String convertCookieComparison(String cookieValue) { + return "== '$cookieValue'" + } + + protected String convertCookieComparison(Pattern cookieValue) { + return patternComparison(cookieValue) + } + protected String patternComparison(Pattern pattern) { return patternComparison(pattern.toString()) } 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 9d0517e231..9286c8ade2 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 @@ -34,6 +34,56 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub @Shared ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(assertJsonSize: true) + @Shared + // tag::contract_with_cookies[] + Contract contractDslWithCookiesValue = Contract.make { + request { + method "GET" + url "/foo" + headers { + header 'Accept': 'application/json' + } + cookies { + cookie 'cookie-key': 'cookie-value' + } + } + response { + status 200 + headers { + header 'Content-Type': 'application/json' + } + cookies { + cookie 'cookie-key': 'new-cookie-value' + } + body([status: 'OK']) + } + } + // end::contract_with_cookies[] + + @Shared + Contract contractDslWithCookiesPattern = Contract.make { + request { + method "GET" + url "/foo" + headers { + header 'Accept': 'application/json' + } + cookies { + cookie 'cookie-key': regex('[A-Za-z]+') + } + } + response { + status 200 + headers { + header 'Content-Type': 'application/json' + } + cookies { + cookie 'cookie-key': regex('[A-Za-z]+') + } + body([status: 'OK']) + } + } + def "should generate assertions for simple response body with #methodBuilderName"() { given: Contract contractDsl = Contract.make { @@ -1205,4 +1255,56 @@ DATA methodBuilderName | methodBuilder "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } } + + def "should generate test for cookies with string value in JAX-RS JUnit test"() { + given: + MethodBodyBuilder builder = new JaxRsClientJUnitMethodBodyBuilder(contractDslWithCookiesValue, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''.cookie("cookie-key", "cookie-value")''') + test.contains('''assertThat(response.getCookies().get("cookie-key")).isNotNull();''') + test.contains('''assertThat(response.getCookies().get("cookie-key").getValue()).isEqualTo("new-cookie-value");''') + } + + def "should generate test for cookies with pattern in JAX-RS JUnit test"() { + given: + MethodBodyBuilder builder = new JaxRsClientJUnitMethodBodyBuilder(contractDslWithCookiesPattern, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''.cookie("cookie-key", "[A-Za-z]+")''') + test.contains('''assertThat(response.getCookies().get("cookie-key")).isNotNull();''') + test.contains('''assertThat(response.getCookies().get("cookie-key").getValue()).matches("[A-Za-z]+");''') + } + + def "should generate test for cookies with string value in JAX-RS Spock test"() { + given: + MethodBodyBuilder builder = new JaxRsClientSpockMethodRequestProcessingBodyBuilder(contractDslWithCookiesValue, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''.cookie('cookie-key', 'cookie-value')''') + test.contains('''response.getCookies().get('cookie-key') != null''') + test.contains("response.getCookies().get('cookie-key').getValue() == 'new-cookie-value'") + } + + def "should generate test for cookies with pattern in JAX-RS Spock test"() { + given: + MethodBodyBuilder builder = new JaxRsClientSpockMethodRequestProcessingBodyBuilder(contractDslWithCookiesPattern, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''.cookie('cookie-key', '[A-Za-z]+')''') + test.contains('''response.getCookies().get('cookie-key') != null''') + test.contains('''response.getCookies().get('cookie-key').getValue() ==~ java.util.regex.Pattern.compile('[A-Za-z]+')''') + } } 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 7111920b88..7f7389e61c 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,6 +40,54 @@ class MockMvcMethodBodyBuilderSpec extends Specification implements WireMockStub assertJsonSize: true ) + @Shared + Contract contractDslWithCookiesValue = Contract.make { + request { + method "GET" + url "/foo" + headers { + header 'Accept': 'application/json' + } + cookies { + cookie 'cookie-key': 'cookie-value' + } + } + response { + status 200 + headers { + header 'Content-Type': 'application/json' + } + cookies { + cookie 'cookie-key': 'new-cookie-value' + } + body([status: 'OK']) + } + } + + @Shared + Contract contractDslWithCookiesPattern = Contract.make { + request { + method "GET" + url "/foo" + headers { + header 'Accept': 'application/json' + } + cookies { + cookie 'cookie-key': regex('[A-Za-z]+') + } + } + response { + status 200 + headers { + header 'Content-Type': 'application/json' + } + cookies { + cookie 'cookie-key': regex('[A-Za-z]+') + } + body([status: 'OK']) + } + } + @Shared // tag::contract_with_regex[] Contract dslWithOptionalsInString = Contract.make { @@ -2412,4 +2460,57 @@ DocumentContext parsedJson = JsonPath.parse(json); "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | { String body -> body.contains("response.getHeaderString('Authorization') == 'foo secret bar'") } "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | { String body -> body.contains('assertThat(response.getHeaderString("Authorization")).isEqualTo("foo secret bar");') } } + + def "should generate JUnit assertions with cookies"() { + given: + MethodBodyBuilder builder = new MockMvcJUnitMethodBodyBuilder(contractDslWithCookiesValue, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''.cookie("cookie-key", "cookie-value")''') + test.contains('''assertThat(response.getCookie("cookie-key")).isNotNull();''') + test.contains('''assertThat(response.getCookie("cookie-key")).isEqualTo("new-cookie-value");''') + } + + def "should generate JUnit assertions with cookies pattern"() { + given: + MethodBodyBuilder builder = new MockMvcJUnitMethodBodyBuilder(contractDslWithCookiesPattern, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''.cookie("cookie-key", "[A-Za-z]+")''') + test.contains('''assertThat(response.getCookie("cookie-key")).isNotNull();''') + test.contains('''assertThat(response.getCookie("cookie-key")).matches("[A-Za-z]+");''') + } + + def "should generate spock assertions with cookies"() { + given: + MethodBodyBuilder builder = new MockMvcSpockMethodRequestProcessingBodyBuilder(contractDslWithCookiesValue, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''.cookie("cookie-key", "cookie-value")''') + test.contains('''response.cookie('cookie-key') != null''') + test.contains('''response.cookie('cookie-key') == 'new-cookie-value''') + } + + def "should generate spock assertions with cookies pattern"() { + given: + MethodBodyBuilder builder = new MockMvcSpockMethodRequestProcessingBodyBuilder(contractDslWithCookiesPattern, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('''.cookie("cookie-key", "[A-Za-z]+")''') + test.contains('''response.cookie('cookie-key') != null''') + test.contains('''response.cookie('cookie-key') ==~ java.util.regex.Pattern.compile('[A-Za-z]+')''') + } + }