diff --git a/samples/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java b/samples/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java index cf26b27f35..beae2c7594 100644 --- a/samples/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java +++ b/samples/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java @@ -86,6 +86,17 @@ public class LoanApplicationService { return response.getBody().getCount(); } + public String getCookies() { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("Cookie", "name=foo"); + httpHeaders.add("Cookie", "name2=bar"); + ResponseEntity response = + restTemplate.exchange("http://localhost:" + port + "/frauds/name", HttpMethod.GET, + new HttpEntity<>(httpHeaders), + String.class); + return response.getBody(); + } + public void setPort(int port) { this.port = port; } diff --git a/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java b/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java index 6a7bc3d207..97ff1f3e34 100644 --- a/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java +++ b/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java @@ -71,4 +71,12 @@ public class LoanApplicationServiceTests { assertThat(count).isEqualTo(100); } + @Test + public void shouldSuccessfullyGetCookies() { + // when: + String cookies = service.getCookies(); + // then: + assertThat(cookies).isEqualTo("foo bar"); + } + } diff --git a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudNameController.java b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudNameController.java index 4ba6881594..fe99c7c713 100644 --- a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudNameController.java +++ b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudNameController.java @@ -1,5 +1,7 @@ package com.example.fraud; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -24,6 +26,12 @@ class FraudNameController { } return new NameResponse("Don't worry " + request.getName() + " you're not a fraud"); } + + @GetMapping(value = "/frauds/name") + public String checkByName(@CookieValue("name") String value, + @CookieValue("name2") String value2) { + return value + " " + value2; + } } interface FraudVerifier { diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnACookie.groovy b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnACookie.groovy new file mode 100644 index 0000000000..54983e928e --- /dev/null +++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnACookie.groovy @@ -0,0 +1,16 @@ +package contracts.fraudname + +org.springframework.cloud.contract.spec.Contract.make { + request { + method GET() + url '/frauds/name' + cookies { + cookie("name", "foo") + cookie(name2: "bar") + } + } + response { + status 200 + body("foo bar") + } +} \ No newline at end of file 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..e47d2f41b3 --- /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.2.5 + */ +@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..29b7472de3 --- /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.2.5 + */ +@EqualsAndHashCode(includeFields = true) +@ToString(includePackage = false, includeFields = true, ignoreNulls = true, includeNames = true) +@TypeChecked +class Cookies { + + Set entries = [] + + void cookie(Map singleCookie) { + Map.Entry first = singleCookie.entrySet().first() + entries << new Cookie(first?.key, first?.value) + } + + void cookie(String cookieKey, Object cookieValue) { + entries << new Cookie(cookieKey, cookieValue) + } + + void executeForEachCookie(Closure closure) { + entries?.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.entries) return false + return true + } + + int hashCode() { + return entries.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-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.jar b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 3baa851b28..0000000000 Binary files a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.properties b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.properties index 95d7279349..e69de29bb2 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.properties +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +0,0 @@ -#Fri Aug 19 15:38:58 CEST 2016 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.0-bin.zip diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/m2repo/repository/com/example/jersey-contracts/0.0.1-SNAPSHOT/jersey-contracts-0.0.1-SNAPSHOT.jar b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/m2repo/repository/com/example/jersey-contracts/0.0.1-SNAPSHOT/jersey-contracts-0.0.1-SNAPSHOT.jar index 04621a7bb9..8de83843d0 100644 Binary files a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/m2repo/repository/com/example/jersey-contracts/0.0.1-SNAPSHOT/jersey-contracts-0.0.1-SNAPSHOT.jar and b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/m2repo/repository/com/example/jersey-contracts/0.0.1-SNAPSHOT/jersey-contracts-0.0.1-SNAPSHOT.jar differ diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/m2repo/repository/com/example/jersey-contracts/0.0.1-SNAPSHOT/maven-metadata-local.xml b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/m2repo/repository/com/example/jersey-contracts/0.0.1-SNAPSHOT/maven-metadata-local.xml index 05d9ce3299..b6f14758ae 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/m2repo/repository/com/example/jersey-contracts/0.0.1-SNAPSHOT/maven-metadata-local.xml +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/sampleJerseyProject/m2repo/repository/com/example/jersey-contracts/0.0.1-SNAPSHOT/maven-metadata-local.xml @@ -12,7 +12,7 @@ jar 0.0.1-SNAPSHOT - 20160916125313 + 20170916125313 pom 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/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy index df528651fe..468923744c 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy @@ -43,6 +43,7 @@ class YamlContract { public String urlPath public Map queryParameters = [:] public Map headers = [:] + public Map cookies = [:] public Object body public String bodyFromFile public StubMatchers matchers = new StubMatchers() @@ -66,6 +67,7 @@ class YamlContract { static class StubMatchers { public List body = [] public List headers = [] + public List cookies = [] public MultipartStubMatcher multipart } @@ -121,6 +123,14 @@ class YamlContract { public PredefinedRegex predefined } + @CompileStatic + static class TestCookieMatcher { + public String key + public String regex + public String command + public PredefinedRegex predefined + } + @CompileStatic static enum PredefinedRegex { only_alpha_unicode, number, any_boolean, ip_address, hostname, @@ -142,6 +152,7 @@ class YamlContract { static class Response { public int status public Map headers = [:] + public Map cookies = [:] public Object body public String bodyFromFile public TestMatchers matchers = new TestMatchers() @@ -152,6 +163,7 @@ class YamlContract { static class TestMatchers { public List body = [] public List headers = [] + public List cookies = [] } @CompileStatic diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy index 9f9b759d17..11a18e8e32 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy @@ -45,6 +45,7 @@ import org.springframework.cloud.contract.verifier.converter.YamlContract.KeyVal import org.springframework.cloud.contract.verifier.converter.YamlContract.StubMatcherType import org.springframework.cloud.contract.verifier.converter.YamlContract.StubMatchers import org.springframework.cloud.contract.verifier.converter.YamlContract.TestHeaderMatcher +import org.springframework.cloud.contract.verifier.converter.YamlContract.TestCookieMatcher import org.springframework.cloud.contract.verifier.converter.YamlContract.TestMatcherType import org.springframework.cloud.contract.verifier.util.MapConverter @@ -132,6 +133,16 @@ class YamlContractConverter implements ContractConverter> { } } } + if (yamlContract.request?.cookies) { + cookies { + yamlContract.request?.cookies?.each { String key, Object value -> + KeyValueMatcher matcher = yamlContract.request.matchers.cookies.find { it.key == key } + Object clientValue = clientValue(value, matcher, key) + + cookie(key, new DslProperty(clientValue, value)) + } + } + } if (yamlContract.request.body) body(yamlContract.request.body) if (yamlContract.request.bodyFromFile) body(file(yamlContract.request.bodyFromFile)) if (yamlContract.request.multipart) { @@ -211,6 +222,16 @@ class YamlContractConverter implements ContractConverter> { } } } + if (yamlContract.response?.cookies) { + cookies { + yamlContract.response?.cookies?.each { String key, Object value -> + TestCookieMatcher matcher = yamlContract.response.matchers.cookies.find { it.key == key } + Object serverValue = serverCookieValue(value, matcher, key) + + cookie(key, new DslProperty(value, serverValue)) + } + } + } if (yamlContract.response.body) body(yamlContract.response.body) if (yamlContract.response.bodyFromFile) body(file(yamlContract.response.bodyFromFile)) if (yamlContract.response.async) async() @@ -381,6 +402,20 @@ class YamlContractConverter implements ContractConverter> { return serverValue } + protected Object serverCookieValue(Object value, TestCookieMatcher matcher, String key) { + Object serverValue = value + if (matcher?.regex) { + serverValue = Pattern.compile(matcher.regex) + Pattern pattern = (Pattern) serverValue + assertPatternMatched(pattern, value, key) + } else if (matcher?.predefined) { + Pattern pattern = predefinedToPattern(matcher.predefined) + serverValue = pattern + assertPatternMatched(pattern, value, key) + } + return serverValue + } + protected Object clientValue(Object value, KeyValueMatcher matcher, String key) { Object clientValue = value if (matcher?.regex) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy index 1550e13773..c19f145576 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy @@ -23,6 +23,7 @@ import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder import com.github.tomakehurst.wiremock.matching.StringValuePattern import com.github.tomakehurst.wiremock.matching.UrlPattern import groovy.json.JsonOutput +import groovy.transform.CompileDynamic import groovy.transform.PackageScope import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode @@ -72,6 +73,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { } RequestPatternBuilder requestPatternBuilder = appendMethodAndUrl() appendHeaders(requestPatternBuilder) + appendCookies(requestPatternBuilder) appendQueryParameters(requestPatternBuilder) appendBody(requestPatternBuilder) appendMultipart(requestPatternBuilder) @@ -148,6 +150,15 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { } } + private void appendCookies(RequestPatternBuilder requestPattern) { + if(!request.cookies) { + return + } + request.cookies.entries.each { + requestPattern.withCookie(it.key, convertToValuePattern(it.clientValue)) + } + } + private UrlPattern urlPattern() { Object urlPath = urlPathOrUrlIfQueryPresent() if (urlPath) { @@ -293,6 +304,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { return containsPattern(map.entrySet()) } + @CompileDynamic private boolean containsPattern(Collection collection) { return collection.collect(this.&containsPattern).inject('') { a, b -> a || b } } 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..82b4a5528b 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,71 @@ 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']) + } + } + + @Shared + Contract contractDslWithAbsentCookies = Contract.make { + request { + method "GET" + url "/foo" + cookies { + cookie 'cookie-key': absent() + } + } + response { + status 200 + body([status: 'OK']) + } + } + def "should generate assertions for simple response body with #methodBuilderName"() { given: Contract contractDsl = Contract.make { @@ -1205,4 +1270,90 @@ 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");''') + and: + SyntaxChecker.tryToCompile("JaxRsClientJUnitMethodBodyBuilder", blockBuilder.toString()) + } + + 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]+");''') + and: + SyntaxChecker.tryToCompile("JaxRsClientJUnitMethodBodyBuilder", blockBuilder.toString()) + } + + def "should not generate cookie assertions with absent value in JAX-RS JUnit test"() { + given: + MethodBodyBuilder builder = new JaxRsClientJUnitMethodBodyBuilder(contractDslWithAbsentCookies, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + !test.contains("cookie") + and: + SyntaxChecker.tryToCompile("JaxRsClientJunitMethodBodyBuilder", blockBuilder.toString()) + } + + 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'") + and: + SyntaxChecker.tryToCompile("JaxRsClientSpockMethodRequestProcessingBodyBuilder", blockBuilder.toString()) + } + + 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]+')''') + and: + SyntaxChecker.tryToCompile("JaxRsClientSpockMethodRequestProcessingBodyBuilder", blockBuilder.toString()) + } + + def "should not generate cookie assertions with absent value in JAX-RS Spock test"() { + given: + MethodBodyBuilder builder = new JaxRsClientSpockMethodRequestProcessingBodyBuilder(contractDslWithAbsentCookies, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + !test.contains("cookie") + and: + SyntaxChecker.tryToCompile("JaxRsClientSpockMethodRequestProcessingBodyBuilder", blockBuilder.toString()) + } } 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..d6fce5b5da 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,69 @@ 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 + Contract contractDslWithAbsentCookies = Contract.make { + request { + method "GET" + url "/foo" + cookies { + cookie 'cookie-key': absent() + } + } + response { + status 200 + body([status: 'OK']) + } + } + @Shared // tag::contract_with_regex[] Contract dslWithOptionalsInString = Contract.make { @@ -2412,4 +2475,91 @@ 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");''') + and: + SyntaxChecker.tryToCompile("MockMvcJUnitMethodBodyBuilder", blockBuilder.toString()) + } + + 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]+");''') + and: + SyntaxChecker.tryToCompile("MockMvcJUnitMethodBodyBuilder", blockBuilder.toString()) + } + + def "should not generate JUnit cookie assertion with absent cookie"() { + given: + MethodBodyBuilder builder = new MockMvcJUnitMethodBodyBuilder(contractDslWithAbsentCookies, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + !test.contains("cookie") + and: + SyntaxChecker.tryToCompile("MockMvcJUnitMethodBodyBuilder", blockBuilder.toString()) + } + + 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''') + and: + SyntaxChecker.tryToCompile("MockMvcSpockMethodRequestProcessingBodyBuilder", blockBuilder.toString()) + } + + 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]+')''') + and: + SyntaxChecker.tryToCompile("MockMvcSpockMethodRequestProcessingBodyBuilder", blockBuilder.toString()) + } + + def "should not generate spock cookie assertion with absent cookie"() { + given: + MethodBodyBuilder builder = new MockMvcSpockMethodRequestProcessingBodyBuilder(contractDslWithAbsentCookies, properties) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + !test.contains("cookie") + and: + SyntaxChecker.tryToCompile("MockMvcSpockMethodRequestProcessingBodyBuilder", blockBuilder.toString()) + } + } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy index 02d67a9506..0d6bd5d4b3 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy @@ -58,8 +58,34 @@ class YamlContractConverterSpec extends Specification { File ymlMultiple = new File(ymlMultipleFile.toURI()) URL ymlMessagingMatchersFile = YamlContractConverterSpec.getResource("/yml/contract_message_matchers.yml") File ymlMessagingMatchers = new File(ymlMessagingMatchersFile.toURI()) + URL ymlCookiesUrl = YamlContractConverterSpec.getResource("/yml/contract_cookies.yml") + File ymlCookies = new File(ymlCookiesUrl.toURI()) YamlContractConverter converter = new YamlContractConverter() + def "should convert YAML with Cookies to DSL"() { + given: + assert converter.isAccepted(ymlCookies) + when: + Collection contracts = converter.convertFrom(ymlCookies) + then: + contracts.size() == 1 + Contract contract = contracts.first() + contract.description == "Contract with cookies" + contract.name == "cookies-contract" + contract.priority == 1 + contract.ignored == true + contract.request.method.clientValue == "PUT" + contract.request.url.clientValue == "/foo" + contract.request.cookies.entries.find { it.key == "foo" && it.serverValue == "bar" } + contract.request.cookies.entries.find { it.key == "fooRegex" && ((Pattern) it.clientValue).pattern == "reg" && it.serverValue == "reg" } + and: + contract.response.status.clientValue == 200 + contract.response.cookies.entries.find { it.key == "foo" && it.clientValue == "baz" } + contract.response.cookies.entries.find { it.key == "fooRegex" && ((Pattern) it.serverValue).pattern == "[0-9]+" && it.clientValue == 123 } + contract.response.cookies.entries.find { it.key == "source" && ((Pattern) it.serverValue).pattern == "ip_address" && it.clientValue == "ip_address" } + contract.response.body.clientValue == ["status": "OK"] + } + def "should convert YAML with REST to DSL for [#yamlFile]"() { given: assert converter.isAccepted(yamlFile) diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy index dc31b06385..991131503b 100755 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/WireMockGroovyDslSpec.groovy @@ -1750,6 +1750,9 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie header(authorization(), "secret") header(authorization(), "secret2") } + cookies { + cookie("foo", "bar") + } body(foo: "bar", baz: 5) } response { @@ -1785,6 +1788,11 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie "equalTo" : "secret2" } }, + "cookies" : { + "foo" : { + "equalTo" : "bar" + } + }, "queryParameters" : { "foo" : { "equalTo" : "bar2" @@ -2017,6 +2025,7 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie RequestEntity.post(URI.create("http://localhost:" + port + "/api/v1/xxxx?foo=bar&foo=bar2")) .header("Authorization", "secret") .header("Authorization", "secret2") + .header("Cookie", "foo=bar") .body("{\"foo\":\"bar\",\"baz\":5}"), String.class) } } \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_cookies.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_cookies.yml new file mode 100644 index 0000000000..24c68802d0 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_cookies.yml @@ -0,0 +1,28 @@ +description: Contract with cookies +name: cookies-contract +priority: 1 +ignored: true +request: + method: PUT + url: /foo + cookies: + foo: bar + fooRegex: reg + matchers: + cookies: + - key: fooRegex + regex: reg +response: + status: 200 + cookies: + foo: baz + fooRegex: 123 + source: ip_address + body: + status: OK + matchers: + cookies: + - key: fooRegex + regex: "[0-9]+" + - key: source + regex: ip_address