From 898dbcab98510320e3b685a4819a4f266740c946 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 24 Oct 2018 22:31:22 +0200 Subject: [PATCH] Added query parameter matchers for YAML; fixes gh-766 --- .../verifier/converter/YamlToContracts.groovy | 54 ++++++++++++++++--- .../YamlContractConverterSpec.groovy | 29 ++++++++++ .../WireMockResponseStubStrategySpec.groovy | 5 ++ .../test/resources/yml/contract_matchers.yml | 46 ++++++++++++++-- 4 files changed, 123 insertions(+), 11 deletions(-) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy index badf0829c6..554ba0a2e3 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy @@ -1,20 +1,20 @@ package org.springframework.cloud.contract.verifier.converter -import java.nio.file.Files -import java.util.regex.Pattern - import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.yaml.YAMLMapper import groovy.transform.CompileStatic import groovy.transform.PackageScope -import org.yaml.snakeyaml.Yaml - import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.internal.DslProperty import org.springframework.cloud.contract.spec.internal.ExecutionProperty import org.springframework.cloud.contract.spec.internal.MatchingTypeValue import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.RegexPatterns +import org.springframework.cloud.contract.spec.internal.Request +import org.yaml.snakeyaml.Yaml + +import java.nio.file.Files +import java.util.regex.Pattern /** * @author Marcin Grzejszczak @@ -86,10 +86,10 @@ class YamlToContracts { yamlContract.request.queryParameters.each { String key, Object value -> if (value instanceof List) { ((List) value).each { - parameter(key, it) + parameter(key, queryParamValue(yamlContract, key, it)) } } else { - parameter(key, value) + parameter(key, queryParamValue(yamlContract, key, value)) } } } @@ -202,8 +202,15 @@ class YamlToContracts { case YamlContract.StubMatcherType.by_equality: value = byEquality() break + case YamlContract.StubMatcherType.by_null: + // do nothing + break + default: + throw new UnsupportedOperationException("The type [" + matcher.type + "] is unsupported. Hint: If you're using remember to pass ") + } + if (value) { + jsonPath(matcher.path, value) } - jsonPath(matcher.path, value) } } } @@ -282,6 +289,8 @@ class YamlToContracts { case YamlContract.TestMatcherType.by_null: value = byNull() break + default: + throw new UnsupportedOperationException("The type [" + testMatcher.type + "] is unsupported. Hint: If you're using remember to pass ") } if (testMatcher.path) { jsonPath(testMatcher.path, value) @@ -451,6 +460,33 @@ class YamlToContracts { return clientValue } + protected Object queryParamValue(YamlContract yamlContract, String key, Object value) { + Request request = new Request() + YamlContract.QueryParameterMatcher matcher = yamlContract.request. + matchers.queryParameters.find { it.key == key} + if (!matcher) { + return value + } + switch (matcher.type) { + case YamlContract.MatchingType.equal_to: + return new DslProperty(request.equalTo(matcher.value), value) + case YamlContract.MatchingType.containing: + return new DslProperty(request.containing(matcher.value), value) + case YamlContract.MatchingType.matching: + return new DslProperty(request.matching(matcher.value), value) + case YamlContract.MatchingType.not_matching: + return new DslProperty(request.notMatching(matcher.value), value) + case YamlContract.MatchingType.equal_to_json: + return new DslProperty(request.equalToJson(matcher.value), value) + case YamlContract.MatchingType.equal_to_xml: + return new DslProperty(request.equalToXml(matcher.value), value) + case YamlContract.MatchingType.absent: + return new DslProperty(request.absent(), null) + default: + throw new UnsupportedOperationException("The provided matching type [" + matcher + "] is unsupported. Use on of " + YamlContract.MatchingType.values()) + } + } + protected Object serverValue(Object value, YamlContract.KeyValueMatcher matcher) { Object serverValue = value if (matcher?.command) { @@ -499,6 +535,8 @@ class YamlToContracts { return patterns.nonEmpty() case YamlContract.PredefinedRegex.non_blank: return patterns.nonBlank() + default: + throw new UnsupportedOperationException("The predefined regex [" + predefinedRegex + "] is unsupported. Use on of " + YamlContract.PredefinedRegex.values()) } } 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 3becd7cac8..84a11bef6c 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 @@ -16,6 +16,8 @@ package org.springframework.cloud.contract.verifier.converter +import org.springframework.cloud.contract.spec.internal.MatchingStrategy +import org.springframework.cloud.contract.spec.internal.QueryParameters import spock.lang.Issue import java.util.regex.Pattern @@ -187,6 +189,22 @@ class YamlContractConverterSpec extends Specification { RegexPatterns patterns = new RegexPatterns() contract.request.headers.entries.find { it.name == "Content-Type" && ((Pattern) it.clientValue).pattern == "application/json.*" && it.serverValue == "application/json" } + contract.request.urlPath.queryParameters.parameters.size() == 8 + QueryParameters queryParameters = contract.request.urlPath.queryParameters + assertQueryParam(queryParameters, "limit", 10, + MatchingStrategy.Type.EQUAL_TO, 20) + assertQueryParam(queryParameters, "offset", 20, + MatchingStrategy.Type.CONTAINS, 20) + assertQueryParam(queryParameters, "sort", "name", + MatchingStrategy.Type.EQUAL_TO, "name") + assertQueryParam(queryParameters, "search", 55, + MatchingStrategy.Type.NOT_MATCHING, (~/^[0-9]{2}$/).pattern()) + assertQueryParam(queryParameters, "age", 99, + MatchingStrategy.Type.NOT_MATCHING, "^\\\\w*\$") + assertQueryParam(queryParameters, "name", "John.Doe", + MatchingStrategy.Type.MATCHING, "John.*") + assertQueryParam(queryParameters, "hello", true, + MatchingStrategy.Type.ABSENT, null) contract.request.bodyMatchers.jsonPathRegexMatchers[0].path() == '$.duck' contract.request.bodyMatchers.jsonPathRegexMatchers[0].matchingType() == MatchingType.REGEX contract.request.bodyMatchers.jsonPathRegexMatchers[0].value() == '[0-9]{3}' @@ -264,6 +282,17 @@ class YamlContractConverterSpec extends Specification { contract.response.bodyMatchers.jsonPathRegexMatchers[15].value() == new ExecutionProperty('assertThatValueIsANumber($it)') } + protected Object assertQueryParam(QueryParameters queryParameters, String queryParamName, Object serverValue, + MatchingStrategy.Type clientType, Object clientValue) { + if (clientType == MatchingStrategy.Type.ABSENT) { + return ! queryParameters.parameters.find { it.name == queryParamName} + } + return queryParameters.parameters.find { it.name == queryParamName && + it.serverValue == serverValue && + ((MatchingStrategy) it.clientValue).type == clientType && + ((MatchingStrategy) it.clientValue).clientValue == clientValue } + } + @Issue("#604") def "should convert YAML with Message matchers to DSL"() { given: diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy index d5070c821f..6fbca1a0ba 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy @@ -209,5 +209,10 @@ class WireMockResponseStubStrategySpec extends Specification { subject.buildClientRequestContent() then: noExceptionThrown() + when: + def response = new WireMockResponseStubStrategy(contract) + response.buildClientResponseContent() + then: + noExceptionThrown() } } diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml index dfa11813c5..43c9ff6e20 100644 --- a/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml @@ -1,6 +1,19 @@ request: method: GET - urlPath: /get + urlPath: /get/1 + headers: + Content-Type: application/json + cookies: + foo: 2 + queryParameters: + limit: 10 + offset: 20 + filter: 'email' + sort: name + search: 55 + age: 99 + name: John.Doe + email: 'bob@email.com' body: duck: 123 alpha: "abc" @@ -15,6 +28,35 @@ request: "complex.key": 'foo' nullValue: null matchers: + url: + regex: /get/[0-9] + # predefined: + # execute a method + # command: + queryParameters: + - key: limit + type: equal_to + value: 20 + - key: offset + type: containing + value: 20 + - key: sort + type: equal_to + value: name + - key: search + type: not_matching + value: '^[0-9]{2}$' + - key: age + type: not_matching + value: '^\\w*$' + - key: name + type: matching + value: 'John.*' + - key: hello + type: absent + cookies: + - key: foo + regex: '[0-9]' headers: - key: Content-Type regex: "application/json.*" @@ -45,8 +87,6 @@ request: type: by_equality - path: $.nullvalue type: by_null - headers: - Content-Type: application/json response: status: 200 body: