Groups pacts by name and parses to get consumer and provider

fixes gh-792
This commit is contained in:
Marcin Grzejszczak
2018-11-10 21:25:04 +01:00
parent 8cf9dc8252
commit 3109d5b610
8 changed files with 420 additions and 41 deletions

View File

@@ -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<Contract> 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) {

View File

@@ -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
}
}

View File

@@ -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<Collection<Pact>> {
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<Collection<Pact>> {
@Override
Collection<Pact> convertTo(Collection<Contract> contracts) {
List<Pact> pactContracts = new ArrayList<>()
for (Contract contract : contracts) {
if (contract.request) {
pactContracts.add(requestResponsePactCreator.createFromContract(contract))
}
if (contract.input) {
pactContracts.add(messagePactCreator.createFromContract(contract))
}
Map<String, List<Contract>> groupedContracts = contracts.groupBy { NamingUtil.name(it).toString() }
for (List<Contract> list : groupedContracts.values()) {
List<Contract> httpOnly = list.findAll { it.request }
List<Contract> 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
}

View File

@@ -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<Contract> 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

View File

@@ -135,6 +135,7 @@ class PactContractConverterSpec extends Specification {
given:
Collection<Contract> 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<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"),
new File("src/test/resources/contracts/grouped/shouldWorkWithBeer.groovy"))
when:
Collection<Pact> 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<Contract> expectedContracts = [

View File

@@ -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())
}
}
}
]

View File

@@ -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"
}
}
}

View File

@@ -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": {