diff --git a/samples/standalone/kotlin/http-client/src/test/kotlin/com/example/loan/LoanApplicationServiceTests.kt b/samples/standalone/kotlin/http-client/src/test/kotlin/com/example/loan/LoanApplicationServiceTests.kt index 227b1b8da9..f6d6147334 100644 --- a/samples/standalone/kotlin/http-client/src/test/kotlin/com/example/loan/LoanApplicationServiceTests.kt +++ b/samples/standalone/kotlin/http-client/src/test/kotlin/com/example/loan/LoanApplicationServiceTests.kt @@ -23,6 +23,7 @@ import java.nio.file.Files import com.example.loan.model.Client import com.example.loan.model.LoanApplication import com.example.loan.model.LoanApplicationStatus +import com.fasterxml.jackson.annotation.JsonProperty import com.jayway.jsonpath.JsonPath import com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson import io.restassured.RestAssured @@ -145,4 +146,23 @@ class LoanApplicationServiceTests { assertThat(exchange.body).isEqualTo(Files.readAllBytes(response.toPath())) } + @Test + fun shouldSuccessfullyReturnRequestDataInResponse() { + // when: + val exchange: ResponseEntity = RestTemplate().exchange( + RequestEntity.put(URI.create("http://localhost:6565/frauds/name")) + .header("Content-Type", "application/json") + .body(FraudCheckRequest("Tim")), + FraudCheckResponse::class.java) + // then: + assertThat(exchange.statusCodeValue).isEqualTo(200) + assertThat(exchange.headers["Content-Type"]?.get(0)) + .isEqualTo("application/json") + // and: + assertThat(exchange.body?.message).isEqualTo("Don't worry Tim you're not a fraud") + } + } + +data class FraudCheckRequest(@JsonProperty("name") val name: String) +data class FraudCheckResponse(@JsonProperty("result") val message: String) diff --git a/samples/standalone/kotlin/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.kts b/samples/standalone/kotlin/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.kts index f364c70e90..b486db8848 100644 --- a/samples/standalone/kotlin/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.kts +++ b/samples/standalone/kotlin/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.kts @@ -17,8 +17,6 @@ package fraudname import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract -import org.springframework.cloud.contract.spec.internal.HttpMethods -import org.springframework.cloud.contract.spec.internal.HttpStatus contract { // highest priority @@ -33,10 +31,9 @@ contract { } response { status = OK -// TODO -// body = body("result" to "Sorry ${fromRequest().body("$.name")} but you're a fraud") -// headers { -// header(contentType(), fromRequest().header(contentType())) -// } + body = body("result" to "Sorry ${fromRequest().body("$.name")} but you're a fraud") + headers { + contentType = fromRequest().header(CONTENT_TYPE) + } } } diff --git a/samples/standalone/kotlin/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.kts b/samples/standalone/kotlin/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.kts index 2216585934..509c83e854 100644 --- a/samples/standalone/kotlin/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.kts +++ b/samples/standalone/kotlin/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.kts @@ -17,8 +17,6 @@ package fraudname import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract -import org.springframework.cloud.contract.spec.internal.HttpMethods -import org.springframework.cloud.contract.spec.internal.HttpStatus contract { request { @@ -31,10 +29,9 @@ contract { } response { status = OK -// TODO -// body("result" to "Don't worry ${fromRequest().body("$.name")} you're not a fraud") -// headers { -// header(contentType(), fromRequest().header(contentType())) -// } + body = body("result" to "Don't worry ${fromRequest().body("$.name")} you're not a fraud") + headers { + contentType = fromRequest().header(CONTENT_TYPE) + } } } diff --git a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/FromRequestDsl.kt b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/FromRequestDsl.kt new file mode 100644 index 0000000000..59a3f6f295 --- /dev/null +++ b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/FromRequestDsl.kt @@ -0,0 +1,173 @@ +/* + * Copyright 2013-2019 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 + * + * https://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 org.springframework.cloud.contract.spec.ContractTemplate + +/** + * @author Tim Ysewyn + */ +class FromRequestDsl { + + private val template: ContractTemplate = HandlebarsContractTemplate() + + /** + * @return URL path and query. + */ + fun url() = FromRequestDslProperty(template.escapedUrl()) + + /** + * First value of a query parameter e.g. request.query.search. + * @param key key for the query param + * @return dsl property + */ + fun query(key: String) = FromRequestDslProperty(template.escapedQuery(key)) + + /** + * nth value of a query parameter (zero indexed) e.g. request.query.search.[5]. + * @param key key for the query param + * @param index index of the query param + * @return dsl property + */ + fun query(key: String, index: Int) = FromRequestDslProperty(template.escapedQuery(key, index)) + + /** + * URL path. + * @return dsl property + */ + fun path() = FromRequestDslProperty(template.escapedPath()) + + /** + * nth value of a URL path (zero indexed) e.g. {{{ request.path.[2] }}}* @param index. + * @param index path index + * @return dsl property + */ + fun path(index: Int) = FromRequestDslProperty(template.escapedPath(index)) + + /** + * First value of a request header e.g. request.headers.X-Request-Id. + * @param key header key + * @return dsl property + */ + fun header(key: String) = FromRequestDslProperty(template.escapedHeader(key)) + + /** + * nth value of a request header (zero indexed) e.g. request.headers.X-Request-Id. + * @param key header key + * @param index header index + * @return dsl property + */ + fun header(key: String, index: Int) = FromRequestDslProperty(template.escapedHeader(key, index)) + + /** + * Retruns the tempalte for retrieving the first value of a cookie with certain key. + * @param key cookie key + * @return dsl property + */ + fun cookie(key: String) = FromRequestDslProperty(template.escapedCookie(key)) + + /** + * Request body text (avoid for non-text bodies). + * @return dsl property + */ + fun body() = FromRequestDslProperty(template.escapedBody()) + + /** + * Request body text for the given JsonPath. + * @param jsonPath json path body + * @return dsl property + */ + fun body(jsonPath: String) = FromRequestDslProperty(template.escapedBody(jsonPath)) + + /** + * Unescaped URL path and query. + * @return dsl property + */ + fun rawUrl() = FromRequestDslProperty(template.url()) + + /** + * Unescaped First value of a query parameter e.g. request.query.search. + * @param key query key + * @return dsl property + */ + fun rawQuery(key: String) = FromRequestDslProperty(template.query(key)) + + /** + * Unescaped nth value of a query parameter (zero indexed) e.g. + * request.query.search.[5]. + * @param key query key + * @param index query index + * @return dsl property + */ + fun rawQuery(key: String, index: Int) = FromRequestDslProperty(template.query(key, index)) + + /** + * Unescaped URL path. + * @return dsl property + */ + fun rawPath() = FromRequestDslProperty(template.path()) + + /** + * Unescaped nth value of a URL path (zero indexed) e.g. {{{ request.path.[2]. }}}* + * @param index path index + * @return dsl property + */ + fun rawPath(index: Int) = FromRequestDslProperty(template.path(index)) + + /** + * Unescaped First value of a request header e.g. request.headers.X-Request-Id. + * @param key header key + * @return dsl property + */ + fun rawHeader(key: String) = FromRequestDslProperty(template.header(key)) + + /** + * Unescaped nth value of a request header (zero indexed) e.g. + * request.headers.X-Request-Id. + * @param key header key + * @param index header index + * @return dsl property + */ + fun rawHeader(key: String, index: Int) = FromRequestDslProperty(template.header(key, index)) + + /** + * Unescaped Returns the template for retrieving the first value of a cookie with + * certain key. + * @param key cookie key + * @return dsl property + */ + fun rawCookie(key: String) = FromRequestDslProperty(template.cookie(key)) + + /** + * Unescaped Request body text (avoid for non-text bodies). + * @return dsl property + */ + fun rawBody() = FromRequestDslProperty(template.body()) + + /** + * Unescaped Request body text for the given JsonPath. + * @param jsonPath json path body + * @return dsl property + */ + fun rawBody(jsonPath: String) = FromRequestDslProperty(template.body(jsonPath)) + +} + +class FromRequestDslProperty(private val content: String) : DslProperty(content) { + // Overridden to support String interpolation + override fun toString(): String = content +} \ No newline at end of file diff --git a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/HeadersDsl.kt b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/HeadersDsl.kt index bd5401f4bf..e8d4239f2f 100644 --- a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/HeadersDsl.kt +++ b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/HeadersDsl.kt @@ -275,6 +275,132 @@ open class HeadersDsl: CommonDsl() { /* HELPER VARIABLES */ + /* HTTP HEADERS */ + + val ACCEPT = HttpHeaders.ACCEPT + + val ACCEPT_CHARSET = HttpHeaders.ACCEPT_CHARSET + + val ACCEPT_ENCODING = HttpHeaders.ACCEPT_ENCODING + + val ACCEPT_LANGUAGE = HttpHeaders.ACCEPT_LANGUAGE + + val ACCEPT_RANGES = HttpHeaders.ACCEPT_RANGES + + val ACCESS_CONTROL_ALLOW_CREDENTIALS = HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS + + val ACCESS_CONTROL_ALLOW_HEADERS = HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS + + val ACCESS_CONTROL_ALLOW_METHODS = HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS + + val ACCESS_CONTROL_ALLOW_ORIGIN = HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN + + val ACCESS_CONTROL_EXPOSE_HEADERS = HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS + + val ACCESS_CONTROL_MAX_AGE = HttpHeaders.ACCESS_CONTROL_MAX_AGE + + val ACCESS_CONTROL_REQUEST_HEADERS = HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS + + val ACCESS_CONTROL_REQUEST_METHOD = HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD + + val AGE = HttpHeaders.AGE + + val ALLOW = HttpHeaders.ALLOW + + val AUTHORIZATION = HttpHeaders.AUTHORIZATION + + val CACHE_CONTROL = HttpHeaders.CACHE_CONTROL + + val CONNECTION = HttpHeaders.CONNECTION + + val CONTENT_ENCODING = HttpHeaders.CONTENT_ENCODING + + val CONTENT_DISPOSITION = HttpHeaders.CONTENT_DISPOSITION + + val CONTENT_LANGUAGE = HttpHeaders.CONTENT_LANGUAGE + + val CONTENT_LENGTH = HttpHeaders.CONTENT_LENGTH + + val CONTENT_LOCATION = HttpHeaders.CONTENT_LOCATION + + val CONTENT_RANGE = HttpHeaders.CONTENT_RANGE + + val CONTENT_TYPE = HttpHeaders.CONTENT_TYPE + + val COOKIE = HttpHeaders.COOKIE + + val DATE = HttpHeaders.DATE + + val ETAG = HttpHeaders.ETAG + + val EXPECT = HttpHeaders.EXPECT + + val EXPIRES = HttpHeaders.EXPIRES + + val FROM = HttpHeaders.FROM + + val HOST = HttpHeaders.HOST + + val IF_MATCH = HttpHeaders.IF_MATCH + + val IF_MODIFIED_SINCE = HttpHeaders.IF_MODIFIED_SINCE + + val IF_NONE_MATCH = HttpHeaders.IF_NONE_MATCH + + val IF_RANGE = HttpHeaders.IF_RANGE + + val IF_UNMODIFIED_SINCE = HttpHeaders.IF_UNMODIFIED_SINCE + + val LAST_MODIFIED = HttpHeaders.LAST_MODIFIED + + val LINK = HttpHeaders.LINK + + val LOCATION = HttpHeaders.LOCATION + + val MAX_FORWARDS = HttpHeaders.MAX_FORWARDS + + val ORIGIN = HttpHeaders.ORIGIN + + val PRAGMA = HttpHeaders.PRAGMA + + val PROXY_AUTHENTICATE = HttpHeaders.PROXY_AUTHENTICATE + + val PROXY_AUTHORIZATION = HttpHeaders.PROXY_AUTHORIZATION + + val RANGE = HttpHeaders.RANGE + + val REFERER = HttpHeaders.REFERER + + val RETRY_AFTER = HttpHeaders.RETRY_AFTER + + val SERVER = HttpHeaders.SERVER + + val SET_COOKIE = HttpHeaders.SET_COOKIE + + val SET_COOKIE_2 = HttpHeaders.SET_COOKIE_2 + + val TE = HttpHeaders.TE + + val TRAILER = HttpHeaders.TRAILER + + val TRANSFER_ENCODING = HttpHeaders.TRANSFER_ENCODING + + val UPGRADE = HttpHeaders.UPGRADE + + val USER_AGENT = HttpHeaders.USER_AGENT + + val VARY = HttpHeaders.VARY + + val VIA = HttpHeaders.VIA + + val WARNING = HttpHeaders.WARNING + + val WWW_AUTHENTICATE = HttpHeaders.WWW_AUTHENTICATE + + /* MESSAGING HEADERS */ + + val MESSAGING_CONTENT_TYPE = MessagingHeaders.MESSAGING_CONTENT_TYPE + /* MEDIA TYPES */ val ALL_VALUE = MediaTypes.ALL_VALUE diff --git a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/ResponseDsl.kt b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/ResponseDsl.kt index 184a0c1664..aa6f4b2a63 100644 --- a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/ResponseDsl.kt +++ b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/ResponseDsl.kt @@ -229,8 +229,7 @@ class ResponseDsl : CommonDsl(), RegexCreatingProperty { fun `$`(server: ServerDslProperty, client: ClientDslProperty) = delegate.value(client, server) -// TODO, needs to be reworked - no lazy string interpolation like in Groovy -// fun fromRequest() = FromRequest() + fun fromRequest() = FromRequestDsl() override fun anyAlphaUnicode() = delegate.anyAlphaUnicode() diff --git a/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/ContractTests.kt b/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/ContractTests.kt index d10791b1a1..2e066fe9da 100644 --- a/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/ContractTests.kt +++ b/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/ContractTests.kt @@ -881,4 +881,37 @@ then: } } + @Test + fun `should support fromRequest`() { + val contract = contract { + request { + method = GET + url = url("/path") + body = body("id" to mapOf("value" to "132")) + headers { + accept = APPLICATION_JSON + } + } + response { + status = OK + body = body("value is ${fromRequest().body("$.value")}") + headers { + contentType = fromRequest().header(ACCEPT) + } + } + } + + assertDoesNotThrow { + Contract.assertContract(contract) + }.also { + val response = contract.response + assertThat(response.body.clientValue).isEqualTo("value is {{{jsonPath request.body '$.value'}}}") + val headers = response.headers.entries + assertThat(headers).hasSize(1) + assertThat(headers.elementAt(0).name).isEqualTo("Content-Type") + assertThat(headers.elementAt(0).clientValue).isEqualTo("{{{request.headers.Accept.[0]}}}") + assertThat(headers.elementAt(0).serverValue).isEqualTo("{{{request.headers.Accept.[0]}}}") + } + } + } \ No newline at end of file