diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.groovy index 99e9b5026c..634e42325f 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.groovy @@ -15,6 +15,7 @@ */ package org.springframework.cloud.contract.verifier.spec.pact + import au.com.dius.pact.consumer.MessagePactBuilder import au.com.dius.pact.consumer.dsl.DslPart import au.com.dius.pact.model.v3.messaging.MessagePact @@ -27,7 +28,6 @@ import org.springframework.cloud.contract.spec.internal.Headers import org.springframework.cloud.contract.spec.internal.Input import org.springframework.cloud.contract.spec.internal.OutputMessage import org.springframework.cloud.contract.verifier.util.ContentUtils - /** * Creator of {@link MessagePact} instances * @@ -40,26 +40,33 @@ class MessagePactCreator { private static final Closure clientValueExtractor = { DslProperty property -> property.clientValue } - MessagePact createFromContract(Contract contract) { - MessagePactBuilder messagePactBuilder = MessagePactBuilder.consumer("Consumer") - .hasPactWith("Provider") - .given(getGiven(contract.input)) - .expectsToReceive(getOutcome(contract)) - if (contract.outputMessage) { - OutputMessage message = contract.outputMessage - if (message.body) { - DslPart pactResponseBody = BodyConverter.toPactBody(message.body, clientValueExtractor) - if (message.bodyMatchers) { - pactResponseBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(message.bodyMatchers)) + MessagePact createFromContract(List contracts) { + if (contracts.empty) { + return null + } + Names names = NamingUtil.name(contracts.get(0)) + MessagePactBuilder pactBuilder = MessagePactBuilder.consumer(names.consumer) + .hasPactWith(names.producer) + contracts.each { Contract contract -> + pactBuilder = pactBuilder + .given(getGiven(contract.input)) + .expectsToReceive(getOutcome(contract)) + if (contract.outputMessage) { + OutputMessage message = contract.outputMessage + if (message.body) { + DslPart pactResponseBody = BodyConverter.toPactBody(message.body, clientValueExtractor) + if (message.bodyMatchers) { + pactResponseBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(message.bodyMatchers)) + } + pactResponseBody.setGenerators(ValueGeneratorConverter.extract(message, { DslProperty dslProperty -> dslProperty.serverValue })) + pactBuilder = pactBuilder.withContent(pactResponseBody) + } + if (message.headers) { + pactBuilder = pactBuilder.withMetadata(getMetadata(message.headers)) } - pactResponseBody.setGenerators(ValueGeneratorConverter.extract(message, { DslProperty dslProperty -> dslProperty.serverValue })) - messagePactBuilder = messagePactBuilder.withContent(pactResponseBody) - } - if (message.headers) { - messagePactBuilder = messagePactBuilder.withMetadata(getMetadata(message.headers)) } } - return messagePactBuilder.toPact() + return pactBuilder.toPact() } private String getGiven(Input input) { diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/NamingUtil.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/NamingUtil.groovy new file mode 100644 index 0000000000..2ae59da9b9 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/NamingUtil.groovy @@ -0,0 +1,45 @@ +package org.springframework.cloud.contract.verifier.spec.pact + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import org.springframework.cloud.contract.spec.Contract + +/** + * @author Marcin Grzejszczak + * @since + */ +@PackageScope +@CompileStatic +final class NamingUtil { + + // consumer___producer___testname + private static final String SEPARATOR = "___" + + protected static Names name(Contract contract) { + String contractName = contract.name + if (!contractName || !contractName.contains(SEPARATOR)) { + return new Names(["Consumer", "Provider" , ""] as String[]) + } + return new Names(contractName.split(SEPARATOR)) + } +} + +@PackageScope +@CompileStatic +class Names { + final String consumer + final String producer + final String test + + Names(String[] strings) { + this.consumer = strings[0] + this.producer = strings[1] + this.test = strings.length >= 2 ? strings[2] : "" + } + + + @Override + String toString() { + return this.consumer + "_" + this.producer + } +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy index fb83d7a008..49161f87d5 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy @@ -22,7 +22,6 @@ import au.com.dius.pact.model.v3.messaging.MessagePact import groovy.transform.CompileStatic import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.ContractConverter - /** * Converter of JSON PACT file * @@ -33,11 +32,10 @@ import org.springframework.cloud.contract.spec.ContractConverter @CompileStatic class PactContractConverter implements ContractConverter> { - private RequestResponseSCContractCreator requestResponseSCContractCreator = new RequestResponseSCContractCreator() - private MessagingSCContractCreator messagingSCContractCreator = new MessagingSCContractCreator() - - private RequestResponsePactCreator requestResponsePactCreator = new RequestResponsePactCreator() - private MessagePactCreator messagePactCreator = new MessagePactCreator() + private final RequestResponseSCContractCreator requestResponseSCContractCreator = new RequestResponseSCContractCreator() + private final MessagingSCContractCreator messagingSCContractCreator = new MessagingSCContractCreator() + private final RequestResponsePactCreator requestResponsePactCreator = new RequestResponsePactCreator() + private final MessagePactCreator messagePactCreator = new MessagePactCreator() @Override boolean isAccepted(File file) { @@ -64,13 +62,14 @@ class PactContractConverter implements ContractConverter> { @Override Collection convertTo(Collection contracts) { List pactContracts = new ArrayList<>() - for (Contract contract : contracts) { - if (contract.request) { - pactContracts.add(requestResponsePactCreator.createFromContract(contract)) - } - if (contract.input) { - pactContracts.add(messagePactCreator.createFromContract(contract)) - } + Map> groupedContracts = contracts.groupBy { NamingUtil.name(it).toString() } + for (List list : groupedContracts.values()) { + List httpOnly = list.findAll { it.request } + List messagingOnly = list.findAll { it.input } + RequestResponsePact responsePact = requestResponsePactCreator.createFromContract(httpOnly) + if (responsePact) pactContracts.add(responsePact) + MessagePact messagePact = messagePactCreator.createFromContract(messagingOnly) + if (messagePact) pactContracts.add(messagePact) } return pactContracts } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.groovy index a745d4f7eb..4f884cdfe3 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.groovy @@ -42,18 +42,29 @@ import org.springframework.cloud.contract.spec.internal.Response @PackageScope class RequestResponsePactCreator { - RequestResponsePact createFromContract(Contract contract) { - assertNoExecutionProperty(contract) - PactDslWithProvider pactDslWithProvider = ConsumerPactBuilder.consumer("Consumer") - .hasPactWith("Provider") - PactDslRequestWithPath pactDslRequest = createPactDslRequestWithPath(contract, pactDslWithProvider) - PactDslResponse pactDslResponse = createPactDslResponse(contract, pactDslRequest) + RequestResponsePact createFromContract(List contracts) { + if (contracts.empty) { + return null + } + Names names = NamingUtil.name(contracts.get(0)) + PactDslWithProvider pactDslWithProvider = ConsumerPactBuilder + .consumer(names.consumer).hasPactWith(names.producer) + PactDslResponse pactDslResponse = null; + contracts.each { Contract contract -> + assertNoExecutionProperty(contract) + PactDslRequestWithPath pactDslRequest = pactDslResponse ? + createPactDslRequestWithPath(contract, pactDslResponse) : + createPactDslRequestWithPath(contract, pactDslWithProvider) + pactDslResponse = createPactDslResponse(contract, pactDslRequest) + } return pactDslResponse.toPact() } private void assertNoExecutionProperty(Contract contract) { - assertNoExecutionPropertyInBody(contract.request.body, { DslProperty dslProperty -> dslProperty.serverValue }) - assertNoExecutionPropertyInBody(contract.response.body, { DslProperty dslProperty -> dslProperty.clientValue }) + assertNoExecutionPropertyInBody(contract.request.body, + { DslProperty dslProperty -> dslProperty.serverValue }) + assertNoExecutionPropertyInBody(contract.response.body, + { DslProperty dslProperty -> dslProperty.clientValue }) } private void assertNoExecutionPropertyInBody(Body body, Closure dslPropertyValueExtractor) { @@ -76,6 +87,32 @@ class RequestResponsePactCreator { } } + private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract, PactDslResponse pactDslResponse) { + Request request = contract.request + PactDslRequestWithPath pactDslRequest = pactDslResponse + .uponReceiving(contract.description ?: "") + .path(url(request)) + .method(request.method.serverValue.toString()) + String query = query(request) + if (query) { + pactDslRequest = pactDslRequest.encodedQuery(query) + } + if (request.headers) { + request.headers.entries.each { Header header -> + pactDslRequest = processHeader(pactDslRequest, header) + } + } + if (request.body) { + DslPart pactRequestBody = BodyConverter.toPactBody(request.body, { DslProperty property -> property.serverValue }) + if (request.bodyMatchers) { + pactRequestBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(request.bodyMatchers)) + } + pactRequestBody.setGenerators(ValueGeneratorConverter.extract(request.body, { DslProperty dslProperty -> dslProperty.clientValue })) + pactDslRequest = pactDslRequest.body(pactRequestBody) + } + return pactDslRequest + } + private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract, PactDslWithProvider pactDslWithProvider) { Request request = contract.request PactDslRequestWithPath pactDslRequest = pactDslWithProvider diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy index 4d3b6ba497..b94c30b2bd 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy @@ -135,6 +135,7 @@ class PactContractConverterSpec extends Specification { given: Collection inputContracts = [ Contract.make { + name("my_consumer___my_producer___testname") description("a retrieve Mallory request") request { method(GET()) @@ -196,10 +197,10 @@ class PactContractConverterSpec extends Specification { String expectedJson = ''' { "provider": { - "name": "Provider" + "name": "my_producer" }, "consumer": { - "name": "Consumer" + "name": "my_consumer" }, "interactions": [ { @@ -402,6 +403,20 @@ class PactContractConverterSpec extends Specification { } } + def "should convert contracts from grouped contracts to pacts"() { + given: + List contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"), + new File("src/test/resources/contracts/grouped/shouldWorkWithBeer.groovy")) + when: + Collection pacts = converter.convertTo(contracts) + then: + pacts.size() == 1 + String convertedPactAsText = JsonOutput.toJson(pacts.first().toMap(PactSpecVersion.V3)) + JSONAssert.assertEquals( + new File("src/test/resources/contracts/grouped/shouldWorkWithBeer.json").text, + convertedPactAsText, false) + } + def "should convert from pact v2 to two SC contracts"() { given: Collection expectedContracts = [ diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/resources/contracts/grouped/shouldWorkWithBeer.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/resources/contracts/grouped/shouldWorkWithBeer.groovy new file mode 100644 index 0000000000..b93d180bb7 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/resources/contracts/grouped/shouldWorkWithBeer.groovy @@ -0,0 +1,90 @@ +package contracts.grouped + + +import org.springframework.cloud.contract.spec.Contract + +[ + Contract.make { + description(""" +Represents a successful scenario of getting a beer + +``` +given: + client is old enough +when: + he applies for a beer +then: + we'll grant him the beer +``` + +""") + request { + method POST() + name("10-04-pact-consumer___10-05-pact-producer___too young") + url '/check' + body( + age: 60 + ) + headers { + contentType(applicationJson()) + } + bodyMatchers { + jsonPath('$.age', byRegex(regex("[2-9][0-9]"))) + } + } + response { + status 200 + body(""" + { + "status": "OK" + } + """) + headers { + contentType(applicationJson()) + } + } + }, + Contract.make { + description(""" +Represents an unsuccessful scenario of getting a beer + +``` +given: + client is not old enough +when: + he applies for a beer +then: + we'll NOT grant him the beer +``` + +""") + request { + method POST() + name("10-04-pact-consumer___10-05-pact-producer___old enough") + url '/check' + body( + age: 10 + ) + headers { + contentType(applicationJson()) + } + bodyMatchers { + jsonPath('$.age', byRegex(regex("[0-1][0-9]"))) + } + } + response { + status 200 + body(""" + { + "status": "NOT_OK" + } + """) + headers { + contentType(applicationJson()) + } + bodyMatchers { + jsonPath('$.status', byType()) + } + } + } +] diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/resources/contracts/grouped/shouldWorkWithBeer.json b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/resources/contracts/grouped/shouldWorkWithBeer.json new file mode 100644 index 0000000000..0e1aeecb0a --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/resources/contracts/grouped/shouldWorkWithBeer.json @@ -0,0 +1,156 @@ +{ + "provider": { + "name": "10-05-pact-producer" + }, + "consumer": { + "name": "10-04-pact-consumer" + }, + "interactions": [ + { + "description": "\nRepresents an unsuccessful scenario of getting a beer\n\n```\ngiven:\n\tclient is not old enough\nwhen:\n\the applies for a beer\nthen:\n\twe'll NOT grant him the beer\n```\n\n", + "request": { + "method": "POST", + "path": "/check", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "age": 10 + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + }, + "body": { + "$.age": { + "matchers": [ + { + "match": "regex", + "regex": "[0-1][0-9]" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "NOT_OK" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + }, + "body": { + "$.status": { + "matchers": [ + { + "match": "type" + } + ], + "combine": "AND" + } + } + } + } + }, + { + "description": "\nRepresents a successful scenario of getting a beer\n\n```\ngiven:\n\tclient is old enough\nwhen:\n\the applies for a beer\nthen:\n\twe'll grant him the beer\n```\n\n", + "request": { + "method": "POST", + "path": "/check", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "age": 60 + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + }, + "body": { + "$.age": { + "matchers": [ + { + "match": "regex", + "regex": "[2-9][0-9]" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "OK" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + }, + "body": { + "$.status": { + "matchers": [ + { + "match": "type" + } + ], + "combine": "AND" + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.13" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/resources/contracts/shouldReturnFraudStats.json b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/resources/contracts/shouldReturnFraudStats.json index e3c9668686..570a333c06 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/resources/contracts/shouldReturnFraudStats.json +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/resources/contracts/shouldReturnFraudStats.json @@ -35,6 +35,36 @@ "body": {} } } + }, + { + "description": "", + "request": { + "method": "GET", + "path": "/drunks" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": { + "count": 100 + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/vnd\\.fraud\\.v1\\+json.*" + } + ], + "combine": "AND" + } + }, + "body": {} + } + } } ], "metadata": {