diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy index 1798ad43c9..2ba9b6059f 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Headers.groovy @@ -16,11 +16,13 @@ package org.springframework.cloud.contract.spec.internal +import java.util.regex.Pattern + import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import groovy.transform.TypeChecked - -import java.util.regex.Pattern +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType /** * Represents a set of headers of a request / response or a message @@ -80,27 +82,34 @@ class Headers { } /** - * Converts the headers into their stub side representations and returns as - * a map of String key => Object value. + * Converts the headers via the provided closure */ - Map asStubSideMap() { + Map asMap(@ClosureParams(value = SimpleType.class, + options = [ + "java.lang.String", + "org.springframework.cloud.contract.spec.internal.DslProperty" + ]) Closure closure) { def acc = [:].withDefault { [] as Collection } return entries.inject(acc as Map) { Map map, Header header -> - map[header.name] = header.clientValue + map[header.name] = closure(header.name, header) return map } as Map } + /** + * Converts the headers into their stub side representations and returns as + * a map of String key => Object value. + */ + Map asStubSideMap() { + return asMap({ String headerName, DslProperty prop -> prop.clientValue }) + } + /** * Converts the headers into their stub side representations and returns as * a map of String key => Object value. */ Map asTestSideMap() { - def acc = [:].withDefault { [] as Collection } - return entries.inject(acc as Map) { Map map, Header header -> - map[header.name] = header.serverValue - return map - } as Map + return asMap({ String headerName, DslProperty prop -> prop.serverValue }) } boolean equals(o) { 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 8b30d8ea5a..176c75351b 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 @@ -17,6 +17,8 @@ package org.springframework.cloud.contract.verifier.converter import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString /** * YAML representation of a {@link org.springframework.cloud.contract.spec.Contract} @@ -38,6 +40,8 @@ class YamlContract { public boolean ignored @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class Request { public String method public String url @@ -52,12 +56,16 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class Multipart { public Map params = [:] public List named = [] } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class Named { public String paramName public String fileName @@ -65,7 +73,10 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class StubMatchers { + // TODO: Add URL matching public List body = [] public List headers = [] public List cookies = [] @@ -73,6 +84,8 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class BodyStubMatcher { public String path public StubMatcherType type @@ -81,12 +94,16 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class MultipartStubMatcher { public List params = [] public List named = [] } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class MultipartNamedStubMatcher { public String paramName public ValueMatcher fileName @@ -94,12 +111,16 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class ValueMatcher { public String regex public PredefinedRegex predefined } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class BodyTestMatcher { public String path public TestMatcherType type @@ -110,6 +131,8 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class KeyValueMatcher { public String key public String regex @@ -117,6 +140,8 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class TestHeaderMatcher { public String key public String regex @@ -125,6 +150,8 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class TestCookieMatcher { public String key public String regex @@ -150,6 +177,8 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class Response { public int status public Map headers = [:] @@ -161,6 +190,8 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class TestMatchers { public List body = [] public List headers = [] @@ -168,6 +199,8 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class Input { public String messageFrom public String triggeredBy @@ -179,6 +212,8 @@ class YamlContract { } @CompileStatic + @ToString(includeFields = true) + @EqualsAndHashCode static class OutputMessage { public String sentTo public Map headers = [:] 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 042ff77db4..b26ee19a3b 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 @@ -503,31 +503,34 @@ class YamlContractConverter implements ContractConverter> { List convertTo(Collection contracts) { return contracts.collect { Contract contract -> YamlContract yamlContract = new YamlContract() + yamlContract.description = contract.description if (contract?.request?.method) { yamlContract.request = new Request() - yamlContract.request.with { - method = contract?.request?.method?.clientValue - url = contract?.request?.url?.clientValue - headers = (contract?.request?.headers as Headers)?.asTestSideMap() - cookies = (contract?.request?.cookies as Cookies)?.asTestSideMap() - body = MapConverter.getTestSideValues(contract?.request?.body) - matchers = new StubMatchers() + yamlContract.request.with { Request request -> + request.method = contract?.request?.method?.clientValue + request.url = contract?.request?.url?.clientValue + request.urlPath = contract?.request?.urlPath?.clientValue + request.headers = (contract?.request?.headers as Headers)?.asTestSideMap() + request.cookies = (contract?.request?.cookies as Cookies)?.asTestSideMap() + request.body = MapConverter.getTestSideValues(contract?.request?.body) + request.matchers = new StubMatchers() contract?.request?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> - matchers.body << new BodyStubMatcher( + request.matchers.body << new BodyStubMatcher( path: matcher.path(), type: stubMatcherType(matcher.matchingType()), value: matcher.value()?.toString() ) } + setInputHeaders(contract?.request?.headers as Headers, yamlContract.request.matchers.headers) } yamlContract.response = new Response() - yamlContract.response.with { - status = contract?.response?.status?.clientValue as Integer - headers = (contract?.response?.headers as Headers)?.asStubSideMap() - cookies = (contract?.response?.cookies as Cookies)?.asStubSideMap() - body = MapConverter.getStubSideValues(contract?.response?.body) + yamlContract.response.with { Response response -> + response.status = contract?.response?.status?.clientValue as Integer + response.headers = (contract?.response?.headers as Headers)?.asStubSideMap() + response.cookies = (contract?.response?.cookies as Cookies)?.asStubSideMap() + response.body = MapConverter.getStubSideValues(contract?.response?.body) contract?.response?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> - matchers.body << new BodyTestMatcher( + response.matchers.body << new BodyTestMatcher( path: matcher.path(), type: testMatcherType(matcher.matchingType()), value: matcher.value()?.toString(), @@ -535,45 +538,73 @@ class YamlContractConverter implements ContractConverter> { maxOccurrence: matcher.maxTypeOccurrence() ) } + setOutputHeaders(contract?.response?.headers, yamlContract.response.matchers.headers) } } + yamlContract.label = contract.label if (contract.input) { yamlContract.input = new Input() - yamlContract.input.assertThat = contract?.input?.assertThat?.toString() - yamlContract.input.triggeredBy = contract?.input?.triggeredBy?.toString() + yamlContract.input.assertThat = MapConverter.getTestSideValues(contract?.input?.assertThat?.toString()) + yamlContract.input.triggeredBy = MapConverter.getTestSideValues(contract?.input?.triggeredBy?.toString()) yamlContract.input.messageHeaders = (contract?.input?.messageHeaders as Headers)?.asTestSideMap() yamlContract.input.messageBody = MapConverter.getTestSideValues(contract?.input?.messageBody) - yamlContract.input.messageFrom = contract?.input?.messageFrom?.serverValue - yamlContract.input.matchers.body.each { - contract?.input?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> - yamlContract.input.matchers.body << new BodyStubMatcher( - path: matcher.path(), - type: stubMatcherType(matcher.matchingType()), - value: matcher.value()?.toString() - ) - } + yamlContract.input.messageFrom = MapConverter.getTestSideValues(contract?.input?.messageFrom) + contract?.input?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> + yamlContract.input.matchers.body << new BodyStubMatcher( + path: matcher.path(), + type: stubMatcherType(matcher.matchingType()), + value: matcher.value()?.toString() + ) } + setInputHeaders(contract?.input?.messageHeaders as Headers, yamlContract.input.matchers.headers) } if (contract.outputMessage) { yamlContract.outputMessage = new OutputMessage() + yamlContract.outputMessage.sentTo = MapConverter.getStubSideValues(contract.outputMessage.sentTo) yamlContract.outputMessage.headers = (contract?.outputMessage?.headers as Headers)?.asStubSideMap() yamlContract.outputMessage.body = MapConverter.getStubSideValues(contract?.outputMessage?.body) - yamlContract.outputMessage.matchers.body.each { - contract?.input?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> - yamlContract.outputMessage.matchers.body << new BodyTestMatcher( - path: matcher.path(), - type: testMatcherType(matcher.matchingType()), - value: matcher.value()?.toString(), - minOccurrence: matcher.minTypeOccurrence(), - maxOccurrence: matcher.maxTypeOccurrence() - ) - } + contract?.outputMessage?.bodyMatchers?.jsonPathMatchers()?.each { BodyMatcher matcher -> + yamlContract.outputMessage.matchers.body << new BodyTestMatcher( + path: matcher.path(), + type: testMatcherType(matcher.matchingType()), + value: matcher.value()?.toString(), + minOccurrence: matcher.minTypeOccurrence(), + maxOccurrence: matcher.maxTypeOccurrence() + ) } + setOutputHeaders(contract?.outputMessage?.headers, yamlContract.outputMessage.matchers.headers) } return yamlContract } } + protected void setInputHeaders(Headers headers, List headerMatchers) { + headers?.asStubSideMap()?.each { String key, Object value -> + if (value instanceof Pattern) { + headerMatchers << new KeyValueMatcher( + key: key, + regex: value.pattern(), + ) + } + } + } + + protected void setOutputHeaders(Headers headers, List headerMatchers) { + headers?.asTestSideMap()?.each { String key, Object value -> + if (value instanceof Pattern) { + headerMatchers << new TestHeaderMatcher( + key: key, + regex: value.pattern(), + ) + } else if (value instanceof ExecutionProperty) { + headerMatchers << new TestHeaderMatcher( + key: key, + command: value.executionCommand, + ) + } + } + } + protected TestMatcherType testMatcherType(MatchingType matchingType) { switch (matchingType) { case MatchingType.EQUALITY: diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/DslToYamlContractConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/DslToYamlContractConverterSpec.groovy new file mode 100644 index 0000000000..998474e4d4 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/DslToYamlContractConverterSpec.groovy @@ -0,0 +1,328 @@ +/* + * Copyright 2013-2018 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.verifier.converter + + +import spock.lang.Specification + +import org.springframework.cloud.contract.spec.Contract + +/** + * @author Marcin Grzejszczak + * @author Tim Ysewyn + */ +class DslToYamlContractConverterSpec extends Specification { + + YamlContractConverter converter = new YamlContractConverter() + + def "should convert rest DSL to YAML"() { + given: + List contracts = [Contract.make { + request { + method 'GET' + urlPath '/get' + body([ + duck: 123, + alpha: "abc", + number: 123, + aBoolean: true, + date: "2017-01-01", + dateTime: "2017-01-01T01:23:45", + time: "01:02:34", + valueWithoutAMatcher: "foo", + valueWithTypeMatch: "string", + key: [ + 'complex.key' : 'foo' + ] + ]) + bodyMatchers { + jsonPath('$.duck', byRegex("[0-9]{3}")) + jsonPath('$.duck', byEquality()) + jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.alpha', byEquality()) + jsonPath('$.number', byRegex(number())) + jsonPath('$.aBoolean', byRegex(anyBoolean())) + jsonPath('$.date', byDate()) + jsonPath('$.dateTime', byTimestamp()) + jsonPath('$.time', byTime()) + jsonPath("\$.['key'].['complex.key']", byEquality()) + } + headers { + header("sample", $(c(regex('foo.*')), p('foo'))) + header("Content-Type", applicationJson()) + } + } + response { + status OK() + body([ + duck: 123, + alpha: "abc", + number: 123, + positiveInteger: 1234567890, + negativeInteger: -1234567890, + positiveDecimalNumber: 123.4567890, + negativeDecimalNumber: -123.4567890, + aBoolean: true, + date: "2017-01-01", + dateTime: "2017-01-01T01:23:45", + time: "01:02:34", + valueWithoutAMatcher: "foo", + valueWithTypeMatch: "string", + valueWithMin: [ + 1,2,3 + ], + valueWithMax: [ + 1,2,3 + ], + valueWithMinMax: [ + 1,2,3 + ], + valueWithMinEmpty: [], + valueWithMaxEmpty: [], + key: [ + 'complex.key' : 'foo' + ], + nullValue: null + ]) + bodyMatchers { + // asserts the jsonpath value against manual regex + jsonPath('$.duck', byRegex("[0-9]{3}")) + // asserts the jsonpath value against the provided value + jsonPath('$.duck', byEquality()) + // asserts the jsonpath value against some default regex + jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.alpha', byEquality()) + jsonPath('$.number', byRegex(number())) + jsonPath('$.positiveInteger', byRegex(positiveInt())) + jsonPath('$.integer', byRegex(anInteger())) + jsonPath('$.double', byRegex(aDouble())) + jsonPath('$.aBoolean', byRegex(anyBoolean())) + // asserts vs inbuilt time related regex + jsonPath('$.date', byDate()) + jsonPath('$.dateTime', byTimestamp()) + jsonPath('$.time', byTime()) + // asserts that the resulting type is the same as in response body + jsonPath('$.valueWithTypeMatch', byType()) + jsonPath('$.valueWithMin', byType { + // results in verification of size of array (min 1) + minOccurrence(1) + }) + jsonPath('$.valueWithMax', byType { + // results in verification of size of array (max 3) + maxOccurrence(3) + }) + jsonPath('$.valueWithMinMax', byType { + // results in verification of size of array (min 1 & max 3) + minOccurrence(1) + maxOccurrence(3) + }) + jsonPath('$.valueWithMinEmpty', byType { + // results in verification of size of array (min 0) + minOccurrence(0) + }) + jsonPath('$.valueWithMaxEmpty', byType { + // results in verification of size of array (max 0) + maxOccurrence(0) + }) + // will execute a method `assertThatValueIsANumber` + jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)')) + jsonPath("\$.['key'].['complex.key']", byEquality()) + jsonPath('$.nullValue', byNull()) + } + headers { + contentType(applicationJson()) + header('Some-Header', $(c('someValue'), p(regex('[a-zA-Z]{9}')))) + } + } + }] + when: + Collection yamlContracts = converter.convertTo(contracts) + then: + yamlContracts.size() == 1 + YamlContract yamlContract = yamlContracts.first() + yamlContract.request.method == "GET" + yamlContract.request.urlPath == "/get" + yamlContract.request.body == [ + duck: 123, + alpha: "abc", + number: 123, + aBoolean: true, + date: "2017-01-01", + dateTime: "2017-01-01T01:23:45", + time: "01:02:34", + valueWithoutAMatcher: "foo", + valueWithTypeMatch: "string", + key: ["complex.key": 'foo'] + ] + yamlContract.request.headers == [ + sample: 'foo', + "Content-Type": "application/json" + ] + yamlContract.request.matchers.headers == [ + new YamlContract.KeyValueMatcher( + key: "sample", regex: "foo.*") + ] + yamlContract.request.matchers.body == [ + new YamlContract.BodyStubMatcher( + path: '$.duck', + type: YamlContract.StubMatcherType.by_regex, + value: "[0-9]{3}"), + new YamlContract.BodyStubMatcher( + path: '$.duck', + type: YamlContract.StubMatcherType.by_equality), + new YamlContract.BodyStubMatcher( + path: '$.alpha', + type: YamlContract.StubMatcherType.by_regex, + predefined: YamlContract.PredefinedRegex.only_alpha_unicode), + new YamlContract.BodyStubMatcher( + path: '$.alpha', + type: YamlContract.StubMatcherType.by_equality), + new YamlContract.BodyStubMatcher( + path: '$.number', + type: YamlContract.StubMatcherType.by_regex, + predefined: YamlContract.PredefinedRegex.number), + new YamlContract.BodyStubMatcher( + path: '$.aBoolean', + type: YamlContract.StubMatcherType.by_regex, + predefined: YamlContract.PredefinedRegex.any_boolean), + new YamlContract.BodyStubMatcher( + path: '$.date', + type: YamlContract.StubMatcherType.by_date), + new YamlContract.BodyStubMatcher( + path: '$.dateTime', + type: YamlContract.StubMatcherType.by_timestamp), + new YamlContract.BodyStubMatcher( + path: '$.time', + type: YamlContract.StubMatcherType.by_time), + new YamlContract.BodyStubMatcher( + path: "\$.['key'].['complex.key']", + type: YamlContract.StubMatcherType.by_equality), + ] + yamlContract.response.status == 200 + yamlContract.response.body == [duck: 123, + alpha: "abc", + number: 123, + aBoolean: true, + date: "2017-01-01", + dateTime: "2017-01-01T01:23:45", + time: "01:02:34", + positiveInteger: 1234567890, + negativeInteger: -1234567890, + positiveDecimalNumber: 123.4567890, + negativeDecimalNumber: -123.4567890, + valueWithoutAMatcher: "foo", + valueWithTypeMatch: "string", + valueWithMin: [1, 2, 3], + valueWithMax: [1, 2, 3], + valueWithMinMax: [1, 2, 3], + valueWithMinEmpty: [], + valueWithMaxEmpty: [], + key: ['complex.key' : 'foo'], + nullValue: null + ] + yamlContract.response.headers == [ + "Content-Type": "application/json", + "Some-Header": "someValue" + ] + yamlContract.response.matchers.headers == [ + new YamlContract.TestHeaderMatcher( + key: "Content-Type", regex: "application/json.*") + ] + yamlContract.response.matchers.body == [ + new YamlContract.BodyTestMatcher( + path: '$.duck', + type: YamlContract.TestMatcherType.by_regex, + value: "[0-9]{3}"), + new YamlContract.BodyTestMatcher( + path: '$.duck', + type: YamlContract.TestMatcherType.by_equality), + new YamlContract.BodyTestMatcher( + path: '$.alpha', + type: YamlContract.TestMatcherType.by_regex, + value: '[\\p{L}]*'), + new YamlContract.BodyTestMatcher( + path: '$.alpha', + type: YamlContract.TestMatcherType.by_equality), + new YamlContract.BodyTestMatcher( + path: '$.number', + type: YamlContract.TestMatcherType.by_regex, + value: '-?(\\d*\\.\\d+|\\d+)'), + new YamlContract.BodyTestMatcher( + path: '$.positiveInteger', + type: YamlContract.TestMatcherType.by_regex, + value: '([1-9]\\d*)'), + new YamlContract.BodyTestMatcher( + path: '$.integer', + type: YamlContract.TestMatcherType.by_regex, + value: '-?(\\d+)'), + new YamlContract.BodyTestMatcher( + path: '$.double', + type: YamlContract.TestMatcherType.by_regex, + value: '-?(\\d*\\.\\d+)'), + new YamlContract.BodyTestMatcher( + path: '$.aBoolean', + type: YamlContract.TestMatcherType.by_regex, + value: '(true|false)'), + new YamlContract.BodyTestMatcher( + path: '$.date', + type: YamlContract.TestMatcherType.by_regex, + value: '(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])'), + new YamlContract.BodyTestMatcher( + path: '$.dateTime', + type: YamlContract.TestMatcherType.by_regex, + value: '([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])'), + new YamlContract.BodyTestMatcher( + path: '$.time', + type: YamlContract.TestMatcherType.by_regex, + value: '(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])'), + new YamlContract.BodyTestMatcher( + path: '$.valueWithTypeMatch', + type: YamlContract.TestMatcherType.by_type), + new YamlContract.BodyTestMatcher( + path: '$.valueWithMin', + type: YamlContract.TestMatcherType.by_type, + minOccurrence: 1), + new YamlContract.BodyTestMatcher( + path: '$.valueWithMax', + type: YamlContract.TestMatcherType.by_type, + maxOccurrence: 3), + new YamlContract.BodyTestMatcher( + path: '$.valueWithMinMax', + type: YamlContract.TestMatcherType.by_type, + minOccurrence: 1, + maxOccurrence: 3), + new YamlContract.BodyTestMatcher( + path: '$.valueWithMinEmpty', + type: YamlContract.TestMatcherType.by_type, + minOccurrence: 0), + new YamlContract.BodyTestMatcher( + path: '$.valueWithMaxEmpty', + type: YamlContract.TestMatcherType.by_type, + maxOccurrence: 0), + new YamlContract.BodyTestMatcher( + path: '$.duck', + type: YamlContract.TestMatcherType.by_command, + value: 'assertThatValueIsANumber($it)'), + new YamlContract.BodyTestMatcher( + path: "\$.['key'].['complex.key']", + type: YamlContract.TestMatcherType.by_equality), + new YamlContract.BodyTestMatcher( + path: '$.nullValue', + type: YamlContract.TestMatcherType.by_null), + ] + } +} \ No newline at end of file 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 1cedf0dade..5e19620590 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 @@ -519,7 +519,7 @@ class YamlContractConverterSpec extends Specification { } } - def "should convert DSL to YAML"() { + def "should convert HTTP DSL to YAML"() { given: assert converter.isAccepted(ymlWithRest) and: @@ -565,4 +565,421 @@ class YamlContractConverterSpec extends Specification { yamlContract.response.cookies.find { it.key == "foo" && it.value == "client" } yamlContract.response.cookies.find { it.key == "bar" && it.value == "client" } } -} + + def "should convert multiple messaging DSLs to YAML"() { + given: + assert converter.isAccepted(ymlMessagingMethod) + and: + List contracts = [Contract.make { + input { + description("Some description") + label("some_label") + triggeredBy("bookReturnedTriggered()") + } + outputMessage { + sentTo("output") + body([bookName: "foo"]) + headers { + header("BOOK-NAME", "foo") + } + } + },Contract.make { + input { + description("Some description2") + label("some_label2") + triggeredBy("bookReturnedTriggered()2") + } + outputMessage { + sentTo("output2") + body([bookName2: "foo"]) + headers { + header("BOOK-NAME2", "foo") + } + } + }] + when: + Collection yamlContracts = converter.convertTo(contracts) + then: + yamlContracts.size() == 2 + YamlContract yamlContract = yamlContracts.first() + yamlContract.description == "Some description" + yamlContract.label == "some_label" + yamlContract.input.triggeredBy == "bookReturnedTriggered()" + yamlContract.outputMessage.sentTo == "output" + yamlContract.outputMessage.body == [bookName: "foo"] + yamlContract.outputMessage.headers == ["BOOK-NAME": "foo"] + and: + YamlContract yamlContract2 = yamlContracts.last() + yamlContract2.description == "Some description2" + yamlContract2.label == "some_label2" + yamlContract2.input.triggeredBy == "bookReturnedTriggered()2" + yamlContract2.outputMessage.sentTo == "output2" + yamlContract2.outputMessage.body == [bookName2: "foo"] + yamlContract2.outputMessage.headers == ["BOOK-NAME2": "foo"] + } + + def "should convert Messaging DSL with input triggered by method to YAML"() { + given: + assert converter.isAccepted(ymlMessagingMethod) + and: + List contracts = [Contract.make { + input { + description("Some description") + label("some_label") + triggeredBy("bookReturnedTriggered()") + } + outputMessage { + sentTo("output") + body([bookName: "foo"]) + headers { + header("BOOK-NAME", "foo") + } + } + }] + when: + Collection yamlContracts = converter.convertTo(contracts) + then: + yamlContracts.size() == 1 + YamlContract yamlContract = yamlContracts.first() + yamlContract.description == "Some description" + yamlContract.label == "some_label" + yamlContract.input.triggeredBy == "bookReturnedTriggered()" + yamlContract.outputMessage.sentTo == "output" + yamlContract.outputMessage.body == [bookName: "foo"] + yamlContract.outputMessage.headers == ["BOOK-NAME": "foo"] + } + + def "should convert Messaging DSL with input and output message to YAML"() { + given: + List contracts = [Contract.make { + input { + messageFrom("jms:input") + messageBody([bookName: 'foo']) + messageHeaders { + header("sample", "header") + } + } + outputMessage { + sentTo("output") + body([bookName: "foo"]) + headers { + header("BOOK-NAME", "foo") + } + } + }] + when: + Collection yamlContracts = converter.convertTo(contracts) + then: + yamlContracts.size() == 1 + YamlContract yamlContract = yamlContracts.first() + yamlContract.input.messageFrom == "jms:input" + yamlContract.input.messageBody == [bookName: 'foo'] + yamlContract.input.messageHeaders == ["sample": "header"] + yamlContract.outputMessage.sentTo == "output" + yamlContract.outputMessage.body == [bookName: "foo"] + yamlContract.outputMessage.headers == ["BOOK-NAME": "foo"] + } + + def "should convert Messaging DSL with only input message to YAML"() { + given: + List contracts = [Contract.make { + input { + messageFrom("jms:input") + messageBody([bookName: 'foo']) + messageHeaders { + header("sample", "header") + } + assertThat("bookWasDeleted()") + } + }] + when: + Collection yamlContracts = converter.convertTo(contracts) + then: + yamlContracts.size() == 1 + YamlContract yamlContract = yamlContracts.first() + yamlContract.input.messageFrom == "jms:input" + yamlContract.input.messageBody == [bookName: 'foo'] + yamlContract.input.messageHeaders == ["sample": "header"] + yamlContract.input.assertThat == "bookWasDeleted()" + } + + def "should convert Messaging with a message DSL to YAML"() { + given: + assert converter.isAccepted(ymlMessagingMatchers) + and: + List contracts = [Contract.make { + label("card_rejected") + input { + messageFrom("input") + messageBody([ + duck: 123, + alpha: "abc", + number: 123, + aBoolean: true, + date: "2017-01-01", + dateTime: "2017-01-01T01:23:45", + time: "01:02:34", + valueWithoutAMatcher: "foo", + valueWithTypeMatch: "string", + key: ["complex.key": 'foo'] + ]) + bodyMatchers { + jsonPath('$.duck', byRegex("[0-9]{3}")) + jsonPath('$.duck', byEquality()) + jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.alpha', byEquality()) + jsonPath('$.number', byRegex(number())) + jsonPath('$.aBoolean', byRegex(anyBoolean())) + jsonPath('$.date', byDate()) + jsonPath('$.dateTime', byTimestamp()) + jsonPath('$.time', byTime()) + jsonPath("\$.['key'].['complex.key']", byEquality()) + } + messageHeaders { + header("sample", $(c(regex("foo.*")), p("foo"))) + messagingContentType(applicationJson()) + } + } + outputMessage { + sentTo("channel") + body([duck: 123, + alpha: "abc", + number: 123, + aBoolean: true, + date: "2017-01-01", + dateTime: "2017-01-01T01:23:45", + time: "01:02:34", + valueWithoutAMatcher: "foo", + valueWithTypeMatch: "string", + valueWithMin: [1, 2, 3], + valueWithMax: [1, 2, 3], + valueWithMinMax: [1, 2, 3], + valueWithMinEmpty: [], + valueWithMaxEmpty: [], + key: ['complex.key' : 'foo'], + nullValue: null + ]) + bodyMatchers { + // asserts the jsonpath value against manual regex + jsonPath('$.duck', byRegex("[0-9]{3}")) + // asserts the jsonpath value against the provided value + jsonPath('$.duck', byEquality()) + // asserts the jsonpath value against some default regex + jsonPath('$.alpha', byRegex(onlyAlphaUnicode())) + jsonPath('$.alpha', byEquality()) + jsonPath('$.number', byRegex(number())) + jsonPath('$.positiveInteger', byRegex(positiveInt())) + jsonPath('$.integer', byRegex(anInteger())) + jsonPath('$.double', byRegex(aDouble())) + jsonPath('$.aBoolean', byRegex(anyBoolean())) + // asserts vs inbuilt time related regex + jsonPath('$.date', byDate()) + jsonPath('$.dateTime', byTimestamp()) + jsonPath('$.time', byTime()) + // asserts that the resulting type is the same as in response body + jsonPath('$.valueWithTypeMatch', byType()) + jsonPath('$.valueWithMin', byType { + // results in verification of size of array (min 1) + minOccurrence(1) + }) + jsonPath('$.valueWithMax', byType { + // results in verification of size of array (max 3) + maxOccurrence(3) + }) + jsonPath('$.valueWithMinMax', byType { + // results in verification of size of array (min 1 & max 3) + minOccurrence(1) + maxOccurrence(3) + }) + jsonPath('$.valueWithMinEmpty', byType { + // results in verification of size of array (min 0) + minOccurrence(0) + }) + jsonPath('$.valueWithMaxEmpty', byType { + // results in verification of size of array (max 0) + maxOccurrence(0) + }) + // will execute a method `assertThatValueIsANumber` + jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)')) + jsonPath("\$.['key'].['complex.key']", byEquality()) + jsonPath('$.nullValue', byNull()) + } + headers { + messagingContentType(applicationJson()) + header('Some-Header', $(c('someValue'), p(regex('[a-zA-Z]{9}')))) + } + } + }] + when: + Collection yamlContracts = converter.convertTo(contracts) + then: + yamlContracts.size() == 1 + YamlContract yamlContract = yamlContracts.first() + yamlContract.label == "card_rejected" + yamlContract.input.messageFrom == "input" + yamlContract.input.messageBody == [ + duck: 123, + alpha: "abc", + number: 123, + aBoolean: true, + date: "2017-01-01", + dateTime: "2017-01-01T01:23:45", + time: "01:02:34", + valueWithoutAMatcher: "foo", + valueWithTypeMatch: "string", + key: ["complex.key": 'foo'] + ] + yamlContract.input.messageHeaders == [ + sample: 'foo', + contentType: "application/json" + ] + yamlContract.input.matchers.headers == [ + new YamlContract.KeyValueMatcher( + key: "sample", regex: "foo.*") + ] + yamlContract.input.matchers.body == [ + new YamlContract.BodyStubMatcher( + path: '$.duck', + type: YamlContract.StubMatcherType.by_regex, + value: "[0-9]{3}"), + new YamlContract.BodyStubMatcher( + path: '$.duck', + type: YamlContract.StubMatcherType.by_equality), + new YamlContract.BodyStubMatcher( + path: '$.alpha', + type: YamlContract.StubMatcherType.by_regex, + predefined: YamlContract.PredefinedRegex.only_alpha_unicode), + new YamlContract.BodyStubMatcher( + path: '$.alpha', + type: YamlContract.StubMatcherType.by_equality), + new YamlContract.BodyStubMatcher( + path: '$.number', + type: YamlContract.StubMatcherType.by_regex, + predefined: YamlContract.PredefinedRegex.number), + new YamlContract.BodyStubMatcher( + path: '$.aBoolean', + type: YamlContract.StubMatcherType.by_regex, + predefined: YamlContract.PredefinedRegex.any_boolean), + new YamlContract.BodyStubMatcher( + path: '$.date', + type: YamlContract.StubMatcherType.by_date), + new YamlContract.BodyStubMatcher( + path: '$.dateTime', + type: YamlContract.StubMatcherType.by_timestamp), + new YamlContract.BodyStubMatcher( + path: '$.time', + type: YamlContract.StubMatcherType.by_time), + new YamlContract.BodyStubMatcher( + path: "\$.['key'].['complex.key']", + type: YamlContract.StubMatcherType.by_equality), + ] + yamlContract.outputMessage.sentTo == "channel" + yamlContract.outputMessage.body == [duck: 123, + alpha: "abc", + number: 123, + aBoolean: true, + date: "2017-01-01", + dateTime: "2017-01-01T01:23:45", + time: "01:02:34", + valueWithoutAMatcher: "foo", + valueWithTypeMatch: "string", + valueWithMin: [1, 2, 3], + valueWithMax: [1, 2, 3], + valueWithMinMax: [1, 2, 3], + valueWithMinEmpty: [], + valueWithMaxEmpty: [], + key: ['complex.key' : 'foo'], + nullValue: null + ] + yamlContract.outputMessage.headers == [ + "contentType": "application/json", + "Some-Header": "someValue" + ] + yamlContract.outputMessage.matchers.headers == [ + new YamlContract.TestHeaderMatcher( + key: "Content-Type", regex: "application/json.*") + ] + yamlContract.outputMessage.matchers.body == [ + new YamlContract.BodyTestMatcher( + path: '$.duck', + type: YamlContract.TestMatcherType.by_regex, + value: "[0-9]{3}"), + new YamlContract.BodyTestMatcher( + path: '$.duck', + type: YamlContract.TestMatcherType.by_equality), + new YamlContract.BodyTestMatcher( + path: '$.alpha', + type: YamlContract.TestMatcherType.by_regex, + value: '[\\p{L}]*'), + new YamlContract.BodyTestMatcher( + path: '$.alpha', + type: YamlContract.TestMatcherType.by_equality), + new YamlContract.BodyTestMatcher( + path: '$.number', + type: YamlContract.TestMatcherType.by_regex, + value: '-?(\\d*\\.\\d+|\\d+)'), + new YamlContract.BodyTestMatcher( + path: '$.positiveInteger', + type: YamlContract.TestMatcherType.by_regex, + value: '([1-9]\\d*)'), + new YamlContract.BodyTestMatcher( + path: '$.integer', + type: YamlContract.TestMatcherType.by_regex, + value: '-?(\\d+)'), + new YamlContract.BodyTestMatcher( + path: '$.double', + type: YamlContract.TestMatcherType.by_regex, + value: '-?(\\d*\\.\\d+)'), + new YamlContract.BodyTestMatcher( + path: '$.aBoolean', + type: YamlContract.TestMatcherType.by_regex, + value: '(true|false)'), + new YamlContract.BodyTestMatcher( + path: '$.date', + type: YamlContract.TestMatcherType.by_regex, + value: '(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])'), + new YamlContract.BodyTestMatcher( + path: '$.dateTime', + type: YamlContract.TestMatcherType.by_regex, + value: '([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])'), + new YamlContract.BodyTestMatcher( + path: '$.time', + type: YamlContract.TestMatcherType.by_regex, + value: '(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])'), + new YamlContract.BodyTestMatcher( + path: '$.valueWithTypeMatch', + type: YamlContract.TestMatcherType.by_type), + new YamlContract.BodyTestMatcher( + path: '$.valueWithMin', + type: YamlContract.TestMatcherType.by_type, + minOccurrence: 1), + new YamlContract.BodyTestMatcher( + path: '$.valueWithMax', + type: YamlContract.TestMatcherType.by_type, + maxOccurrence: 3), + new YamlContract.BodyTestMatcher( + path: '$.valueWithMinMax', + type: YamlContract.TestMatcherType.by_type, + minOccurrence: 1, + maxOccurrence: 3), + new YamlContract.BodyTestMatcher( + path: '$.valueWithMinEmpty', + type: YamlContract.TestMatcherType.by_type, + minOccurrence: 0), + new YamlContract.BodyTestMatcher( + path: '$.valueWithMaxEmpty', + type: YamlContract.TestMatcherType.by_type, + maxOccurrence: 0), + new YamlContract.BodyTestMatcher( + path: '$.duck', + type: YamlContract.TestMatcherType.by_command, + value: 'assertThatValueIsANumber($it)'), + new YamlContract.BodyTestMatcher( + path: "\$.['key'].['complex.key']", + type: YamlContract.TestMatcherType.by_equality), + new YamlContract.BodyTestMatcher( + path: '$.nullValue', + type: YamlContract.TestMatcherType.by_null), + ] + } +} \ No newline at end of file