diff --git a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/CommonDsl.kt b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/CommonDsl.kt index d4e5536533..5f4f7eb7dd 100644 --- a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/CommonDsl.kt +++ b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/CommonDsl.kt @@ -16,6 +16,9 @@ package org.springframework.cloud.contract.spec.internal +import java.io.File +import java.net.URISyntaxException +import java.nio.charset.Charset import java.util.regex.Pattern /** @@ -85,6 +88,43 @@ open class CommonDsl { fun execute(commandToExecute: String) = ExecutionProperty(commandToExecute) + /** + * Read file contents as String. + * @param relativePath of the file to read + * @return String file contents + */ + fun file(relativePath: String) = file(relativePath, Charset.defaultCharset()) + + /** + * Read file contents as String with the given Charset. + * @param relativePath of the file to read + * @param charset to use for converting the bytes to String + * @return String file contents + */ + fun file(relativePath: String, charset: Charset) = Body(FromFileProperty(fileLocation(relativePath), String::class.java, charset)) + + /** + * Read file contents as bytes[]. + * @param relativePath of the file to read + * @return String file contents + */ + fun fileAsBytes(relativePath: String) = Body(FromFileProperty(fileLocation(relativePath), ByteArray::class.java)) + + /** + * Read file contents as array of bytes. + * @param relativePath of the file to read + * @return file contents as an array of bytes + */ + private fun fileLocation(relativePath: String): File { + val resource = Thread.currentThread().contextClassLoader + .getResource(relativePath) ?: throw IllegalStateException("File [$relativePath] is not present") + try { + return File(resource.toURI()) + } catch (ex: URISyntaxException) { + throw IllegalStateException(ex) + } + } + /* REGEX */ fun regexProperty(value: Any) = RegexProperty(value) diff --git a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/KotlinContractConverter.kt b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/KotlinContractConverter.kt index 98285809fc..ee63ced2ce 100644 --- a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/KotlinContractConverter.kt +++ b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/KotlinContractConverter.kt @@ -19,6 +19,7 @@ package org.springframework.cloud.contract.spec.internal import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.ContractConverter import java.io.File +import java.net.URLClassLoader.newInstance import javax.script.ScriptEngineManager /** @@ -39,10 +40,12 @@ class KotlinContractConverter: ContractConverter> { } override fun convertFrom(file: File): Collection { - val eval = file.reader().use { - // Get a new engine every time we need to process a file. - // Reusing the script engine could leak context and will fail subsequent evals - ScriptEngineManager().getEngineByExtension(ext).eval(it) + val eval = withUpdatedClassloader(file) { + file.reader().use { + // Get a new engine every time we need to process a file. + // Reusing the script engine could leak context and will fail subsequent evals + ScriptEngineManager().getEngineByExtension(ext).eval(it) + } } return when (eval) { is Contract -> listOf(eval) @@ -53,4 +56,15 @@ class KotlinContractConverter: ContractConverter> { } override fun convertTo(contract: Collection) = contract.toList() + + private fun withUpdatedClassloader(file: File, block: ClassLoader.() -> Any): Any { + val currentClassLoader = Thread.currentThread().contextClassLoader + try { + val tempClassLoader = newInstance(arrayOf(file.parentFile.toURI().toURL()), currentClassLoader) + Thread.currentThread().contextClassLoader = tempClassLoader + return tempClassLoader.block() + } finally { + Thread.currentThread().contextClassLoader = currentClassLoader + } + } } \ No newline at end of file diff --git a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/RequestDsl.kt b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/RequestDsl.kt index bf7de2fea8..cbaaab54f4 100644 --- a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/RequestDsl.kt +++ b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/internal/RequestDsl.kt @@ -17,6 +17,7 @@ package org.springframework.cloud.contract.spec.internal import org.springframework.cloud.contract.spec.toDslProperties +import org.springframework.cloud.contract.spec.toDslProperty import java.util.regex.Pattern /** @@ -36,73 +37,45 @@ open class RequestDsl : CommonDsl(), RegexCreatingProperty { var multipart: Multipart? = null var bodyMatchers: BodyMatchers? = null - fun method(method: String) { - this.method = DslProperty(method) - } + fun method(method: String) = method.toDslProperty() - fun method(method: HttpMethods.HttpMethod) { - this.method(method.toString()) - } + fun method(method: HttpMethods.HttpMethod) = this.method(method.toString()) - fun url(url: String) { - this.url = Url(url) - } + fun url(url: String) = Url(url) - fun url(url: DslProperty) { - this.url = Url(url) - } + fun url(url: DslProperty) = Url(url) - fun urlPath(url: String) { - this.urlPath = UrlPath(url) - } + fun path(path: String) = UrlPath(path) - fun urlPath(url: DslProperty) { - this.urlPath = UrlPath(url) - } + fun path(path: DslProperty) = UrlPath(path) fun headers(headers: Headers.() -> Unit) { - this.headers = Headers().apply(headers) + this.headers = Request.RequestHeaders().apply(headers) } fun cookies(cookies: Cookies.() -> Unit) { - this.cookies = Cookies().apply(cookies) + this.cookies = Request.RequestCookies().apply(cookies) } - fun body(body: Map) { - this.body = Body(body.toDslProperties()) - } + fun body(body: Map) = Body(body.toDslProperties()) - fun body(vararg body: Pair) { - this.body = Body(body.toMap().toDslProperties()) - } + fun body(vararg body: Pair) = Body(body.toMap().toDslProperties()) - fun body(body: Pair) { - this.body = Body(mapOf(body).toDslProperties()) - } + fun body(body: Pair) = Body(mapOf(body).toDslProperties()) - fun body(body: List) { - this.body = Body(body.toDslProperties()) - } + fun body(body: List) = Body(body.toDslProperties()) - fun body(body: DslProperty) { - this.body = Body(body) - } + fun body(body: DslProperty) = Body(body) - fun multipart(multipart: Map) { - this.multipart = Multipart(multipart.toDslProperties()) - } + fun body(body: Any) = Body(body) - fun multipart(multipart: List) { - this.multipart = Multipart(multipart.toDslProperties()) - } + fun multipart(multipart: Map) = Multipart(multipart.toDslProperties()) - fun multipart(multipart: DslProperty) { - this.multipart = Multipart(multipart) - } + fun multipart(multipart: List) = Multipart(multipart.toDslProperties()) - fun multipart(multipart: Any) { - this.multipart = Multipart(multipart) - } + fun multipart(multipart: DslProperty) = Multipart(multipart) + + fun multipart(multipart: Any) = Multipart(multipart) fun bodyMatchers(block: BodyMatchers.() -> Unit) { bodyMatchers = BodyMatchers().apply(block) 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 2f5d06b189..e9816fc999 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 @@ -36,45 +36,29 @@ class ResponseDsl : CommonDsl(), RegexCreatingProperty { var async: Boolean = false var bodyMatchers: ResponseBodyMatchers? = null - fun status(code: Int) { - this.status = DslProperty(code) - } + fun code(code: Int): DslProperty = code.toDslProperty() - fun fixedDelayMilliseconds(delay: Long) { - this.delay = delay.toDslProperty() - } + fun fixedMilliseconds(delay: Long) = delay.toDslProperty() fun headers(headers: Headers.() -> Unit) { - this.headers = Headers().apply(headers) + this.headers = Response.ResponseHeaders().apply(headers) } fun cookies(cookies: Cookies.() -> Unit) { - this.cookies = Cookies().apply(cookies) + this.cookies = Response.ResponseCookies().apply(cookies) } - fun body(body: Map) { - this.body = Body(body.toDslProperties()) - } + fun body(body: Map) = Body(body.toDslProperties()) - fun body(vararg body: Pair) { - this.body = Body(body.toMap().toDslProperties()) - } + fun body(vararg body: Pair) = Body(body.toMap().toDslProperties()) - fun body(body: Pair) { - this.body = Body(mapOf(body).toDslProperties()) - } + fun body(body: Pair) = Body(mapOf(body).toDslProperties()) - fun body(body: List) { - this.body = Body(body.toDslProperties()) - } + fun body(body: List) = Body(body.toDslProperties()) - fun body(body: DslProperty) { - this.body = Body(body.toDslProperty()) - } + fun body(body: DslProperty) = Body(body.toDslProperty()) - fun body(body: Any) { - this.body = Body(body) - } + fun body(body: Any) = Body(body) fun bodyMatchers(bodyMatchers: ResponseBodyMatchers.() -> Unit) { this.bodyMatchers = ResponseBodyMatchers().apply(bodyMatchers) @@ -124,6 +108,9 @@ 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() + override fun anyAlphaUnicode() = delegate.anyAlphaUnicode() override fun anyAlphaNumeric() = delegate.anyAlphaNumeric() 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 37f195eccf..f7686d7008 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 @@ -24,8 +24,6 @@ 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 import org.springframework.cloud.contract.spec.internal.RegexProperty -import org.springframework.cloud.contract.spec.internal.Url -import org.springframework.cloud.contract.spec.internal.UrlPath /** * Tests written based on the Java contract tests written in Groovy @@ -38,19 +36,19 @@ class ContractTests { fun `should work for http`() { val contract = contract { request { - url = Url("/foo") - method(HttpMethods.HttpMethod.PUT) + url = url("/foo") + method = method(HttpMethods.HttpMethod.PUT) headers { header("foo", "bar") } - body("foo" to "bar") + body = body("foo" to "bar") } response { - status(200) + status = code(200) headers { header("foo2", "bar") } - body("foo2" to "bar") + body = body("foo2" to "bar") } } @@ -87,10 +85,10 @@ class ContractTests { fun `should fail when no method is present`() { val contract = contract { request { - url = Url("/foo") + url = url("/foo") } response { - status(200) + status = code(200) } } @@ -105,10 +103,10 @@ class ContractTests { fun `should fail when no url is present`() { val contract = contract { request { - method("GET") + method = method("GET") } response { - status(200) + status = code(200) } } @@ -123,8 +121,8 @@ class ContractTests { fun `should fail when no status is present`() { val contract = contract { request { - url = Url("/foo") - method("GET") + url = url("/foo") + method = method("GET") } response { } @@ -264,14 +262,14 @@ then: fun `should make equals and hashcode work properly for URL`() { val a: Contract = contract { request { - method("GET") - url = Url("/1") + method = method("GET") + url = url("/1") } } val b: Contract = contract { request { - method("GET") - url = Url("/1") + method = method("GET") + url = url("/1") } } @@ -287,14 +285,14 @@ then: fun `should make equals and hashcode work properly for URL with consumer producer`() { val a: Contract = contract { request { - method("GET") - url = Url(value(c("/1"), p("/1"))) + method = method("GET") + url = url(value(c("/1"), p("/1"))) } } val b: Contract = contract { request { - method("GET") - url = Url(value(c("/1"), p("/1"))) + method = method("GET") + url = url(value(c("/1"), p("/1"))) } } @@ -311,26 +309,26 @@ then: val index = 1 val a: Contract = contract { request { - method(HttpMethods.HttpMethod.PUT) + method = method(HttpMethods.HttpMethod.PUT) headers { contentType(applicationJson()) } - url = Url("/$index") + url = url("/$index") } response { - status(HttpStatus.OK()) + status = code(HttpStatus.OK()) } } val b: Contract = contract { request { - method(HttpMethods.HttpMethod.PUT) + method = method(HttpMethods.HttpMethod.PUT) headers { contentType(applicationJson()) } - url = Url("/$index") + url = url("/$index") } response { - status(HttpStatus.OK()) + status = code(HttpStatus.OK()) } } @@ -347,27 +345,27 @@ then: var index = 1 val a: Contract = contract { request { - method(HttpMethods.HttpMethod.PUT) + method = method(HttpMethods.HttpMethod.PUT) headers { contentType(applicationJson()) } - url = Url("/$index") + url = url("/$index") } response { - status(HttpStatus.OK()) + status = code(HttpStatus.OK()) } } index = 2 val b: Contract = contract { request { - method(HttpMethods.HttpMethod.PUT) + method = method(HttpMethods.HttpMethod.PUT) headers { contentType(applicationJson()) } - url = Url("/$index") + url = url("/$index") } response { - status(HttpStatus.OK()) + status = code(HttpStatus.OK()) } } @@ -383,8 +381,8 @@ then: fun `should return true when comparing two equal complex contracts`() { val a: Contract = contract { request { - method(HttpMethods.HttpMethod.GET) - url = Url("/path") + method = method(HttpMethods.HttpMethod.GET) + url = url("/path") headers { header("Accept", value( consumer(regex("text/.*")), @@ -397,8 +395,8 @@ then: } } response { - status(HttpStatus.OK()) - body("id" to mapOf("value" to "132"), + status = code(HttpStatus.OK()) + body = body("id" to mapOf("value" to "132"), "surname" to "Kowalsky", "name" to "Jan", "created" to "2014-02-02 12:23:43" @@ -410,8 +408,8 @@ then: } val b: Contract = contract { request { - method(HttpMethods.HttpMethod.GET) - url = Url("/path") + method = method(HttpMethods.HttpMethod.GET) + url = url("/path") headers { header("Accept", value( consumer(regex("text/.*")), @@ -424,8 +422,8 @@ then: } } response { - status(HttpStatus.OK()) - body("id" to mapOf("value" to "132"), + status = code(HttpStatus.OK()) + body = body("id" to mapOf("value" to "132"), "surname" to "Kowalsky", "name" to "Jan", "created" to "2014-02-02 12:23:43" @@ -482,15 +480,15 @@ then: fun `should support bodyMatchers`() { val contract = contract { request { - method(HttpMethods.HttpMethod.GET) - url = Url("/path") + method = method(HttpMethods.HttpMethod.GET) + url = url("/path") body("id" to mapOf("value" to "132")) bodyMatchers { jsonPath("$.id.value", byRegex(anInteger())) } } response { - status(HttpStatus.OK()) + status = code(HttpStatus.OK()) body("id" to mapOf("value" to "132"), "surname" to "Kowalsky", "name" to "Jan", @@ -526,13 +524,13 @@ then: fun `should support query parameters for url`() { val contract = contract { request { - method(HttpMethods.HttpMethod.GET) - url = Url("/path") withQueryParameters { + method = method(HttpMethods.HttpMethod.GET) + url = url("/path") withQueryParameters { parameter("foo", "bar") } } response { - status(HttpStatus.OK()) + status = code(HttpStatus.OK()) } } @@ -559,13 +557,13 @@ then: fun `should support query parameters for url path`() { val contract = contract { request { - method(HttpMethods.HttpMethod.GET) - urlPath = UrlPath("/path") withQueryParameters { + method = method(HttpMethods.HttpMethod.GET) + urlPath = path("/path") withQueryParameters { parameter("foo", "bar") } } response { - status(HttpStatus.OK()) + status = code(HttpStatus.OK()) } } @@ -592,13 +590,13 @@ then: fun `should work with list as body`() { val contract = contract { request { - method(HttpMethods.HttpMethod.PUT) - url = Url("/path") - body(listOf("foo", "bar")) + method = method(HttpMethods.HttpMethod.PUT) + url = url("/path") + body = body(listOf("foo", "bar")) } response { - status(HttpStatus.OK()) - body(listOf("foo2", "bar2")) + status = code(HttpStatus.OK()) + body = body(listOf("foo2", "bar2")) } } diff --git a/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/internal/KotlinContractConverterTest.kt b/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/internal/KotlinContractConverterTest.kt index 4d2cd56536..10382879d8 100644 --- a/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/internal/KotlinContractConverterTest.kt +++ b/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/internal/KotlinContractConverterTest.kt @@ -16,7 +16,8 @@ package org.springframework.cloud.contract.spec.internal -import org.junit.Test +import org.junit.jupiter.api.Test +import org.springframework.cloud.contract.spec.Contract import java.io.File import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -42,5 +43,13 @@ class KotlinContractConverterTest { assertEquals(2, contracts.size) } + @Test + fun `should work with binary payload`() { + val converter = KotlinContractConverter() + val contracts = converter.convertFrom(file("contracts/shouldWorkWithBinaryPayload.kts")) + assertEquals(1, contracts.size) + Contract.assertContract(contracts.elementAt(0)) + } + private fun file(filename: String) = File(javaClass.classLoader.getResource(filename)!!.toURI()) } \ No newline at end of file diff --git a/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/multipleDefinitions.kts b/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/multipleDefinitions.kts index 8838386cd8..39f8977658 100644 --- a/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/multipleDefinitions.kts +++ b/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/multipleDefinitions.kts @@ -25,7 +25,7 @@ arrayOf( url("/frauds") } response { - status(200) + code(200) body("count" to 200) headers { contentType("application/vnd.fraud.v1+json") @@ -38,7 +38,7 @@ arrayOf( url("/drunks") } response { - status(200) + code(200) body("count" to 100) headers { contentType("application/vnd.fraud.v1+json") diff --git a/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/request.pdf b/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/request.pdf new file mode 100644 index 0000000000..69503635d8 Binary files /dev/null and b/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/request.pdf differ diff --git a/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/response.pdf b/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/response.pdf new file mode 100644 index 0000000000..c3b233056b Binary files /dev/null and b/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/response.pdf differ diff --git a/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/shouldWorkWithBinaryPayload.kts b/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/shouldWorkWithBinaryPayload.kts new file mode 100644 index 0000000000..19664fd291 --- /dev/null +++ b/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/shouldWorkWithBinaryPayload.kts @@ -0,0 +1,38 @@ +/* + * 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 contracts + +import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract +import org.springframework.cloud.contract.spec.internal.HttpMethods + +contract { + request { + url = url("/1") + method = method(HttpMethods.PUT()) + headers { + contentType(applicationOctetStream()) + } + body = fileAsBytes("contracts/request.pdf") + } + response { + status = code(200) + body = fileAsBytes("contracts/response.pdf") + headers { + contentType(applicationOctetStream()) + } + } +} diff --git a/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/singleDefinition.kts b/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/singleDefinition.kts index f3047ff2c7..65bcb44039 100644 --- a/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/singleDefinition.kts +++ b/specs/spring-cloud-contract-spec-kotlin/src/test/resources/contracts/singleDefinition.kts @@ -31,7 +31,7 @@ contract { } response { - status(200) + code(200) body( "fraudCheckStatus" to "OK", "rejectionReason" to listOf(value(consumer(null), producer("assertThatRejectionReasonIsNull(\$it)")))