Removing the Pact integration; fixes gh-1845

This commit is contained in:
Marcin Grzejszczak
2022-11-17 16:01:40 +01:00
parent cff332fc40
commit dc581e8934
66 changed files with 4 additions and 6689 deletions

View File

@@ -4,8 +4,6 @@ IMPORTANT: The following properties can be passed as a system property (for exam
|===
|Name | Default | Description
|`stubrunner.properties.pactbroker.provider-name-with-group-id` | `false` | When using the Pact Broker-based approach, you can automatically group ID to the provider name.
|`stubrunner.properties.git.branch` | | When using the SCM-based approach, you can customize the branch name to check out.
|`stubrunner.properties.git.commit-message` | Updating project [$project] with stubs | When using the SCM based approach, you can customize the commit message for created stubs. The `$project` text is replaced with the project name.
|`stubrunner.properties.git.no-of-attempts` | `10` | When using the-SCM based approach, you can customize the number of retries to push the stubs to Git.

View File

@@ -36,4 +36,3 @@
:doc_samples_url: {samples_url}/wiremock-for-contract-docs
:wiremock_tests: {core_path}/spring-cloud-contract-wiremock
:introduction_url: {core_path}
:standalone_pact_path: {samples_url}/

View File

@@ -14,5 +14,5 @@ The reference documentation consists of the following sections:
<<using.adoc#using,Using {project-full-name}>> :: {project-full-name} usage examples and workflows.
<<project-features.adoc#features,{project-full-name} Features>> :: Contract DSL, Messaging, Spring Cloud Contract Stub Runner, and Spring Cloud Contract WireMock.
<<project-features.adoc#features-build-tools,Build Tools>> :: Maven Plugin, Gradle Plugin, and Docker.
<<howto.adoc#howto,"`How-to`" Guides>> :: Stubs versioning, Pact integration, Debugging, and more.
<<howto.adoc#howto,"`How-to`" Guides>> :: Stubs versioning, Debugging, and more.
<<appendix.adoc#appendix,Appendices>> :: Properties, Metadata, Configuration, Dependencies, and more.

View File

@@ -96,7 +96,6 @@ Need more details about {project-full-name}'s core features?
<<project-features.adoc#features-jax-rs, JAX-RS>> |
<<project-features.adoc#features-context-paths, Context Paths>> |
<<project-features.adoc#features-rest-docs, RESTDocs>>
<<howto.adoc#how-to-generate-pact-from-scc, Pact>>
* *Modules:*
<<project-features.adoc#features-stub-runner, Stub Runner>> |
<<project-features.adoc#features-wiremock, WireMock>>

View File

@@ -112,7 +112,7 @@ buildscript {
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}"
classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifier_version}"
// here you can also pass additional dependencies such as Pact or Kotlin spec e.g.:
// here you can also pass additional dependencies such as Kotlin spec e.g.:
// classpath "org.springframework.cloud:spring-cloud-contract-spec-kotlin:${verifier_version}"
}
}

View File

@@ -28,7 +28,6 @@ Spring Cloud Contract Verifier stand out on the market of Consumer Driven Contra
- Automatic generation of tests from the defined contract.
- Stub Runner functionality: The stubs are automatically downloaded at runtime from Nexus or Artifactory.
- Spring Cloud integration: No discovery service is needed for integration tests.
- Spring Cloud Contract integrates with Pact and provides easy hooks to extend its functionality.
- Ability to add support for any language & framework through Docker.
[[how-to-not-write-contracts-in-groovy]]
@@ -831,427 +830,6 @@ to find stub definitions and contracts. For example, for `com.example:foo:1.0.0`
* Stub servers are started and fed with mappings.
* Messaging definitions are read and used in the messaging tests.
[[how-to-use-pact-broker]]
== How Can I Use the Pact Broker?
When using https://pact.io/[Pact], you can use the https://github.com/pact-foundation/pact_broker[Pact Broker]
to store and share Pact definitions. Starting from Spring Cloud Contract
2.0.0, you can fetch Pact files from the Pact Broker to generate
tests and stubs.
IMPORTANT: Pact follows the consumer contract convention. That means
that the consumer creates the Pact definitions first and then
shares the files with the Producer. Those expectations are generated
from the Consumer's code and can break the Producer if the expectations
are not met.
[[how-to-use-pact-broker-pact]]
=== How to Work with Pact
Spring Cloud Contract includes support for the https://docs.pact.io/[Pact] representation of
contracts up until version 4. Instead of using the DSL, you can use Pact files. In this section, we
show how to add Pact support for your project. Note, however, that not all functionality is supported.
Starting with version 3, you can combine multiple matchers for the same element:
You can use matchers for the body, headers, request and path, and you can use value generators.
Spring Cloud Contract currently only supports multiple matchers that are combined by using the `AND` rule logic.
Next to that, the request and path matchers are skipped during the conversion.
When using a date, time, or datetime value generator with a given format,
the given format is skipped and the ISO format is used.
[[how-to-use-pact-broker-pact-converter]]
=== Pact Converter
In order to properly support the Spring Cloud Contract way of doing messaging
with Pact, you have to provide some additional metadata entries.
To define the destination to which a message gets sent, you have to
set a `metaData` entry in the Pact file with the `sentTo` key equal to the destination to
which a message is to be sent (for example, `"metaData": { "sentTo": "activemq:output" }`).
[[how-to-use-pact-broker-pact-contract]]
=== Pact Contract
Spring Cloud Contract can read the Pact JSON definition. You can place the file in the
`src/test/resources/contracts` folder. Remember to put the `spring-cloud-contract-pact` dependency to your classpath.
[[how-to-use-pact-broker-pact-for-producers]]
=== Pact for Producers
On the producer side, you must add two additional dependencies to your plugin
configuration. One is the Spring Cloud Contract Pact support, and the other represents
the current Pact version that you use. The following listing shows how to do so for both
Maven and Gradle:
====
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
.Maven
----
include::{standalone_pact_path}/producer_pact/pom.xml[tags=pact_dependency,indent=0]
----
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
.Gradle
----
include::{standalone_pact_path}/producer_pact/build.gradle[tags=pact_dependency,indent=0]
----
====
When you build your application, a test and stub is generated. The following
example shows a test and stub that came from this process:
====
[source,java,indent=0,subs="verbatim,attributes",role="primary"]
.test
----
@Test
public void validate_shouldMarkClientAsFraud() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/vnd.fraud.v1+json")
.body("{\"clientId\":\"1234567890\",\"loanAmount\":99999}");
// when:
ResponseOptions response = given().spec(request)
.put("/fraudcheck");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/vnd\\.fraud\\.v1\\+json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['rejectionReason']").isEqualTo("Amount too high");
// and:
assertThat(parsedJson.read("$.fraudCheckStatus", String.class)).matches("FRAUD");
}
----
[source,json,indent=0,subs="verbatim,attributes",role="secondary"]
.stub
----
{
"id" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
"uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
"request" : {
"url" : "/fraudcheck",
"method" : "PUT",
"headers" : {
"Content-Type" : {
"matches" : "application/vnd\\.fraud\\.v1\\+json.*"
}
},
"bodyPatterns" : [ {
"matchesJsonPath" : "$[?(@.['loanAmount'] = 99999)]"
}, {
"matchesJsonPath" : "$[?(@.clientId =~ /([0-9]{10})/)]"
} ]
},
"response" : {
"status" : 200,
"body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}",
"headers" : {
"Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8"
},
"transformers" : [ "response-template" ]
},
}
----
====
[[how-to-use-pact-broker-pact-consumers]]
=== Pact for Consumers
On the consumer side, you must add two additional dependencies to your project
dependencies. One is the Spring Cloud Contract Pact support, and the other represents the
current Pact version that you use. The following listing shows how to do so for both
Maven and Gradle:
====
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
.Maven
----
include::{standalone_pact_path}/consumer_pact_stubrunner/pom.xml[tags=pact_dependency,indent=0]
----
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
.Gradle
----
include::{standalone_pact_path}/consumer_pact_stubrunner/build.gradle[tags=pact_dependency,indent=0]
----
====
[[pact-stub-downloader]]
=== Communicating with the Pact Broker
Whenever the `repositoryRoot` property starts with a Pact protocol
(that is, starts with `pact://`), the stub downloader tries
to fetch the Pact contract definitions from the Pact Broker.
Whatever is set after `pact://` is parsed as the Pact Broker URL.
By setting environment variables, system properties, or properties set
inside the plugin or contracts repository configuration, you can
tweak the downloader's behavior. The following table describes the
properties:
.Pact Stub Downloader properties
|====
|Name of a property |Default | Description
|
* `pactbroker.host` (plugin prop)
* `stubrunner.properties.pactbroker.host` (system prop)
* `STUBRUNNER_PROPERTIES_PACTBROKER_HOST` (env prop)
|Host from URL passed to `repositoryRoot`
|The URL of the Pact Broker.
|
* `pactbroker.port` (plugin prop)
* `stubrunner.properties.pactbroker.port` (system prop)
* `STUBRUNNER_PROPERTIES_PACTBROKER_PORT` (env prop)
|Port from URL passed to `repositoryRoot`
|The port of Pact Broker.
|
* `pactbroker.protocol` (plugin prop)
* `stubrunner.properties.pactbroker.protocol` (system prop)
* `STUBRUNNER_PROPERTIES_PACTBROKER_PROTOCOL` (env prop)
|Protocol from URL passed to `repositoryRoot`
|The protocol of Pact Broker.
|
* `pactbroker.tags` (plugin prop)
* `stubrunner.properties.pactbroker.tags` (system prop)
* `STUBRUNNER_PROPERTIES_PACTBROKER_TAGS` (env prop)
|Version of the stub, or `latest` if version is `+`
|The tags that should be used to fetch the stub.
|
* `pactbroker.auth.scheme` (plugin prop)
* `stubrunner.properties.pactbroker.auth.scheme` (system prop)
* `STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_SCHEME` (env prop)
|`Basic`
|The kind of authentication that should be used to connect to the Pact Broker.
|
* `pactbroker.auth.username` (plugin prop)
* `stubrunner.properties.pactbroker.auth.username` (system prop)
* `STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_USERNAME` (env prop)
|The username passed to `contractsRepositoryUsername` (Maven) or `contractRepository.username` (Gradle)
|The username to use when connecting to the Pact Broker.
|
* `pactbroker.auth.password` (plugin prop)
* `stubrunner.properties.pactbroker.auth.password` (system prop)
* `STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_PASSWORD` (env prop)
|The password passed to `contractsRepositoryPassword` (Maven) or `contractRepository.password` (Gradle)
|The password to use when connecting to the Pact Broker.
|
* `pactbroker.provider-name-with-group-id` (plugin prop)
* `stubrunner.properties.pactbroker.provider-name-with-group-id` (system prop)
* `STUBRUNNER_PROPERTIES_PACTBROKER_PROVIDER_NAME_WITH_GROUP_ID` (env prop)
|false
|When `true`, the provider name is a combination of `groupId:artifactId`. If `false`, only `artifactId` is used.
|====
[[how-to-pact-consumer]]
=== Flow: Consumer Contract Approach with Pact Broker on the Consumer Side
The consumer uses the Pact framework to generate Pact files. The
Pact files are sent to the Pact Broker. You can find an example of such a setup
https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/{samples_branch}/consumer_pact[here].
[[how-to-pact-producer]]
=== Flow: Consumer Contract Approach with Pact Broker on the Producer Side
For the producer to use the Pact files from the Pact Broker, we can reuse the
same mechanism we use for external contracts. We route Spring Cloud Contract
to use the Pact implementation with the URL that contains
the `pact://` protocol. You can pass the URL to the
Pact Broker. You can find an example of such a setup
https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/{samples_branch}/producer_pact[here].
The following listing shows the configuration details for both Maven and Gradle:
====
[source,xml,indent=0,role="primary"]
.Maven
----
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<!-- Base class mappings etc. -->
<!-- We want to pick contracts from a Git repository -->
<contractsRepositoryUrl>pact://http://localhost:8085</contractsRepositoryUrl>
<!-- We reuse the contract dependency section to set up the path
to the folder that contains the contract definitions. In our case the
path will be /groupId/artifactId/version/contracts -->
<contractDependency>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<!-- When + is passed, a latest tag will be applied when fetching pacts -->
<version>+</version>
</contractDependency>
<!-- The contracts mode can't be classpath -->
<contractsMode>REMOTE</contractsMode>
</configuration>
<!-- Don't forget to add spring-cloud-contract-pact to the classpath! -->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-pact</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
</dependencies>
</plugin>
----
[source,groovy,indent=0,role="secondary"]
.Gradle
----
buildscript {
repositories {
//...
}
dependencies {
// ...
// Don't forget to add spring-cloud-contract-pact to the classpath!
classpath "org.springframework.cloud:spring-cloud-contract-pact:${contractVersion}"
}
}
contracts {
// When + is passed, a latest tag will be applied when fetching pacts
contractDependency {
stringNotation = "${project.group}:${project.name}:+"
}
contractRepository {
repositoryUrl = "pact://http://localhost:8085"
}
// The mode can't be classpath
contractsMode = "REMOTE"
// Base class mappings etc.
}
----
====
With such a setup:
* Pact files are downloaded from the Pact Broker.
* Spring Cloud Contract converts the Pact files into tests and stubs.
* The JAR with the stubs gets automatically created, as usual.
[[how-to-pact-consumer-producer-contract]]
=== Flow: Producer Contract Approach with Pact on the Consumer Side
In the scenario where you do not want to do the consumer contract approach
(for every single consumer, define the expectations) but you prefer
to do producer contracts (the producer provides the contracts and
publishes stubs), you can use Spring Cloud Contract with the
Stub Runner option. You can find an example of such a setup
https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/{samples_branch}/consumer_pact_stubrunner[here].
Remember to add the Stub Runner and Spring Cloud Contract Pact modules
as test dependencies.
The following listing shows the configuration details for both Maven and Gradle:
====
[source,xml,indent=0,role="primary"]
.Maven
----
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Don't forget to add spring-cloud-contract-pact to the classpath! -->
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-pact</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
----
[source,groovy,indent=0,role="secondary"]
.Gradle
----
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
dependencies {
//...
testCompile("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
// Don't forget to add spring-cloud-contract-pact to the classpath!
testCompile("org.springframework.cloud:spring-cloud-contract-pact")
}
----
====
Next, you can pass the URL of the Pact Broker to `repositoryRoot`, prefixed
with the `pact://` protocol (for example, `pact://http://localhost:8085`), as the following
example shows:
====
[source,java,indent=0]
----
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.REMOTE,
ids = "com.example:beer-api-producer-pact",
repositoryRoot = "pact://http://localhost:8085")
public class BeerControllerTest {
//Inject the port of the running stub
@StubRunnerPort("beer-api-producer-pact") int producerPort;
//...
}
----
====
With such a setup:
* Pact files are downloaded from the Pact Broker.
* Spring Cloud Contract converts the Pact files into stub definitions.
* The stub servers are started and fed with stubs.
[[how-to-debug]]
== How Can I Debug the Request/Response Being Sent by the Generated Tests Client?

View File

@@ -14,5 +14,5 @@ The reference documentation consists of the following sections:
<<using.adoc#using,Using {project-full-name}>> :: {project-full-name} usage examples and workflows.
<<project-features.adoc#features,{project-full-name} Features>> :: Contract DSL, Messaging, Spring Cloud Contract Stub Runner, and Spring Cloud Contract WireMock.
<<project-features.adoc#features-build-tools,Build Tools>> :: Maven Plugin, Gradle Plugin, and Docker.
<<howto.adoc#howto,"`How-to`" Guides>> :: Stubs versioning, Pact integration, Debugging, and more.
<<howto.adoc#howto,"`How-to`" Guides>> :: Stubs versioning, Debugging, and more.
<<appendix.adoc#appendix,Appendices>> :: Properties, Metadata, Configuration, Dependencies, and more.

31
pom.xml
View File

@@ -25,7 +25,6 @@
<properties>
<!-- Align it also in Docker image under [docker/spring-cloud-contract-docker/project/gradle.properties] -->
<camel.version>3.19.0</camel.version>
<pact.version>4.3.15</pact.version>
<jsch-agent.version>0.0.9</jsch-agent.version>
<spring-cloud-build.version>4.0.0-SNAPSHOT</spring-cloud-build.version>
<spring-cloud-zookeeper.version>4.0.0-SNAPSHOT</spring-cloud-zookeeper.version>
@@ -226,36 +225,6 @@
<artifactId>commons-text</artifactId>
<version>${commons-text.version}</version>
</dependency>
<dependency>
<groupId>au.com.dius.pact.consumer</groupId>
<artifactId>junit5</artifactId>
<version>${pact.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy-all</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>junit5</artifactId>
<version>${pact.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy-all</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars</artifactId>

View File

@@ -96,14 +96,6 @@
<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
<!-- <convertToYaml>true</convertToYaml>-->
</configuration>
<!-- if additional dependencies are needed e.g. for Pact -->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-pact</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
</dependencies>
</plugin>
<!-- end::contract_maven_plugin[] -->
<plugin>

View File

@@ -51,11 +51,6 @@ public class FraudDetectionController {
// end::initial_impl[]
}
@RequestMapping(value = "/pactfraudcheck", method = PUT)
public FraudCheckResult pactFraudCheck(@RequestBody FraudCheck fraudCheck) {
return fraudCheck(fraudCheck);
}
@RequestMapping(value = "/yamlfraudcheck", method = PUT)
public FraudCheckResult yamlFraudCheck(@RequestBody FraudCheck fraudCheck) {
return fraudCheck(fraudCheck);

View File

@@ -46,11 +46,6 @@ public class FraudStatsController {
return new Response(this.statsProvider.count(FraudType.ALL));
}
@GetMapping(value = "/pactfrauds")
public Response countAllPactFrauds() {
return countAllFrauds();
}
@GetMapping(value = "/yamlfrauds")
public Response countAllYamlFrauds() {
return countAllFrauds();
@@ -61,11 +56,6 @@ public class FraudStatsController {
return new Response(this.statsProvider.count(FraudType.DRUNKS));
}
@GetMapping(value = "/pactdrunks")
public Response countAllPactDrunks() {
return countAllDrunks();
}
@GetMapping(value = "/yamldrunks")
public Response countAllYamlDrunks() {
return countAllDrunks();
@@ -101,4 +91,4 @@ class DefaultStatsProvider implements StatsProvider {
public int count(FraudType fraudType) {
return 0;
}
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2013-2020 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 com.example.fraud;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.jupiter.api.BeforeEach;
public class PactBase {
@BeforeEach
public void setup() {
RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(),
new FraudStatsController(stubbedStatsProvider()));
}
private StatsProvider stubbedStatsProvider() {
return fraudType -> {
switch (fraudType) {
case DRUNKS:
return 100;
case ALL:
return 200;
}
return 0;
};
}
public void assertThatRejectionReasonIsNull(Object rejectionReason) {
assert rejectionReason == null;
}
}

View File

@@ -1,98 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "PUT",
"path": "/pactfraudcheck",
"headers": {
"Content-Type": "application/json"
},
"body": {
"clientId": "1234567890",
"loanAmount": 99999
},
"generators": {
"body": {
"$.clientId": {
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine": "AND"
}
},
"body": {
"$.clientId": {
"matchers": [
{
"match": "regex",
"regex": "[0-9]{10}"
}
],
"combine": "AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"fraudCheckStatus": "FRAUD",
"rejection.reason": "Amount too high"
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine": "AND"
}
},
"body": {
"$.fraudCheckStatus": {
"matchers": [
{
"match": "regex",
"regex": "FRAUD"
}
],
"combine": "AND"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -1,42 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "PUT",
"path": "/pactfraudcheck",
"headers": {
"Content-Type": "application/json"
},
"body": {
"clientId": "1234567890",
"loanAmount": 123.123
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"fraudCheckStatus": "OK",
"rejection.reason": null
}
}
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "2.4.18"
}
}
}

View File

@@ -1,34 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "GET",
"path": "/pactdrunks"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"count": 100
}
}
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "2.4.18"
}
}
}

View File

@@ -1,34 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "GET",
"path": "/pactfrauds"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"count": 200
}
}
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "2.4.18"
}
}
}

View File

@@ -162,7 +162,6 @@ public class Contract {
throw new IllegalStateException("Status is missing for HTTP contract");
}
}
// Can't assert messaging part cause Pact doesn't require destinations it seems
}
/**

View File

@@ -55,11 +55,6 @@
<artifactId>spring-cloud-contract-converters</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-pact</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-stub-runner</artifactId>

View File

@@ -19,7 +19,6 @@
<modules>
<module>spring-cloud-contract-converters</module>
<module>spring-cloud-contract-pact</module>
<module>spring-cloud-contract-maven-plugin</module>
<module>spring-cloud-contract-gradle-plugin</module>
<module>spring-cloud-contract-gradle-portal-plugin</module>

View File

@@ -378,11 +378,6 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-pact</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-testing</groupId>
<artifactId>maven-plugin-testing-harness</artifactId>

View File

@@ -22,8 +22,6 @@ import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.util.StringUtils;
import static java.nio.charset.Charset.defaultCharset;
import static org.apache.commons.io.FileUtils.readFileToString;
import static org.apache.maven.plugin.testing.MojoParameters.newParameter;
@@ -309,21 +307,6 @@ public class PluginUnitTest extends AbstractMojoTest {
"target/stubs/META-INF/org.springframework.cloud.verifier.sample/common-repo/0.1/contracts/pom.xml");
}
@Test
public void shouldGenerateContractTestsForPactAndMaintainIndents() throws Exception {
File basedir = getBasedir("pact");
executeMojo(basedir, "generateTests", defaultPackageForTests());
assertFilesPresent(basedir,
"target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/ContractVerifierTest.java");
File test = new File(basedir,
"target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/ContractVerifierTest.java");
String testContents = readFileToString(test, defaultCharset());
int countOccurrencesOf = StringUtils.countOccurrencesOf(testContents, "\t\tMockMvcRequestSpecification");
then(countOccurrencesOf).isEqualTo(4);
}
@Test
public void shouldRunPushStubsToScm() throws Exception {
File basedir = getBasedir("git-basic-remote-contracts");

View File

@@ -1,59 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2013-2020 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.
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.cloud.verifier.sample</groupId>
<artifactId>sample-pact-project</artifactId>
<version>0.1</version>
<properties>
<spring.cloud.contract.version>2.0.0.BUILD-SNAPSHOT
</spring.cloud.contract.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<configuration>
<baseClassForTests>com.example.FooBase</baseClassForTests>
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*com.*</contractPackageRegex>
<baseClassFQN>com.example.TestBase</baseClassFQN>
</baseClassMapping>
</baseClassMappings>
</configuration>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-pact</artifactId>
<version>${spring.cloud.contract.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,42 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "PUT",
"path": "/fraudcheck",
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"clientId": "1234567890",
"loanAmount": 99999
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8"
},
"body": {
"fraudCheckStatus": "FRAUD",
"rejectionReason": "Amount too high"
}
}
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "2.4.18"
}
}
}

View File

@@ -1,42 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "PUT",
"path": "/fraudcheck",
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"clientId": "1234567890",
"loanAmount": 123.123
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8"
},
"body": {
"fraudCheckStatus": "OK",
"rejectionReason": null
}
}
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "2.4.18"
}
}
}

View File

@@ -1,34 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "GET",
"path": "/drunks"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8"
},
"body": {
"count": 100
}
}
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "2.4.18"
}
}
}

View File

@@ -1,34 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "GET",
"path": "/frauds"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8"
},
"body": {
"count": 200
}
}
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "2.4.18"
}
}
}

View File

@@ -1,92 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-tools</artifactId>
<version>4.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-contract-pact</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Contract Pact</name>
<description>Spring Cloud Contract Pact</description>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-converters</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-stub-runner</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
</dependency>
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy-nio</artifactId>
</dependency>
<dependency>
<groupId>au.com.dius.pact.consumer</groupId>
<artifactId>junit5</artifactId>
</dependency>
<dependency>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>junit5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>addSources</goal>
<goal>addTestSources</goal>
<goal>generateStubs</goal>
<goal>compile</goal>
<goal>generateTestStubs</goal>
<goal>compileTests</goal>
<goal>removeStubs</goal>
<goal>removeTestStubs</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,467 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.stubrunner;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import au.com.dius.pact.core.model.Pact;
import au.com.dius.pact.core.model.PactSpecVersion;
import au.com.dius.pact.core.support.expressions.SystemPropertyResolver;
import au.com.dius.pact.core.support.expressions.ValueResolver;
import au.com.dius.pact.provider.junitsupport.loader.PactBroker;
import au.com.dius.pact.provider.junitsupport.loader.PactBrokerAuth;
import au.com.dius.pact.provider.junitsupport.loader.PactBrokerLoader;
import au.com.dius.pact.provider.junitsupport.loader.PactLoader;
import au.com.dius.pact.provider.junitsupport.loader.VersionSelector;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* Allows downloading of Pact files from the Pact Broker.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @since 2.0.0
*/
public final class PactStubDownloaderBuilder implements StubDownloaderBuilder {
private static final List<String> ACCEPTABLE_PROTOCOLS = Collections.singletonList("pact");
/**
* Does any of the accepted protocols matches the URL of the repository.
* @param url - of the repository
* @return {@code true} if the protocol is accepted
*/
private static boolean isProtocolAccepted(String url) {
return ACCEPTABLE_PROTOCOLS.stream().anyMatch(url::startsWith);
}
@Override
public StubDownloader build(StubRunnerOptions stubRunnerOptions) {
if (stubRunnerOptions.getStubsMode() == StubRunnerProperties.StubsMode.CLASSPATH
|| stubRunnerOptions.getStubRepositoryRoot() == null) {
return null;
}
Resource resource = stubRunnerOptions.getStubRepositoryRoot();
if (!(resource instanceof PactResource)) {
return null;
}
return new PactStubDownloader(stubRunnerOptions);
}
@Override
public Resource resolve(String location, ResourceLoader resourceLoader) {
if (!StringUtils.hasText(location) || !isProtocolAccepted(location)) {
return null;
}
return new PactResource(location);
}
}
class PactResource extends AbstractResource {
private final String rawLocation;
PactResource(String location) {
this.rawLocation = location;
}
@Override
public String getDescription() {
return this.rawLocation;
}
@Override
public InputStream getInputStream() {
return null;
}
@Override
public URI getURI() {
return URI.create(this.rawLocation);
}
}
class PactStubDownloader implements StubDownloader {
private static final String TEMP_DIR_PREFIX = "pact";
private static final Log log = LogFactory.getLog(PactStubDownloader.class);
// Preloading class for the shutdown hook not to throw ClassNotFound
private static final Class CLAZZ = TemporaryFileStorage.class;
private static final String ARTIFICIAL_NAME_ENDING_WITH_GROOVY = "name.groovy";
private static final String PROVIDER_NAME_WITH_GROUP_ID = "pactbroker.provider-name-with-group-id";
private final StubRunnerOptions stubRunnerOptions;
private final boolean deleteStubsAfterTest;
private final ObjectMapper objectMapper;
PactStubDownloader(StubRunnerOptions stubRunnerOptions) {
this.stubRunnerOptions = stubRunnerOptions;
this.objectMapper = new ObjectMapper();
this.deleteStubsAfterTest = stubRunnerOptions.isDeleteStubsAfterTest();
registerShutdownHook();
}
@Override
public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar(StubConfiguration stubConfiguration) {
String version = stubConfiguration.version;
final FromPropsThenFromSysEnv resolver = new FromPropsThenFromSysEnv(this.stubRunnerOptions);
List<String> tags = tags(version, resolver);
try {
PactLoader loader = pactBrokerLoader(resolver, tags);
String providerName = providerName(stubConfiguration);
List<Pact> pacts = loader.load(providerName);
if (pacts.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No pact definitions found for provider [" + providerName + "]");
}
return null;
}
File tmpDirWhereStubsWillBeUnzipped = TemporaryFileStorage.createTempDir(TEMP_DIR_PREFIX);
// make the groupid / artifactid folders
String coordinatesFolderName = stubConfiguration.getGroupId().replace(".", File.separator) + File.separator
+ stubConfiguration.getArtifactId();
File contractsFolder = new File(tmpDirWhereStubsWillBeUnzipped,
coordinatesFolderName + File.separator + "contracts");
File mappingsFolder = new File(tmpDirWhereStubsWillBeUnzipped,
coordinatesFolderName + File.separator + "mappings");
boolean createdContractsDirs = contractsFolder.mkdirs();
boolean createdMappingsDirs = mappingsFolder.mkdirs();
if (!createdContractsDirs || !createdMappingsDirs) {
throw new IllegalStateException("Failed to create mandatory [contracts] or [mappings] folders under ["
+ coordinatesFolderName + "]");
}
storePacts(providerName, pacts, contractsFolder, mappingsFolder);
return new AbstractMap.SimpleEntry<>(stubConfiguration, tmpDirWhereStubsWillBeUnzipped);
}
catch (IOException e) {
throw new IllegalStateException(e);
}
}
private void storePacts(String providerName, List<Pact> pacts, File contractsFolder, File mappingsFolder) {
for (int i = 0; i < pacts.size(); i++) {
String json = toJson(pacts.get(i).toMap(PactSpecVersion.V3));
File file = new File(contractsFolder, i + "_" + providerName.replace(":", "_") + "_pact.json");
storeFile(file.toPath(), json.getBytes());
try {
storeMapping(mappingsFolder, file);
}
catch (Exception e) {
log.warn("Exception occurred while trying to store the mapping", e);
}
}
}
private void storeMapping(File mappingsFolder, File file) {
Collection<Contract> contracts = new PactContractConverter().convertFrom(file);
if (log.isDebugEnabled()) {
log.debug("Converted pact file [" + file + "] to [" + contracts.size() + "] contracts");
}
MappingGenerator.toMappings(file, contracts, mappingsFolder);
}
private Path storeFile(Path path, byte[] contents) {
try {
Path storedPath = Files.write(path, contents);
if (log.isDebugEnabled()) {
log.debug("Stored file [" + path + "]");
}
return storedPath;
}
catch (IOException e) {
throw new IllegalStateException(e);
}
}
private String providerName(StubConfiguration stubConfiguration) {
boolean providerNameWithGroupId = Boolean.parseBoolean(StubRunnerPropertyUtils
.getProperty(this.stubRunnerOptions.getProperties(), PROVIDER_NAME_WITH_GROUP_ID));
if (providerNameWithGroupId) {
return stubConfiguration.getGroupId() + ":" + stubConfiguration.getArtifactId();
}
return stubConfiguration.getArtifactId();
}
PactLoader pactBrokerLoader(ValueResolver resolver, List<String> tags) throws IOException {
Resource repo = this.stubRunnerOptions.getStubRepositoryRoot();
String schemeSpecificPart = schemeSpecificPart(repo.getURI());
URI pactBrokerUrl = URI.create(schemeSpecificPart);
String stubRunnerUsername = this.stubRunnerOptions.getUsername() != null ? this.stubRunnerOptions.getUsername()
: "";
String stubRunnerPassword = this.stubRunnerOptions.getPassword() != null ? this.stubRunnerOptions.getPassword()
: "";
return new PactBrokerLoader(new PactBroker() {
@Override
public Class<? extends Annotation> annotationType() {
return PactBroker.class;
}
@Override
public String url() {
return resolver.resolveValue("pactbroker.url",
pactBrokerUrl.getScheme() + "://" + pactBrokerUrl.getHost() + ":" + pactBrokerUrl.getPort());
}
@Override
@Deprecated
public String host() {
return resolver.resolveValue("pactbroker.host", pactBrokerUrl.getHost());
}
@Override
@Deprecated
public String port() {
return resolver.resolveValue("pactbroker.port", String.valueOf(pactBrokerUrl.getPort()));
}
@Override
@Deprecated
public String scheme() {
return resolver.resolveValue("pactbroker.protocol", pactBrokerUrl.getScheme());
}
@Override
@Deprecated
public String[] tags() {
return new String[0];
}
@Override
public VersionSelector[] consumerVersionSelectors() {
return new VersionSelector[] { new VersionSelector() {
@Override
public Class<? extends Annotation> annotationType() {
return VersionSelector.class;
}
@Override
public String tag() {
return resolver.resolveValue("pactbroker.consumerversionselectors.tags", "");
}
@Override
public String latest() {
return resolver.resolveValue("pactbroker.consumerversionselectors.latest", "true");
}
@Override
public String consumer() {
return resolver.resolveValue("pactbroker.consumers", "");
}
@Override
public String fallbackTag() {
return resolver.resolveValue("pactbroker.fallbacktag", "");
}
} };
}
@Override
public String[] consumers() {
return new String[] { resolver.resolveValue("pactbroker.consumers", "") };
}
@Override
public PactBrokerAuth authentication() {
return new PactBrokerAuth() {
@Override
public Class<? extends Annotation> annotationType() {
return PactBrokerAuth.class;
}
@Override
public String username() {
return resolver.resolveValue("pactbroker.auth.username", stubRunnerUsername);
}
@Override
public String password() {
return resolver.resolveValue("pactbroker.auth.password", stubRunnerPassword);
}
@Override
public String token() {
return resolver.resolveValue("pactbroker.auth.token", "");
}
};
}
@Override
public Class<? extends ValueResolver> valueResolver() {
return SystemPropertyResolver.class;
}
@Override
public String enablePendingPacts() {
return resolver.resolveValue("pactbroker.enablePending", "false");
}
@Override
public String[] providerTags() {
return resolver.resolveValue("pactbroker.providerTags", "").split(",");
}
@Override
public String providerBranch() {
return resolver.resolveValue("pactbroker.providerBranch", "");
}
@Override
public String includeWipPactsSince() {
return resolver.resolveValue("pactbroker.includeWipPactsSince", "");
}
@Override
public String enableInsecureTls() {
return resolver.resolveValue("pactbroker.enableInsecureTls", "false");
}
});
}
private String schemeSpecificPart(URI uri) {
String part = uri.getSchemeSpecificPart();
if (!StringUtils.hasText(part)) {
return part;
}
return part.startsWith("//") ? part.substring(2) : part;
}
private List<String> tags(String version, ValueResolver resolver) {
String defaultTag = StubConfiguration.DEFAULT_VERSION.equals(version) ? "latest" : version;
return new ArrayList<>(Arrays.asList(
StringUtils.commaDelimitedListToStringArray(resolver.resolveValue("pactbroker.tags", defaultTag))));
}
private String toJson(Map map) {
try {
return this.objectMapper.writeValueAsString(map);
}
catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
}
private void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(
new Thread(() -> TemporaryFileStorage.cleanup(PactStubDownloader.this.deleteStubsAfterTest)));
}
}
class FromPropsThenFromSysEnv implements ValueResolver {
final SystemPropertyResolver resolver = SystemPropertyResolver.INSTANCE;
StubRunnerOptions options;
FromPropsThenFromSysEnv(StubRunnerOptions options) {
this.options = options;
}
@Override
public String resolveValue(String expression) {
return doResolve(expression, null);
}
@Nullable
private String doResolve(String expression, @Nullable String defaultValue) {
PropertyValueTuple tuple = new PropertyValueTuple(expression).invoke();
String propertyName = tuple.getPropertyName();
String property = StubRunnerPropertyUtils.getProperty(this.options.getProperties(), propertyName);
if (StringUtils.hasText(property)) {
return property;
}
return this.resolver.resolveValue(expression, defaultValue);
}
@Override
public boolean propertyDefined(String property) {
PropertyValueTuple tuple = new PropertyValueTuple(property).invoke();
String propertyName = tuple.getPropertyName();
boolean hasProperty = StubRunnerPropertyUtils.hasProperty(this.options.getProperties(), propertyName);
if (hasProperty) {
return true;
}
return this.resolver.propertyDefined(property);
}
@Nullable
@Override
public String resolveValue(String expression, String defaultValue) {
return doResolve(expression, defaultValue);
}
}
// taken from pact -
// au.com.dius.pact.provider.junit.sysprops.SystemPropertyResolver.PropertyValueTuple
class PropertyValueTuple {
private String propertyName;
PropertyValueTuple(String property) {
this.propertyName = property;
}
String getPropertyName() {
return this.propertyName;
}
PropertyValueTuple invoke() {
if (this.propertyName.contains(":")) {
String[] kv = org.apache.commons.lang3.StringUtils.splitPreserveAllTokens(this.propertyName, ':');
this.propertyName = kv[0];
}
return this;
}
}

View File

@@ -1,234 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
import au.com.dius.pact.consumer.dsl.DslPart;
import au.com.dius.pact.consumer.dsl.PactDslJsonArray;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.core.model.OptionalBody;
import au.com.dius.pact.core.model.Request;
import au.com.dius.pact.core.model.Response;
import au.com.dius.pact.core.model.generators.Generator;
import au.com.dius.pact.core.model.messaging.Message;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.Path;
import com.jayway.jsonpath.internal.path.PathCompiler;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import groovy.lang.GString;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.contract.spec.internal.ClientDslProperty;
import org.springframework.cloud.contract.spec.internal.DslProperty;
import org.springframework.cloud.contract.spec.internal.ServerDslProperty;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
/**
* @author Tim Ysewyn
* @author Stessy Delcroix
* @since 2.0.0
*/
final class BodyConverter {
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.INSTANCE.getMapper();
private BodyConverter() {
}
static DslPart toPactBody(DslProperty<?> dslProperty, Function<DslProperty<?>, Object> dslPropertyValueExtractor) {
return traverse(dslProperty, null, dslPropertyValueExtractor);
}
private static DslPart traverse(Object value, DslPart parent,
Function<DslProperty<?>, Object> dslPropertyValueExtractor) {
boolean isRoot = parent == null;
Object v = value;
if (v instanceof DslProperty) {
v = dslPropertyValueExtractor.apply((DslProperty<?>) v);
}
if (v instanceof GString) {
v = ContentUtils.extractValue((GString) v, dslPropertyValueExtractor);
}
if (v instanceof String) {
String stringValue = ((String) v).trim();
if (StringUtils.startsWith(stringValue, "{") && StringUtils.endsWith(stringValue, "}")) {
try {
v = OBJECT_MAPPER.readValue(stringValue, Object.class);
}
catch (JsonProcessingException ex) { /*
* it wasn't a JSON string after
* all...
*/
}
}
}
DslPart p = isRoot ? createRootDslPart(v) : parent;
if (v instanceof Map) {
processMap((Map) v, (PactDslJsonBody) p, dslPropertyValueExtractor);
}
else if (v instanceof Collection) {
processCollection((Collection) v, (PactDslJsonArray) p, dslPropertyValueExtractor);
}
return p;
}
private static DslPart createRootDslPart(Object value) {
return value instanceof Collection ? new PactDslJsonArray() : new PactDslJsonBody();
}
private static void processCollection(Collection values, PactDslJsonArray jsonArray,
Function<DslProperty<?>, Object> dslPropertyValueExtractor) {
values.forEach(v -> {
if (v instanceof DslProperty) {
v = dslPropertyValueExtractor.apply((DslProperty<?>) v);
}
if (v instanceof GString) {
v = ContentUtils.extractValue((GString) v, dslPropertyValueExtractor);
}
if (v == null) {
jsonArray.nullValue();
}
else if (v instanceof String) {
jsonArray.string((String) v);
}
else if (v instanceof Number) {
jsonArray.number((Number) v);
}
else if (v instanceof Map) {
PactDslJsonBody current = jsonArray.object();
traverse(v, current, dslPropertyValueExtractor);
current.closeObject();
}
else if (v instanceof Collection) {
PactDslJsonArray current = jsonArray.array();
traverse(v, current, dslPropertyValueExtractor);
current.closeArray();
}
});
}
private static void processMap(Map<String, Object> values, PactDslJsonBody jsonObject,
Function<DslProperty<?>, Object> dslPropertyValueExtractor) {
values.forEach((k, v) -> {
if (v instanceof DslProperty) {
v = dslPropertyValueExtractor.apply((DslProperty<?>) v);
}
if (v instanceof GString) {
v = ContentUtils.extractValue((GString) v, dslPropertyValueExtractor);
}
if (v == null) {
jsonObject.nullValue(k);
}
else if (v instanceof Boolean) {
jsonObject.booleanValue(k, (Boolean) v);
}
else if (v instanceof String) {
jsonObject.stringType(k, (String) v);
}
else if (v instanceof Number) {
jsonObject.numberValue(k, (Number) v);
}
else if (v instanceof Map) {
PactDslJsonBody current = jsonObject.object(k);
traverse(v, current, dslPropertyValueExtractor);
current.closeObject();
}
else if (v instanceof Collection) {
PactDslJsonArray current = jsonObject.array(k);
traverse(v, current, dslPropertyValueExtractor);
current.closeArray();
}
});
}
static Object toSCCBody(Request request) {
Object body = parseBody(request.getBody());
if (request.getGenerators().isNotEmpty() && request.getGenerators().getCategories()
.containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) {
applyGenerators(body,
request.getGenerators().getCategories().get(au.com.dius.pact.core.model.generators.Category.BODY),
currentValue -> pattern -> generatedValue -> new DslProperty<>(
new ClientDslProperty(pattern, generatedValue), currentValue));
}
return body;
}
static Object toSCCBody(Response response) {
Object body = parseBody(response.getBody());
if (response.getGenerators().isNotEmpty() && response.getGenerators().getCategories()
.containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) {
applyGenerators(body,
response.getGenerators().getCategories().get(au.com.dius.pact.core.model.generators.Category.BODY),
currentValue -> pattern -> generatedValue -> new DslProperty<>(currentValue,
new ServerDslProperty(pattern, generatedValue)));
}
return body;
}
static Object toSCCBody(Message message) {
Object body = parseBody(message.getContents());
if (message.getGenerators().isNotEmpty() && message.getGenerators().getCategories()
.containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) {
applyGenerators(body,
message.getGenerators().getCategories().get(au.com.dius.pact.core.model.generators.Category.BODY),
currentValue -> pattern -> generatedValue -> new DslProperty<>(
new ClientDslProperty(pattern, generatedValue), currentValue));
}
return body;
}
private static Object parseBody(OptionalBody optionalBody) {
if (optionalBody.isPresent()) {
try {
return OBJECT_MAPPER.readValue(optionalBody.getValue(), Object.class);
}
catch (IOException e) {
throw new RuntimeException("Body could not be read", e);
}
}
else {
return optionalBody.getValue();
}
}
private static void applyGenerators(Object body, Map<String, Generator> generatorsPerPath,
Function<Pattern, Function<Object, Function<Object, DslProperty<Object>>>> dslPropertyProvider) {
Configuration configuration = Configuration.builder().jsonProvider(new JacksonJsonProvider(OBJECT_MAPPER))
.build();
generatorsPerPath.forEach((path, generator) -> {
Path compiledPath = PathCompiler.compile(path);
EvaluationContext evaluationContext = compiledPath.evaluate(body, body, configuration, true);
evaluationContext.updateOperations().forEach(pathRef -> {
pathRef.convert(((currentValue, config) -> ValueGeneratorConverter.convert(generator,
(pattern, generatedValue) -> dslPropertyProvider.apply(pattern).apply(generatedValue)
.apply(currentValue))),
configuration);
});
});
}
}

View File

@@ -1,111 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import au.com.dius.pact.core.model.matchingrules.DateMatcher;
import au.com.dius.pact.core.model.matchingrules.EqualsMatcher;
import au.com.dius.pact.core.model.matchingrules.MatchingRuleCategory;
import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.NullMatcher;
import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.RegexMatcher;
import au.com.dius.pact.core.model.matchingrules.TimeMatcher;
import au.com.dius.pact.core.model.matchingrules.TimestampMatcher;
import au.com.dius.pact.core.model.matchingrules.TypeMatcher;
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
import org.springframework.cloud.contract.spec.internal.MatchingType;
import org.springframework.cloud.contract.spec.internal.RegexPatterns;
/**
* @author Tim Ysewyn
* @author Stessy Delcroix
* @since 2.0.0
*/
final class MatchingRulesConverter {
private MatchingRulesConverter() {
}
static MatchingRuleCategory matchingRulesForBody(BodyMatchers bodyMatchers) {
return matchingRulesFor("body", bodyMatchers);
}
private static MatchingRuleCategory matchingRulesFor(String categoryName, BodyMatchers bodyMatchers) {
MatchingRuleCategory category = new MatchingRuleCategory(categoryName);
bodyMatchers.matchers().forEach((b) -> {
String key = getMatcherKey(b.path());
MatchingType matchingType = b.matchingType();
switch (matchingType) {
case NULL:
category.addRule(key, NullMatcher.INSTANCE);
break;
case EQUALITY:
category.addRule(key, EqualsMatcher.INSTANCE);
break;
case TYPE:
if (b.minTypeOccurrence() != null && b.maxTypeOccurrence() != null) {
category.addRule(key, new MinMaxTypeMatcher(b.minTypeOccurrence(), b.maxTypeOccurrence()));
}
else if (b.minTypeOccurrence() != null) {
category.addRule(key, new MinTypeMatcher(b.minTypeOccurrence()));
}
else if (b.maxTypeOccurrence() != null) {
category.addRule(key, new MaxTypeMatcher(b.maxTypeOccurrence()));
}
else {
category.addRule(key, TypeMatcher.INSTANCE);
}
break;
case DATE:
category.addRule(key, new DateMatcher());
break;
case TIME:
category.addRule(key, new TimeMatcher());
break;
case TIMESTAMP:
category.addRule(key, new TimestampMatcher());
break;
case REGEX:
String pattern = b.value().toString();
if (pattern.equals(RegexPatterns.number().pattern())) {
category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.NUMBER));
}
else if (pattern.equals(RegexPatterns.anInteger().pattern())) {
category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER));
}
else if (pattern.equals(RegexPatterns.aDouble().pattern())) {
category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.DECIMAL));
}
else {
category.addRule(key, new RegexMatcher(pattern));
}
break;
default:
break;
}
});
return category;
}
private static String getMatcherKey(String path) {
return path.startsWith("$") ? path.substring(1) : path;
}
}

View File

@@ -1,118 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import au.com.dius.pact.consumer.MessagePactBuilder;
import au.com.dius.pact.consumer.dsl.DslPart;
import au.com.dius.pact.core.model.messaging.MessagePact;
import groovy.lang.GString;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.DslProperty;
import org.springframework.cloud.contract.spec.internal.Header;
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.
*
* @author Tim Ysewyn
* @author Stessy Delcroix
* @since 2.0.0
*/
class MessagePactCreator {
private static final Function<DslProperty<?>, Object> clientValueExtractor = DslProperty::getClientValue;
MessagePact createFromContract(List<Contract> contracts) {
if (CollectionUtils.isEmpty(contracts)) {
return null;
}
Names names = NamingUtil.name(contracts.get(0));
MessagePactBuilder pactBuilder = new MessagePactBuilder().consumer(names.getConsumer())
.hasPactWith(names.getProducer());
for (Contract contract : contracts) {
pactBuilder = pactBuilder.given(getGiven(contract.getInput())).expectsToReceive(getOutcome(contract));
if (contract.getOutputMessage() != null) {
OutputMessage message = contract.getOutputMessage();
if (message.getBody() != null) {
DslPart pactResponseBody = BodyConverter.toPactBody(message.getBody(), clientValueExtractor);
if (message.getBodyMatchers() != null) {
pactResponseBody
.setMatchers(MatchingRulesConverter.matchingRulesForBody(message.getBodyMatchers()));
}
pactResponseBody
.setGenerators(ValueGeneratorConverter.extract(message, DslProperty::getServerValue));
pactBuilder = pactBuilder.withContent(pactResponseBody);
}
if (message.getHeaders() != null) {
pactBuilder = pactBuilder.withMetadata(getMetadata(message.getHeaders()));
}
}
}
return pactBuilder.toPact();
}
private String getGiven(Input input) {
if (input.getTriggeredBy() != null) {
return input.getTriggeredBy().getExecutionCommand();
}
else {
return "";
}
}
private String getOutcome(Contract contract) {
if (contract.getOutputMessage() != null) {
OutputMessage message = contract.getOutputMessage();
return "message sent to " + clientValueExtractor.apply(message.getSentTo());
}
else {
return "assert that " + contract.getInput().getAssertThat().getExecutionCommand();
}
}
private Map<String, String> getMetadata(Headers headers) {
return headers.getEntries().stream().collect(Collectors.toMap(Header::getName, this::extractValue));
}
private String extractValue(Object value) {
Object v = value;
if (v instanceof DslProperty) {
v = clientValueExtractor.apply((DslProperty) v);
}
if (v instanceof GString) {
v = ContentUtils.extractValue((GString) v, clientValueExtractor);
}
if (v instanceof String) {
return (String) v;
}
else {
return v.toString();
}
}
}

View File

@@ -1,186 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import au.com.dius.pact.core.model.matchingrules.DateMatcher;
import au.com.dius.pact.core.model.matchingrules.MatchingRule;
import au.com.dius.pact.core.model.matchingrules.MatchingRuleCategory;
import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.NullMatcher;
import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.RegexMatcher;
import au.com.dius.pact.core.model.matchingrules.RuleLogic;
import au.com.dius.pact.core.model.matchingrules.TimeMatcher;
import au.com.dius.pact.core.model.matchingrules.TimestampMatcher;
import au.com.dius.pact.core.model.matchingrules.TypeMatcher;
import au.com.dius.pact.core.model.messaging.Message;
import au.com.dius.pact.core.model.messaging.MessagePact;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.Headers;
import org.springframework.cloud.contract.spec.internal.RegexPatterns;
import org.springframework.cloud.contract.spec.internal.ResponseBodyMatchers;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
/**
* Creator of {@link Contract} instances.
*
* @author Tim Ysewyn
* @author Stessy Delcroix
* @since 2.0.0
*/
class MessagingSCContractCreator {
private static final String FULL_BODY = "$";
private static final String DESTINATION_KEY = "sentTo";
private static final List<String> NON_HEADER_META_DATA = Collections.singletonList(DESTINATION_KEY);
Collection<Contract> convertFrom(MessagePact pact) {
return pact.getMessages().stream().map(message -> Contract.make(contract -> {
contract.label(message.getDescription());
if (!message.getProviderStates().isEmpty()) {
contract.input(i -> i.triggeredBy(this.getTriggeredBy(message)));
}
contract.outputMessage((outputMessage) -> {
if (message.getContents().isPresent()) {
outputMessage.body(BodyConverter.toSCCBody(message));
MatchingRuleCategory bodyRules = message.getMatchingRules().rulesForCategory("body");
if (bodyRules != null && !bodyRules.getMatchingRules().isEmpty()) {
outputMessage.bodyMatchers((responseBodyMatchers) -> outputMessageBodyMatchers(message,
bodyRules, responseBodyMatchers));
}
}
if (!message.getMetadata().isEmpty()) {
outputMessage.headers((headers) -> outputMessageHeaders(message, headers));
}
String dest = findDestination(message);
if (StringUtils.isNotBlank(dest)) {
outputMessage.sentTo(dest);
}
});
})).collect(Collectors.toList());
}
private void outputMessageHeaders(Message message, Headers headers) {
message.getMetadata().forEach((key, value) -> {
String matchingRuleGroup = value.toString();
if (key.equalsIgnoreCase("contentType")) {
headers.messagingContentType(matchingRuleGroup);
}
else if (!NON_HEADER_META_DATA.contains(key)) {
headers.header(key, matchingRuleGroup);
}
});
}
private void outputMessageBodyMatchers(Message message, MatchingRuleCategory bodyRules,
ResponseBodyMatchers responseBodyMatchers) {
bodyRules.getMatchingRules().forEach((matchingRuleKey, matchingRuleGroup) -> {
if (matchingRuleGroup.getRuleLogic() != RuleLogic.AND) {
throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported");
}
if (FULL_BODY.equals(matchingRuleKey)) {
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(message.getContents().getValue());
jsonPaths.forEach((j) -> {
responseBodyMatchers.jsonPath(j.keyBeforeChecking(), responseBodyMatchers.byType());
});
}
else {
matchingRuleGroup.getRules().forEach((rule) -> {
applyJsonPathToResponseBodyMatchers(rule, matchingRuleKey, responseBodyMatchers);
});
}
});
}
private String getTriggeredBy(Message message) {
String triggeredBy = message.getProviderStates().get(0).getName().replaceAll(":", " ").replaceAll(" ", "_")
.replaceAll("\\(", "").replaceAll("\\)", "");
return StringUtils.uncapitalize(triggeredBy) + "()";
}
private String findDestination(Message message) {
return message.getMetadata().get(DESTINATION_KEY) != null
? message.getMetadata().get(DESTINATION_KEY).toString() : "";
}
void applyJsonPathToResponseBodyMatchers(MatchingRule matchingRule, String key,
ResponseBodyMatchers responseBodyMatchers) {
if (matchingRule instanceof NullMatcher) {
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byNull());
}
else if (matchingRule instanceof RegexMatcher) {
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(((RegexMatcher) matchingRule).getRegex()));
}
else if (matchingRule instanceof DateMatcher) {
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byDate());
}
else if (matchingRule instanceof TimeMatcher) {
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byTime());
}
else if (matchingRule instanceof TimestampMatcher) {
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byTimestamp());
}
else if (matchingRule instanceof MinTypeMatcher) {
responseBodyMatchers.jsonPath(key,
responseBodyMatchers.byType((b) -> b.minOccurrence(((MinTypeMatcher) matchingRule).getMin())));
}
else if (matchingRule instanceof MinMaxTypeMatcher) {
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byType((c) -> {
c.minOccurrence(((MinMaxTypeMatcher) matchingRule).getMin());
c.maxOccurrence(((MinMaxTypeMatcher) matchingRule).getMax());
}));
}
else if (matchingRule instanceof MaxTypeMatcher) {
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byType((c) -> {
c.maxOccurrence(((MaxTypeMatcher) matchingRule).getMax());
}));
}
else if (matchingRule instanceof TypeMatcher) {
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byType());
}
else if (matchingRule instanceof NumberTypeMatcher) {
switch (((NumberTypeMatcher) matchingRule).getNumberType()) {
case NUMBER:
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(RegexPatterns.number()));
break;
case INTEGER:
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(RegexPatterns.anInteger()));
break;
case DECIMAL:
responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(RegexPatterns.aDouble()));
break;
default:
throw new RuntimeException("Unsupported number type!");
}
}
}
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
/**
* @author Marcin Grzejszczak
* @author Stessy Delcroix
* @since
*/
class Names {
private final String consumer;
private final String producer;
private final String test;
Names(String[] strings) {
this.consumer = strings[0];
this.producer = strings[1];
this.test = strings.length >= 2 ? strings[2] : "";
}
@Override
public String toString() {
return this.consumer + "_" + this.producer;
}
String getConsumer() {
return consumer;
}
String getProducer() {
return producer;
}
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import org.springframework.cloud.contract.spec.Contract;
/**
* @author Marcin Grzejszczak
* @author Stessy Delcroix
* @since
*/
final class NamingUtil {
// consumer___producer___testname
private static final String SEPARATOR = "___";
private NamingUtil() {
}
static Names name(Contract contract) {
String contractName = contract.getName();
if (contractName == null || !contractName.contains(SEPARATOR)) {
return new Names(new String[] { "Consumer", "Provider", "" });
}
return new Names(contractName.split(SEPARATOR));
}
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link ObjectMapper} singleton instance creation.
* @author Stessy Delcroix
* @since 2.0.0
*/
public enum ObjectMapperFactory {
/**
* Singleton instance constant.
*/
INSTANCE;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_YAML_COMMENTS, true);
OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
OBJECT_MAPPER.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true);
}
public ObjectMapper getMapper() {
return OBJECT_MAPPER;
}
}

View File

@@ -1,152 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import au.com.dius.pact.core.model.DefaultPactReader;
import au.com.dius.pact.core.model.Pact;
import au.com.dius.pact.core.model.PactSpecVersion;
import au.com.dius.pact.core.model.RequestResponsePact;
import au.com.dius.pact.core.model.messaging.MessagePact;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.core.util.Separators;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.ContractConverter;
/**
* Converter of JSON PACT file.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
* @author Stessy Delcroix
* @since 1.1.0
*/
public class PactContractConverter implements ContractConverter<Collection<Pact>> {
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.INSTANCE.getMapper();
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
public boolean isAccepted(File file) {
try {
Pact pact = DefaultPactReader.INSTANCE.loadPact(file);
return !pact.getInteractions().isEmpty();
}
catch (Exception e) {
return false;
}
}
@Override
public Collection<Contract> convertFrom(File file) {
Pact pact = DefaultPactReader.INSTANCE.loadPact(file);
if (pact instanceof RequestResponsePact) {
return requestResponseSCContractCreator.convertFrom((RequestResponsePact) pact);
}
if (pact instanceof MessagePact) {
return messagingSCContractCreator.convertFrom((MessagePact) pact);
}
throw new UnsupportedOperationException(
"We currently don't support pact contracts of type" + pact.getClass().getSimpleName());
}
@Override
public Collection<Pact> convertTo(Collection<Contract> contracts) {
List<Pact> pactContracts = new ArrayList<>();
Map<String, List<Contract>> groupedContracts = contracts.stream()
.collect(Collectors.groupingBy(c -> NamingUtil.name(c).toString()));
for (List<Contract> list : groupedContracts.values()) {
List<Contract> httpOnly = list.stream().filter(c -> c.getRequest() != null).collect(Collectors.toList());
List<Contract> messagingOnly = list.stream().filter(c -> c.getInput() != null).collect(Collectors.toList());
RequestResponsePact responsePact = requestResponsePactCreator.createFromContract(httpOnly);
if (responsePact != null) {
pactContracts.add(responsePact);
}
MessagePact messagePact = messagePactCreator.createFromContract(messagingOnly);
if (messagePact != null) {
pactContracts.add(messagePact);
}
}
return pactContracts;
}
@Override
public Map<String, byte[]> store(Collection<Pact> contracts) {
return contracts.stream().collect(Collectors.toMap(this::name, c -> {
try {
return this.buildPrettyPrint(OBJECT_MAPPER.writeValueAsString(c.toMap(PactSpecVersion.V3))).getBytes();
}
catch (JsonProcessingException e) {
throw new IllegalArgumentException("The pact contract is not a valid map", e);
}
}));
}
protected String name(Pact contract) {
return contract.getConsumer().getName() + "_" + contract.getProvider().getName() + "_"
+ Math.abs(contract.hashCode()) + ".json";
}
private String buildPrettyPrint(String contract) {
try {
Object intermediateObjectForPrettyPrinting = OBJECT_MAPPER.reader().readValue(contract, Object.class);
DefaultIndenter customIndenter = new DefaultIndenter(" ", "\n");
return OBJECT_MAPPER
.writer(new CustomPrettyPrinter().withArrayIndenter(customIndenter)
.withObjectIndenter(customIndenter))
.writeValueAsString(intermediateObjectForPrettyPrinting);
}
catch (IOException e) {
throw new RuntimeException("WireMock response body could not be pretty printed");
}
}
private static class CustomPrettyPrinter extends DefaultPrettyPrinter {
@Override
public CustomPrettyPrinter createInstance() {
return new CustomPrettyPrinter();
}
@Override
public DefaultPrettyPrinter withSeparators(Separators separators) {
_separators = separators;
_objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " ";
return this;
}
}
}

View File

@@ -1,94 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import au.com.dius.pact.core.model.ProviderState;
import org.springframework.cloud.contract.verifier.util.MetadataUtil;
import org.springframework.cloud.contract.verifier.util.SpringCloudContractMetadata;
import org.springframework.lang.NonNull;
public class PactMetaData implements SpringCloudContractMetadata {
/**
* Key under which this metadata entry can be found in contract's metadata.
*/
public static final String METADATA_KEY = "pact";
/**
* Metadata for adding {@link ProviderState} in Pact contract.
*/
private List<ProviderStateMetadata> providerStates = new ArrayList<>();
public List<ProviderStateMetadata> getProviderStates() {
return this.providerStates;
}
public void setProviderStates(List<ProviderStateMetadata> providerStates) {
this.providerStates = providerStates;
}
@NonNull
public static PactMetaData fromMetadata(Map<String, Object> metadata) {
return MetadataUtil.fromMetadata(metadata, PactMetaData.METADATA_KEY, new PactMetaData());
}
@Override
public String key() {
return METADATA_KEY;
}
@Override
public String description() {
return "Metadata for converting Contract to Pact";
}
/**
* {@link ProviderState} metadata.
*/
public static class ProviderStateMetadata {
/**
* If set, will be added to PACT provider states.
*/
private String name;
private Map<String, Object> params;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Map<String, Object> getParams() {
return this.params;
}
public void setParams(Map<String, Object> params) {
this.params = params;
}
}
}

View File

@@ -1,356 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import au.com.dius.pact.consumer.ConsumerPactBuilder;
import au.com.dius.pact.consumer.dsl.DslPart;
import au.com.dius.pact.consumer.dsl.PactDslRequestWithPath;
import au.com.dius.pact.consumer.dsl.PactDslResponse;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.dsl.PactDslWithState;
import au.com.dius.pact.core.model.RequestResponsePact;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.Body;
import org.springframework.cloud.contract.spec.internal.Cookies;
import org.springframework.cloud.contract.spec.internal.DslProperty;
import org.springframework.cloud.contract.spec.internal.ExecutionProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.QueryParameters;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.spec.internal.Request;
import org.springframework.cloud.contract.spec.internal.Response;
import org.springframework.util.CollectionUtils;
import static java.util.stream.Collectors.joining;
/**
* Creator of {@link RequestResponsePact} instances.
*
* @author Tim Ysewyn
* @author Stessy Delcroix
* @since 2.0.0
*/
class RequestResponsePactCreator {
RequestResponsePact createFromContract(List<Contract> contracts) {
if (CollectionUtils.isEmpty(contracts)) {
return null;
}
Names names = NamingUtil.name(contracts.get(0));
PactDslWithProvider pactDslWithProvider = ConsumerPactBuilder.consumer(names.getConsumer())
.hasPactWith(names.getProducer());
PactDslResponse pactDslResponse = null;
for (Contract contract : contracts) {
assertNoExecutionProperty(contract);
PactDslRequestWithPath pactDslRequest = pactDslResponse != null
? createPactDslRequestWithPath(contract, pactDslResponse)
: createPactDslRequestWithPath(contract, pactDslWithProvider);
pactDslResponse = createPactDslResponse(contract, pactDslRequest);
}
return pactDslResponse.toPact();
}
private void assertNoExecutionProperty(Contract contract) {
assertNoExecutionPropertyInBody(contract.getRequest().getBody(), DslProperty::getServerValue);
assertNoExecutionPropertyInBody(contract.getResponse().getBody(), DslProperty::getClientValue);
}
private void assertNoExecutionPropertyInBody(Body body,
Function<DslProperty<?>, Object> dslPropertyValueExtractor) {
traverseValues(body, dslPropertyValueExtractor, (Object object) -> {
if (object instanceof ExecutionProperty) {
throw new UnsupportedOperationException("We can't convert a contract that has execution property");
}
return object;
});
}
private void traverseValues(Object value, Function<DslProperty<?>, Object> dslPropertyValueExtractor,
Function<Object, Object> function) {
if (value instanceof DslProperty) {
traverseValues(dslPropertyValueExtractor.apply((DslProperty<?>) value), dslPropertyValueExtractor,
function);
}
else if (value instanceof Map) {
((Map) value).values().forEach(v -> traverseValues(v, dslPropertyValueExtractor, function));
}
else if (value instanceof Collection) {
((Collection<?>) value).forEach(v -> traverseValues(v, dslPropertyValueExtractor, function));
}
else {
function.apply(value);
}
}
private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract, PactDslResponse pactDslResponse) {
PactDslRequestWithPath pactDslRequest = getPactDslRequest(contract,
getPactDslWithStateFunction(pactDslResponse), getPactDslRequestWithPathBiFunction(pactDslResponse));
Request request = contract.getRequest();
final PactDslRequestWithPath finalPactDslRequest = pactDslRequest;
if (request.getHeaders() != null) {
request.getHeaders().getEntries().forEach(h -> processHeader(finalPactDslRequest, h));
}
if (request.getCookies() != null) {
pactDslRequest = processCookies(finalPactDslRequest, request.getCookies());
}
if (request.getBody() != null) {
DslPart pactRequestBody = BodyConverter.toPactBody(request.getBody(), DslProperty::getClientValue);
if (request.getBodyMatchers() != null) {
pactRequestBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(request.getBodyMatchers()));
}
pactRequestBody
.setGenerators(ValueGeneratorConverter.extract(request.getBody(), DslProperty::getClientValue));
pactDslRequest = pactDslRequest.body(pactRequestBody);
}
return pactDslRequest;
}
private Function<PactMetaData.ProviderStateMetadata, PactDslWithState> getPactDslWithStateFunction(
PactDslResponse pactDslResponse) {
return stateMetadata -> pactDslResponse.given(stateMetadata.getName(), stateMetadata.getParams());
}
private BiFunction<String, Request, PactDslRequestWithPath> getPactDslRequestWithPathBiFunction(
PactDslResponse pactDslResponse) {
return (description, request) -> pactDslResponse.uponReceiving(description).path(url(request))
.method(request.getMethod().getServerValue().toString());
}
private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract,
PactDslWithProvider pactDslWithProvider) {
PactDslRequestWithPath pactDslRequest = getPactDslRequest(contract,
getPactDslWithStateFunction(pactDslWithProvider),
getPactDslRequestWithPathBiFunction(pactDslWithProvider));
Request request = contract.getRequest();
final PactDslRequestWithPath finalPactDslRequest = pactDslRequest;
if (request.getHeaders() != null) {
request.getHeaders().getEntries().forEach(h -> processHeader(finalPactDslRequest, h));
}
if (request.getBody() != null) {
DslPart pactRequestBody = BodyConverter.toPactBody(request.getBody(), DslProperty::getServerValue);
if (request.getBodyMatchers() != null) {
pactRequestBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(request.getBodyMatchers()));
}
pactRequestBody
.setGenerators(ValueGeneratorConverter.extract(request.getBody(), DslProperty::getClientValue));
pactDslRequest = pactDslRequest.body(pactRequestBody);
}
return pactDslRequest;
}
private Function<PactMetaData.ProviderStateMetadata, PactDslWithState> getPactDslWithStateFunction(
PactDslWithProvider pactDslWithProvider) {
return stateMetadata -> pactDslWithProvider.given(stateMetadata.getName(), stateMetadata.getParams());
}
private BiFunction<String, Request, PactDslRequestWithPath> getPactDslRequestWithPathBiFunction(
PactDslWithProvider pactDslWithProvider) {
return (description, request) -> pactDslWithProvider.uponReceiving(description).path(url(request))
.method(request.getMethod().getServerValue().toString());
}
private PactDslRequestWithPath getPactDslRequest(Contract contract,
Function<PactMetaData.ProviderStateMetadata, PactDslWithState> pactDslWithStateFunction,
BiFunction<String, Request, PactDslRequestWithPath> pactDslRequestWithPathBiFunction) {
PactDslWithState pactDslWithState = getPactDslWithState(contract, pactDslWithStateFunction);
String description = StringUtils.isNotBlank(contract.getDescription()) ? contract.getDescription() : "";
Request request = contract.getRequest();
PactDslRequestWithPath pactDslRequest;
if (pactDslWithState != null) {
pactDslRequest = pactDslWithState.uponReceiving(description).path(url(request))
.method(request.getMethod().getServerValue().toString());
}
else {
pactDslRequest = pactDslRequestWithPathBiFunction.apply(description, request);
}
String query = query(request);
if (StringUtils.isNotBlank(query)) {
pactDslRequest = pactDslRequest.encodedQuery(query);
}
return pactDslRequest;
}
private PactDslWithState getPactDslWithState(Contract contract,
Function<PactMetaData.ProviderStateMetadata, PactDslWithState> pactDslWithStateFunction) {
PactDslWithState pactDslWithState = null;
if (contract.getMetadata().containsKey(PactMetaData.METADATA_KEY)) {
PactMetaData metadata = PactMetaData.fromMetadata(contract.getMetadata());
if (!metadata.getProviderStates().isEmpty()) {
for (PactMetaData.ProviderStateMetadata stateMetadata : metadata.getProviderStates()) {
if (pactDslWithState == null) {
pactDslWithState = pactDslWithStateFunction.apply(stateMetadata);
}
else {
pactDslWithState = pactDslWithState.given(stateMetadata.getName(), stateMetadata.getParams());
}
}
}
}
return pactDslWithState;
}
private String url(Request request) {
if (request.getUrlPath() != null) {
return request.getUrlPath().getServerValue().toString();
}
else if (request.getUrl() != null) {
return request.getUrl().getServerValue().toString();
}
throw new IllegalStateException("No url provided");
}
private String query(Request request) {
final StringBuilder query = new StringBuilder();
QueryParameters params = queryParams(request);
if (params != null) {
AtomicInteger index = new AtomicInteger();
params.getParameters().forEach(p -> {
query.append(p.getName()).append('=').append(p.getServerValue());
if (index.incrementAndGet() < params.getParameters().size()) {
query.append('&');
}
});
}
return query.toString();
}
private QueryParameters queryParams(Request request) {
if (request.getUrlPath() != null) {
return request.getUrlPath().getQueryParameters();
}
else if (request.getUrl() != null) {
return request.getUrl().getQueryParameters();
}
throw new IllegalStateException("No url provided");
}
private PactDslRequestWithPath processHeader(PactDslRequestWithPath pactDslRequest, Header header) {
if (header.isSingleValue()) {
String value = getDslPropertyServerValue(header).toString();
return pactDslRequest.headers(header.getName(), value);
}
else {
String regex = getDslPropertyClientValue(header).toString();
String example = getDslPropertyServerValue(header).toString();
return pactDslRequest.matchHeader(header.getName(), regex, example);
}
}
private String stubSideCookieExample(Cookies cookies) {
return cookies.asStubSideMap().entrySet().stream().map(Object::toString).collect(joining(";"));
}
private Object getDslPropertyClientValue(Object o) {
Object value = o;
if (value instanceof DslProperty) {
value = getDslPropertyClientValue(((DslProperty) value).getClientValue());
}
return value;
}
private Object getDslPropertyServerValue(Object o) {
Object value = o;
if (value instanceof DslProperty) {
value = getDslPropertyServerValue(((DslProperty) value).getServerValue());
}
return value;
}
private PactDslRequestWithPath processCookies(PactDslRequestWithPath pactDslRequest, Cookies cookies) {
Map<String, Object> stubSideCookies = cookies.asStubSideMap();
Collection<RegexProperty> regexProperties = stubSideCookies.values().stream()
.filter(r -> r instanceof Pattern || r instanceof RegexProperty).map(RegexProperty::new)
.collect(Collectors.toList());
if (!regexProperties.isEmpty()) {
String regex = regexProperties.stream().map(RegexProperty::pattern).collect(joining("|"));
return pactDslRequest.matchHeader("Cookie", regex, testSideCookieExample(cookies));
}
else {
return pactDslRequest.headers("Cookie", testSideCookieExample(cookies));
}
}
private String testSideCookieExample(Cookies cookies) {
return cookies.asTestSideMap().entrySet().stream().map(Object::toString).collect(joining(";"));
}
private PactDslResponse createPactDslResponse(Contract contract, PactDslRequestWithPath pactDslRequest) {
Response response = contract.getResponse();
PactDslResponse pactDslResponse = pactDslRequest.willRespondWith()
.status((Integer) response.getStatus().getClientValue());
PactDslResponse finalPactDslResponse = pactDslResponse;
if (response.getHeaders() != null) {
response.getHeaders().getEntries().forEach(h -> processHeader(finalPactDslResponse, h));
}
if (response.getCookies() != null) {
pactDslResponse = processCookies(pactDslResponse, response.getCookies());
}
if (response.getBody() != null) {
DslPart pactResponseBody = BodyConverter.toPactBody(response.getBody(), DslProperty::getClientValue);
if (response.getBodyMatchers() != null) {
pactResponseBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(response.getBodyMatchers()));
}
pactResponseBody
.setGenerators(ValueGeneratorConverter.extract(response.getBody(), DslProperty::getServerValue));
pactDslResponse = pactDslResponse.body(pactResponseBody);
}
return pactDslResponse;
}
private PactDslResponse processHeader(PactDslResponse pactDslResponse, Header header) {
if (header.isSingleValue()) {
String value = getDslPropertyClientValue(header).toString();
return pactDslResponse.headers(Collections.singletonMap(header.getName(), value));
}
else {
String regex = getDslPropertyServerValue(header).toString();
String example = getDslPropertyClientValue(header).toString();
return pactDslResponse.matchHeader(header.getName(), regex, example);
}
}
private PactDslResponse processCookies(PactDslResponse pactDslResponse, Cookies cookies) {
Map<String, Object> testSideCookies = cookies.asTestSideMap();
Collection<RegexProperty> regexProperties = testSideCookies.values().stream()
.filter(p -> p instanceof Pattern || p instanceof RegexProperty).map(RegexProperty::new)
.collect(Collectors.toList());
if (!regexProperties.isEmpty()) {
String regex = regexProperties.stream().map(RegexProperty::pattern).collect(joining("|"));
return pactDslResponse.matchHeader("Cookie", regex, stubSideCookieExample(cookies));
}
else {
return pactDslResponse.headers(Collections.singletonMap("Cookie", stubSideCookieExample(cookies)));
}
}
}

View File

@@ -1,432 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import au.com.dius.pact.core.model.Interaction;
import au.com.dius.pact.core.model.OptionalBody;
import au.com.dius.pact.core.model.ProviderState;
import au.com.dius.pact.core.model.Request;
import au.com.dius.pact.core.model.RequestResponseInteraction;
import au.com.dius.pact.core.model.RequestResponsePact;
import au.com.dius.pact.core.model.Response;
import au.com.dius.pact.core.model.matchingrules.DateMatcher;
import au.com.dius.pact.core.model.matchingrules.MatchingRule;
import au.com.dius.pact.core.model.matchingrules.MatchingRuleCategory;
import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup;
import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.NullMatcher;
import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher;
import au.com.dius.pact.core.model.matchingrules.RegexMatcher;
import au.com.dius.pact.core.model.matchingrules.RuleLogic;
import au.com.dius.pact.core.model.matchingrules.TimeMatcher;
import au.com.dius.pact.core.model.matchingrules.TimestampMatcher;
import au.com.dius.pact.core.model.matchingrules.TypeMatcher;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.RegexPatterns;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
/**
* Creator of {@link Contract} instances.
*
* @author Tim Ysewyn
* @author Stessy Delcroix
* @since 2.0.0
*/
class RequestResponseSCContractCreator {
private static final String FULL_BODY = "$";
Collection<Contract> convertFrom(RequestResponsePact pact) {
return pact.getInteractions().stream().map(interaction -> Contract.make(contract -> {
mapContractDescription(interaction, contract);
mapContractRequest(interaction, contract);
mapContractResponse(interaction, contract);
})).collect(Collectors.toList());
}
private void mapContractDescription(Interaction interaction, Contract contract) {
contract.description(buildDescription(interaction));
}
private void mapContractRequest(Interaction inter, Contract contract) {
contract.request((contractRequest) -> {
if (!(inter instanceof RequestResponseInteraction)) {
return;
}
RequestResponseInteraction interaction = (RequestResponseInteraction) inter;
Request pactRequest = interaction.getRequest();
contractRequest.method(pactRequest.getMethod());
mapRequestUrl(contractRequest, pactRequest);
if (!pactRequest.getHeaders().isEmpty()) {
mapRequestHeaders(contractRequest, pactRequest);
}
if (pactRequest.getHeaders().containsKey("Cookie")) {
mapRequestCookies(contractRequest, pactRequest);
}
if (pactRequest.getBody().getState() == OptionalBody.State.PRESENT) {
mapRequestBody(contractRequest, pactRequest);
}
MatchingRuleCategory bodyRules = pactRequest.getMatchingRules().rulesForCategory("body");
if (!bodyRules.getMatchingRules().isEmpty()) {
mapRequestBodyRules(contractRequest, bodyRules);
}
});
}
private void mapContractResponse(Interaction inter, Contract contract) {
contract.response((contractResponse) -> {
if (!(inter instanceof RequestResponseInteraction)) {
return;
}
RequestResponseInteraction interaction = (RequestResponseInteraction) inter;
Response pactResponse = interaction.getResponse();
contractResponse.status(pactResponse.getStatus());
if (pactResponse.getBody().isPresent()) {
mapResponseBody(contractResponse, pactResponse);
}
MatchingRuleCategory bodyRules = pactResponse.getMatchingRules().rulesForCategory("body");
if (!bodyRules.getMatchingRules().isEmpty()) {
mapResponseBodyRules(contractResponse, pactResponse, bodyRules);
}
if (!pactResponse.getHeaders().isEmpty()) {
mapResponseHeaders(contractResponse, pactResponse);
}
if (pactResponse.getHeaders().containsKey("Cookie")) {
mapResponseCookies(contractResponse, pactResponse);
}
});
}
private void mapResponseBodyRules(org.springframework.cloud.contract.spec.internal.Response contractResponse,
Response pactResponse, MatchingRuleCategory bodyRules) {
contractResponse.bodyMatchers((bodyMatchers) -> {
bodyRules.getMatchingRules().forEach((key, matchindRuleGroup) -> {
if (matchindRuleGroup.getRuleLogic() != RuleLogic.AND) {
throw new UnsupportedOperationException(
"Currently only the AND combination rule logic is supported");
}
if (FULL_BODY.equals(key)) {
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(
new String(pactResponse.getBody().getValue()));
jsonPaths.forEach(
(jsonPath) -> bodyMatchers.jsonPath(jsonPath.keyBeforeChecking(), bodyMatchers.byType()));
}
else {
matchindRuleGroup.getRules().forEach((matchingRule) -> {
if (matchingRule instanceof NullMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byNull());
}
else if (matchingRule instanceof RegexMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(((RegexMatcher) matchingRule).getRegex()));
}
else if (matchingRule instanceof DateMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byDate());
}
else if (matchingRule instanceof TimeMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byTime());
}
else if (matchingRule instanceof TimestampMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byTimestamp());
}
else if (matchingRule instanceof MinTypeMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byType((valueHolder) -> valueHolder
.minOccurrence((((MinTypeMatcher) matchingRule).getMin()))));
}
else if (matchingRule instanceof MinMaxTypeMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byType((valueHolder) -> {
valueHolder.minOccurrence((((MinMaxTypeMatcher) matchingRule).getMin()));
valueHolder.maxOccurrence((((MinMaxTypeMatcher) matchingRule).getMax()));
}));
}
else if (matchingRule instanceof MaxTypeMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byType((valueHolder) -> valueHolder
.maxOccurrence((((MaxTypeMatcher) matchingRule).getMax()))));
}
else if (matchingRule instanceof TypeMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byType());
}
else if (matchingRule instanceof NumberTypeMatcher) {
switch (((NumberTypeMatcher) matchingRule).getNumberType()) {
case NUMBER:
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.number()));
break;
case INTEGER:
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.anInteger()));
break;
case DECIMAL:
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.aDouble()));
break;
default:
throw new UnsupportedOperationException("Unsupported number type!");
}
}
});
}
});
});
}
private void mapResponseBody(org.springframework.cloud.contract.spec.internal.Response contractResponse,
Response pactResponse) {
Object parsedBody = BodyConverter.toSCCBody(pactResponse);
if (parsedBody instanceof Map) {
contractResponse.body((Map) parsedBody);
}
else if (parsedBody instanceof List) {
contractResponse.body((List) parsedBody);
}
else {
contractResponse.body(parsedBody.toString());
}
}
private void mapRequestBodyRules(org.springframework.cloud.contract.spec.internal.Request contractRequest,
MatchingRuleCategory bodyRules) {
contractRequest.bodyMatchers((bodyMatchers) -> {
bodyRules.getMatchingRules().forEach((key, matchingRuleGroup) -> {
if (matchingRuleGroup.getRuleLogic() != RuleLogic.AND) {
throw new UnsupportedOperationException(
"Currently only the AND combination rule logic is supported");
}
matchingRuleGroup.getRules().forEach((matchingRule) -> {
if (matchingRule instanceof RegexMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(((RegexMatcher) matchingRule).getRegex()));
}
else if (matchingRule instanceof DateMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byDate());
}
else if (matchingRule instanceof TimeMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byTime());
}
else if (matchingRule instanceof TimestampMatcher) {
bodyMatchers.jsonPath(key, bodyMatchers.byTimestamp());
}
else if (matchingRule instanceof NumberTypeMatcher) {
switch (((NumberTypeMatcher) matchingRule).getNumberType()) {
case NUMBER:
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.number()));
break;
case INTEGER:
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.anInteger()));
break;
case DECIMAL:
bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.aDouble()));
break;
default:
throw new RuntimeException("Unsupported number type!");
}
}
});
});
});
}
private void mapRequestBody(org.springframework.cloud.contract.spec.internal.Request contractRequest,
Request pactRequest) {
Object parsedBody = BodyConverter.toSCCBody(pactRequest);
if (parsedBody instanceof Map) {
contractRequest.body((Map) parsedBody);
}
else if (parsedBody instanceof List) {
contractRequest.body((List) parsedBody);
}
else {
contractRequest.body(parsedBody.toString());
}
}
private void mapRequestCookies(org.springframework.cloud.contract.spec.internal.Request contractRequest,
Request pactRequest) {
MatchingRuleCategory headerRules = pactRequest.getMatchingRules().rulesForCategory("header");
String[] splitCookiesHeader = pactRequest.getHeaders().get("Cookie").get(0).split(";");
Map<String, String> foundCookies = Stream.of(splitCookiesHeader).map((cookieHeader) -> cookieHeader.split("="))
.collect(Collectors.toMap(splittedCookieHeader -> splittedCookieHeader[0],
splittedCookieHeader -> splittedCookieHeader[1]));
contractRequest.cookies((cookies) -> foundCookies.forEach((key, value) -> {
if (headerRules.getMatchingRules().containsKey("Cookie")) {
MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get("Cookie");
if (matchingRuleGroup.getRules().size() > 1) {
throw new UnsupportedOperationException(
"Currently only 1 rule at a time for a header is supported");
}
MatchingRule matchingRule = matchingRuleGroup.getRules().get(0);
if (matchingRule instanceof RegexMatcher) {
cookies.cookie(key,
contractRequest.$(
contractRequest.c(contractRequest.regex(((RegexMatcher) matchingRule).getRegex())),
contractRequest.p(value)));
}
else {
throw new UnsupportedOperationException(
"Currently only the header matcher of type regex is supported");
}
}
else {
cookies.cookie(key, value);
}
}));
}
private void mapResponseCookies(org.springframework.cloud.contract.spec.internal.Response contractResponse,
Response pactResponse) {
MatchingRuleCategory headerRules = pactResponse.getMatchingRules().rulesForCategory("header");
String[] splitCookiesHeader = pactResponse.getHeaders().get("Cookie").get(0).split(";");
Map<String, String> foundCookies = Stream.of(splitCookiesHeader).map((cookieHeader) -> cookieHeader.split("="))
.collect(Collectors.toMap(splittedCookieHeader -> splittedCookieHeader[0],
splittedCookieHeader -> splittedCookieHeader[1]));
contractResponse.cookies((cookies) -> foundCookies.forEach((key, value) -> {
if (headerRules.getMatchingRules().containsKey("Cookie")) {
MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get("Cookie");
if (matchingRuleGroup.getRules().size() > 1) {
throw new UnsupportedOperationException(
"Currently only 1 rule at a time for a header is supported");
}
MatchingRule matchingRule = matchingRuleGroup.getRules().get(0);
if (matchingRule instanceof RegexMatcher) {
cookies.cookie(key, contractResponse.$(
contractResponse.p(
contractResponse.regex(Pattern.compile(((RegexMatcher) matchingRule).getRegex()))),
contractResponse.c(value)));
}
else {
throw new UnsupportedOperationException(
"Currently only the header matcher of type regex is supported");
}
}
else {
cookies.cookie(key, value);
}
}));
}
private void mapRequestUrl(org.springframework.cloud.contract.spec.internal.Request contractRequest,
Request pactRequest) {
if (!pactRequest.getQuery().isEmpty()) {
contractRequest.url(pactRequest.getPath(),
(url) -> url.queryParameters((queryParameters) -> pactRequest.getQuery().forEach((key,
values) -> values.forEach((singleValue) -> queryParameters.parameter(key, singleValue)))));
}
else {
contractRequest.url(pactRequest.getPath());
}
}
private void mapRequestHeaders(org.springframework.cloud.contract.spec.internal.Request contractRequest,
Request pactRequest) {
MatchingRuleCategory headerRules = pactRequest.getMatchingRules().rulesForCategory("header");
contractRequest.headers((headers) -> pactRequest.getHeaders().forEach((key, values) -> {
if (key.equalsIgnoreCase("Cookie")) {
return;
}
if (headerRules.getMatchingRules().containsKey(key)) {
MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get(key);
if (matchingRuleGroup.getRules().size() > 1) {
throw new UnsupportedOperationException(
"Currently only 1 rule at a time for a header is supported");
}
MatchingRule matchingRule = matchingRuleGroup.getRules().get(0);
if (matchingRule instanceof RegexMatcher) {
values.forEach((value) -> {
headers.header(key,
contractRequest.$(
contractRequest
.c(contractRequest.regex(((RegexMatcher) matchingRule).getRegex())),
contractRequest.p(value)));
});
}
else {
throw new UnsupportedOperationException(
"Currently only the header matcher of type regex is supported");
}
}
else {
values.forEach((value) -> headers.header(key, value));
}
}));
}
private void mapResponseHeaders(org.springframework.cloud.contract.spec.internal.Response contractResponse,
Response pactResponse) {
MatchingRuleCategory headerRules = pactResponse.getMatchingRules().rulesForCategory("header");
contractResponse.headers((headers) -> pactResponse.getHeaders().forEach((key, values) -> {
if (key.equalsIgnoreCase("Cookie")) {
return;
}
if (headerRules.getMatchingRules().containsKey(key)) {
MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get(key);
if (matchingRuleGroup.getRules().size() > 1) {
throw new UnsupportedOperationException(
"Currently only 1 rule at a time for a header is supported");
}
MatchingRule matchingRule = matchingRuleGroup.getRules().get(0);
if (matchingRule instanceof RegexMatcher) {
values.forEach((value) -> {
headers.header(key,
contractResponse.$(
contractResponse.p(contractResponse
.regex(Pattern.compile(((RegexMatcher) matchingRule).getRegex()))),
contractResponse.c(value)));
});
}
else {
throw new UnsupportedOperationException(
"Currently only the header matcher of type regex is supported");
}
}
else {
values.forEach((value) -> headers.header(key, value));
}
}));
}
private String buildDescription(Interaction interaction) {
StringBuilder description = new StringBuilder(interaction.getDescription());
List<ProviderState> providerStates = interaction.getProviderStates();
for (ProviderState providerState : providerStates) {
description.append(" ").append(providerState.getName());
Map<String, Object> params = providerState.getParams();
if (!params.isEmpty()) {
description.append("(");
params.forEach((k, v) -> {
description.append(k).append(": ").append(v.toString()).append(", ");
});
description.delete(description.length() - 2, description.length());
description.append(")");
}
}
return description.toString();
}
}

View File

@@ -1,215 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Pattern;
import au.com.dius.pact.core.model.generators.Category;
import au.com.dius.pact.core.model.generators.DateGenerator;
import au.com.dius.pact.core.model.generators.DateTimeGenerator;
import au.com.dius.pact.core.model.generators.Generator;
import au.com.dius.pact.core.model.generators.Generators;
import au.com.dius.pact.core.model.generators.RandomBooleanGenerator;
import au.com.dius.pact.core.model.generators.RandomDecimalGenerator;
import au.com.dius.pact.core.model.generators.RandomHexadecimalGenerator;
import au.com.dius.pact.core.model.generators.RandomIntGenerator;
import au.com.dius.pact.core.model.generators.RandomStringGenerator;
import au.com.dius.pact.core.model.generators.RegexGenerator;
import au.com.dius.pact.core.model.generators.TimeGenerator;
import au.com.dius.pact.core.model.generators.UuidGenerator;
import groovy.lang.GString;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.contract.spec.internal.Body;
import org.springframework.cloud.contract.spec.internal.DslProperty;
import org.springframework.cloud.contract.spec.internal.OutputMessage;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
/**
* Convert a value for a given {@link Generator}.
*
* @author Tim Ysewyn
* @author Stessy Delcroix
* @since 2.0.0
*/
final class ValueGeneratorConverter {
private static final String INTEGER_PATTERN = "-?(\\d+)";
private static final Pattern INTEGER = Pattern.compile(INTEGER_PATTERN);
private static final String DECIMAL_PATTERN = "-?(\\d*\\.\\d+)";
private static final Pattern DECIMAL = Pattern.compile(DECIMAL_PATTERN);
private static final String HEX_PATTERN = "[a-fA-F0-9]+";
private static final Pattern HEX = Pattern.compile(HEX_PATTERN);
private static final String ALPHA_NUMERIC_PATTERN = "[a-zA-Z0-9]+";
private static final Pattern ALPHA_NUMERIC = Pattern.compile(ALPHA_NUMERIC_PATTERN);
private static final String UUID_PATTERN = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}";
private static final Pattern UUID = Pattern.compile(UUID_PATTERN);
private static final String ANY_DATE_PATTERN = "(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])";
private static final Pattern ANY_DATE = Pattern.compile(ANY_DATE_PATTERN);
private static final String ANY_TIME_PATTERN = "(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])";
private static final Pattern ANY_TIME = Pattern.compile(ANY_TIME_PATTERN);
private static final String ANY_DATE_TIME_PATTERN = "([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])";
private static final Pattern ANY_DATE_TIME = Pattern.compile(ANY_DATE_TIME_PATTERN);
private static final String TRUE_OR_FALSE_PATTERN = "(true|false)";
private static final Pattern TRUE_OR_FALSE = Pattern.compile(TRUE_OR_FALSE_PATTERN);
private ValueGeneratorConverter() {
}
static DslProperty<Object> convert(Generator generator,
BiFunction<Pattern, Object, DslProperty<Object>> dslPropertyProvider) {
Pattern pattern = null;
if (generator instanceof RandomIntGenerator) {
pattern = INTEGER;
}
else if (generator instanceof RandomDecimalGenerator) {
pattern = DECIMAL;
}
else if (generator instanceof RandomHexadecimalGenerator) {
pattern = HEX;
}
else if (generator instanceof RandomStringGenerator) {
pattern = ALPHA_NUMERIC;
}
else if (generator instanceof RegexGenerator) {
pattern = Pattern.compile(((RegexGenerator) generator).getRegex());
}
else if (generator instanceof UuidGenerator) {
pattern = UUID;
}
else if (generator instanceof DateGenerator) {
pattern = getDateTimePattern(((DateGenerator) generator).getFormat(), ANY_DATE);
}
else if (generator instanceof TimeGenerator) {
pattern = getDateTimePattern(((TimeGenerator) generator).getFormat(), ANY_TIME);
}
else if (generator instanceof DateTimeGenerator) {
pattern = getDateTimePattern(((DateTimeGenerator) generator).getFormat(), ANY_DATE_TIME);
}
else if (generator instanceof RandomBooleanGenerator) {
pattern = TRUE_OR_FALSE;
}
if (pattern == null) {
throw new UnsupportedOperationException(
"We currently don't support a generator of type " + generator.getClass().getSimpleName());
}
else {
Object generatedValue = generator.generate(new HashMap<>(), null);
return dslPropertyProvider.apply(pattern, generatedValue);
}
}
private static Pattern getDateTimePattern(String format, Pattern defaultPattern) {
return StringUtils.isNotBlank(format) ? Pattern.compile(format) : defaultPattern;
}
static Generators extract(Body body, Function<DslProperty<?>, Object> dslPropertyValueProvider) {
Generators generators = new Generators();
traverse(body, dslPropertyValueProvider, "", generators, Category.BODY);
return generators;
}
static Generators extract(OutputMessage message, Function<DslProperty<?>, Object> dslPropertyValueProvider) {
Generators generators = new Generators();
traverse(message.getBody(), dslPropertyValueProvider, "", generators, Category.BODY);
return generators;
}
private static void traverse(Object value, Function<DslProperty<?>, Object> dslPropertyValueProvider, String path,
Generators generators, Category category) {
Object v = value;
if (v instanceof DslProperty) {
v = dslPropertyValueProvider.apply((DslProperty<?>) v);
}
if (v instanceof GString) {
v = ContentUtils.extractValue((GString) v, dslPropertyValueProvider);
}
if (v instanceof Map) {
((Map) v).forEach(
(key, val) -> traverse(val, dslPropertyValueProvider, path + "." + key, generators, category));
}
else if (v instanceof Collection) {
AtomicInteger index = new AtomicInteger();
((Collection<?>) v).forEach(val -> traverse(val, dslPropertyValueProvider,
path + "[" + index.getAndIncrement() + "]", generators, category));
}
else if (v instanceof DslProperty) {
traverse(v, dslPropertyValueProvider, path, generators, category);
}
else if (v instanceof RegexProperty || v instanceof Pattern) {
RegexProperty regexProperty = new RegexProperty(v);
switch (regexProperty.pattern()) {
case INTEGER_PATTERN:
generators.addGenerator(category, path, new RandomIntGenerator(0, Integer.MAX_VALUE));
break;
case DECIMAL_PATTERN:
generators.addGenerator(category, path, new RandomDecimalGenerator(10));
break;
case HEX_PATTERN:
generators.addGenerator(category, path, new RandomHexadecimalGenerator(10));
break;
case ALPHA_NUMERIC_PATTERN:
generators.addGenerator(category, path, new RandomStringGenerator(10));
break;
case UUID_PATTERN:
generators.addGenerator(category, path, new UuidGenerator());
break;
case ANY_DATE_PATTERN:
generators.addGenerator(category, path, new DateGenerator());
break;
case ANY_TIME_PATTERN:
generators.addGenerator(category, path, new TimeGenerator());
break;
case ANY_DATE_TIME_PATTERN:
generators.addGenerator(category, path, new DateTimeGenerator());
break;
case TRUE_OR_FALSE_PATTERN:
generators.addGenerator(category, path, RandomBooleanGenerator.INSTANCE);
break;
default:
generators.addGenerator(category, path, new RegexGenerator(regexProperty.pattern()));
break;
}
}
}
}

View File

@@ -1,4 +0,0 @@
org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter
org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
org.springframework.cloud.contract.stubrunner.PactStubDownloaderBuilder

View File

@@ -1,165 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.stubrunner
import java.nio.file.Files
import au.com.dius.pact.core.model.Pact
import au.com.dius.pact.core.model.PactSource
import au.com.dius.pact.provider.junitsupport.loader.PactLoader
import au.com.dius.pact.core.support.expressions.ValueResolver
import com.github.tomakehurst.wiremock.stubbing.StubMapping
import spock.lang.Ignore
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter
/**
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
@Ignore("Flakey")
class PactStubDownloaderBuilderSpec extends Specification {
def "should retrieve pacts from broker"() throws IOException {
given:
Collection<Pact> pacts = new PactContractConverter().
convertTo([Contract.make {
request {
url "/foo"
method GET()
}
response {
status OK()
}
}, Contract.make {
request {
url "/bar"
method GET()
}
response {
status OK()
}
}])
StubRunnerOptions options = new StubRunnerOptionsBuilder()
.withProperties(props())
.build()
PactStubDownloader downloader = new PactStubDownloader(options) {
@Override
PactLoader pactBrokerLoader(ValueResolver resolver,
List<String> tags) {
return new PactLoader() {
@Override
List<Pact> load(String providerName) {
return pacts
}
@Override
PactSource getPactSource() {
return null
}
}
}
}
when:
Map.Entry<StubConfiguration, File> entry = downloader
.
downloadAndUnpackStubJar(new StubConfiguration("com.example:bobby:+:classifier"))
then:
entry != null
entry.getValue().exists()
File contracts = new File(entry.getValue(), "com/example/bobby/contracts")
contracts.exists()
contracts.list() != null
File mappings = new File(entry.getValue(), "com/example/bobby/mappings")
mappings.exists()
mappings.list() != null
mappings.list().size() == 2
StubMapping.buildFrom(new String(Files.
readAllBytes(mappings.listFiles()[0].toPath())))
StubMapping.buildFrom(new String(Files.
readAllBytes(mappings.listFiles()[1].toPath())))
}
Map<String, String> props() {
Map<String, String> map = new HashMap<>()
// map.put("pactbroker.host", "localhost")
// map.put("pactbroker.port", String.valueOf(this.port))
// map.put("pactbroker.host", "test.pact.dius.com.au")
// map.put("pactbroker.port", "443")
// map.put("pactbroker.protocol", "https")
// map.put("pactbroker.auth.scheme", "Basic")
// map.put("pactbroker.auth.username", "dXfltyFMgNOFZAxr8io9wJ37iUpY42M")
// map.put("pactbroker.auth.password", "O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1")
return map
}
// @After
// void tearDown() {
// SnapshotRecordResult recording = WireMock.stopRecording()
// List<StubMapping> mappings = recording.getStubMappings()
// storeMappings(mappings)
// }
// private void recordFromBroker() {
// WireMock.startRecording(WireMock.recordSpec()
// .forTarget("https://test.pact.dius.com.au")
// .extractTextBodiesOver(9999999L)
// .extractBinaryBodiesOver(9999999L)
// .makeStubsPersistent(false))
// }
// private void storeMappings(List<StubMapping> mappings) {
// try {
// File proxiedStubs = new File("target/stubs")
// proxiedStubs.mkdirs()
// for (StubMapping mapping : mappings) {
// File stub = new File(proxiedStubs, "foo" + ".json")
// stub.createNewFile()
// Files.write(stub.toPath(), mapping.toString().getBytes())
// }
// } catch (Exception e) {
// throw new RuntimeException(e)
// }
// }
def "should retrieve pacts from broker using stubrunner options"() throws IOException {
given:
StubRunnerOptions options = new StubRunnerOptionsBuilder()
.withStubRepositoryRoot("pact://https://test.pact.dius.com.au:443")
.withUsername("dXfltyFMgNOFZAxr8io9wJ37iUpY42M")
.withPassword("O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1")
.build()
PactStubDownloader downloader = new PactStubDownloader(options)
when:
Map.Entry<StubConfiguration, File> entry = downloader
.
downloadAndUnpackStubJar(new StubConfiguration("com.example:bobby:+:classifier"))
then:
entry != null
entry.getValue().exists()
File contracts = new File(entry.getValue(), "com/example/bobby/contracts")
contracts.exists()
contracts.list() != null
contracts.list().size() > 0
File mappings = new File(entry.getValue(), "com/example/bobby/mappings")
mappings.exists()
mappings.list() != null
mappings.list().size() > 0
}
}

View File

@@ -1,847 +0,0 @@
/*
* Copyright 2013-2020 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 org.springframework.cloud.contract.verifier.spec.pact
import java.nio.file.Files
import au.com.dius.pact.core.model.Pact
import au.com.dius.pact.core.model.PactSpecVersion
import groovy.json.JsonOutput
import org.skyscreamer.jsonassert.JSONAssert
import spock.lang.Issue
import spock.lang.Specification
import spock.lang.Subject
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.TestGenerator
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter
import org.springframework.core.io.Resource
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
/**
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class PactContractConverterSpec extends Specification {
File pactJson = new File(PactContractConverterSpec.getResource("/pact/pact.json").
toURI())
File pact509Json = new File(PactContractConverterSpec.
getResource("/pact/pact_509.json").toURI())
File pactv2Json = new File(PactContractConverterSpec.
getResource("/pact/pact_v2.json").toURI())
File pactv3Json = new File(PactContractConverterSpec.
getResource("/pact/pact_v3.json").toURI())
File pactv3Issue889Json = new File(PactContractConverterSpec.
getResource("/pact/pact_v3_issue_889.json").toURI())
File pactv3MessagingJson = new File(PactContractConverterSpec.
getResource("/pact/pact_v3_messaging.json").toURI())
File pactv3UnsupportedRuleLogicJson = new File(PactContractConverterSpec.
getResource("/pact/pact_v3_unsupported_rule_logic.json").toURI())
File pact1043Json = new File(PactContractConverterSpec.
getResource("/pact/for-test-generation/pact_1043.json").toURI())
File pact1134Json = new File(PactContractConverterSpec.
getResource("/pact/1134/pact_1134.json").toURI())
@Subject
PactContractConverter converter = new PactContractConverter()
def "should accept json files that are pact files"() {
expect:
converter.isAccepted(pactJson)
}
def "should reject json files that are invalid pact files"() {
given:
File invalidPact = new File(PactContractConverterSpec.
getResource("/pact/invalid_pact.json").toURI())
expect:
!converter.isAccepted(invalidPact)
}
def "should convert from pact to contract"() {
given:
Contract expectedContract = Contract.make {
description("a retrieve Mallory request a user with username 'username' and password 'password' exists")
request {
method(GET())
url("/mallory") {
queryParameters {
parameter("name", "ron")
parameter("status", "good")
}
}
headers {
header(contentType(), applicationJson())
}
body(id: "123", method: "create")
bodyMatchers {
jsonPath('$.id', byRegex("[0-9]{3}"))
}
}
response {
status(200)
headers {
header(contentType(), applicationJson())
}
body([[
[email : "rddtGwwWMEhnkAPEmsyE",
id : "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
userName: "AJQrokEGPAVdOHprQpKP"]
]])
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id',
byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
maxOccurrence(5)
})
jsonPath('$[0][*].userName', byType())
}
}
}
when:
Collection<Contract> contracts = converter.convertFrom(pactJson)
then:
contracts == [expectedContract]
}
@Issue("#509")
def "should convert from pact with matching rules to whole body to contract"() {
given:
Contract expectedContract = Contract.make {
description("a request to POST a person provider accepts a new person")
request {
method(POST())
url("/user-service/users")
headers {
header(contentType(), applicationJson())
}
body(firstName: "Arthur", lastName: "Dent")
}
response {
status(201)
headers {
header(contentType(), applicationJsonUtf8())
}
body(firstName: "Arthur", id: 42, lastName: "Dent")
bodyMatchers {
jsonPath('''$.['firstName']''', byType())
jsonPath('''$.['id']''', byType())
jsonPath('''$.['lastName']''', byType())
}
}
}
when:
Collection<Contract> contracts = converter.convertFrom(pact509Json)
then:
contracts == [expectedContract]
}
def "should convert from contract to pact"() {
given:
Collection<Contract> inputContracts = [
Contract.make {
name("my_consumer___my_producer___testname")
description("a retrieve Mallory request")
request {
method(GET())
url("/mallory") {
queryParameters {
parameter("name", "ron")
parameter("status", "good")
}
}
headers {
contentType(applicationJson())
}
body(
id: 123,
method: $(stub(regex("[0][1][2]"))),
something: "foo"
)
bodyMatchers {
jsonPath('$.id', byRegex("[0-9]{3}"))
jsonPath('$.something', byEquality())
}
}
response {
status(200)
headers {
contentType(applicationJson())
}
body([[
[email : "rddtGwwWMEhnkAPEmsyE",
id : "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
number :
$(producer(regex("[0-9]{3}")), consumer(923)),
positiveInteger : 1234567890,
negativeInteger : -1234567890,
positiveDecimalNumber: 123.4567890,
negativeDecimalNumber: -123.4567890,
something : "foo",
userName : "AJQrokEGPAVdOHprQpKP",
nullValue : null]
]])
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id',
byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
minOccurrence(1)
maxOccurrence(5)
})
jsonPath('$[0][*].number', byRegex(number()))
jsonPath('$[0][*].positiveInteger', byRegex(anInteger()))
jsonPath('$[0][*].negativeInteger', byRegex(anInteger()))
jsonPath('$[0][*].positiveDecimalNumber', byRegex(aDouble()))
jsonPath('$[0][*].negativeDecimalNumber', byRegex(aDouble()))
jsonPath('$[0][*].userName', byType())
jsonPath('$[0][*].something', byEquality())
jsonPath('$[0][*].nullValue', byNull())
}
}
}
]
String expectedJson = '''
{
"provider": {
"name": "my_producer"
},
"consumer": {
"name": "my_consumer"
},
"interactions": [
{
"description": "a retrieve Mallory request",
"request": {
"method": "GET",
"path": "\\/mallory",
"query": {
"name": ["ron"],
"status": ["good"]
},
"headers": {
"Content-Type": "application\\/json"
},
"body": {
"id": 123,
"method": "012",
"something": "foo"
},
"matchingRules": {
"body": {
"$.id": {
"matchers": [{
"match": "regex",
"regex": "[0-9]{3}"
}]
},
"$.something": {
"matchers": [{
"match": "equality"
}]
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application\\/json"
},
"body": [
[
{
"email": "rddtGwwWMEhnkAPEmsyE",
"id": "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
"number": 923,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890,
"userName": "AJQrokEGPAVdOHprQpKP",
"something": "foo",
"nullValue": null
}
]
],
"matchingRules": {
"body": {
"$[0][*].email": {
"matchers": [{
"match": "type"
}]
},
"$[0][*].id": {
"matchers": [{
"match": "regex",
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
}]
},
"$[0]": {
"matchers": [{
"match": "type",
"min": 1,
"max": 5
}]
},
"$[0][*].number": {
"matchers": [{
"match": "number"
}]
},
"$[0][*].positiveInteger": {
"matchers": [{
"match": "integer"
}]
},
"$[0][*].negativeInteger": {
"matchers": [{
"match": "integer"
}]
},
"$[0][*].positiveDecimalNumber": {
"matchers": [{
"match": "decimal"
}]
},
"$[0][*].negativeDecimalNumber": {
"matchers": [{
"match": "decimal"
}]
},
"$[0][*].userName": {
"matchers": [{
"match": "type"
}]
},
"$[0][*].something": {
"matchers": [{
"match": "equality"
}]
},
"$[0][*].nullValue": {
"matchers": [{
"match": "null"
}]
}
}
}
}
}
],
"metadata": {
"pact-jvm": {
"version": "4.3.15"
}
}
}
'''
when:
Pact pact = converter.convertTo(inputContracts).get(0)
then:
String actual = JsonOutput.toJson(pact.toMap(PactSpecVersion.V3))
JSONAssert.assertEquals(expectedJson, actual, false)
}
def "should fail to convert from contract to pact when contract has execution property in request"() {
given:
Collection<Contract> inputContracts = [
Contract.make {
request {
method(GET())
url("/mallory")
body(
id: $(c("foo"), p(execute("foo")))
)
}
response {
status(200)
}
}
]
when:
converter.convertTo(inputContracts)
then:
def e = thrown(UnsupportedOperationException)
e.message.contains("execution property")
}
def "should fail to convert from contract to pact when contract has execution property in response"() {
given:
Collection<Contract> inputContracts = [
Contract.make {
request {
method(GET())
url("/mallory")
}
response {
status(200)
body(
id: $(c(execute("foo")), p("foo"))
)
}
}
]
when:
converter.convertTo(inputContracts)
then:
def e = thrown(UnsupportedOperationException)
e.message.contains("execution property")
}
def "should convert contracts from samples to pacts"() {
given:
Resource[] contractResources = new PathMatchingResourcePatternResolver().
getResources("contracts/*.groovy")
Resource[] pactResources = new PathMatchingResourcePatternResolver().
getResources("contracts/*.json")
Map<String, Collection<Contract>> contracts = contractResources.
collectEntries {
[(it.filename): ContractVerifierDslConverter.
convertAsCollection(new File("/"), it.getFile())]
}
Map<String, String> jsonPacts = pactResources.
collectEntries { [(it.filename): it.getFile().text] }
when:
Map<String, Collection<Pact>> pacts = contracts.entrySet().
collectEntries { [(it.key): converter.convertTo(it.value)] }
then:
pacts.entrySet().each {
String convertedPactAsText = JsonOutput.
toJson(it.value[0].toMap(PactSpecVersion.V3))
String pactFileName = it.key.replace("groovy", "json")
println "File name [${it.key}]"
JSONAssert.
assertEquals(jsonPacts.get(pactFileName), convertedPactAsText, true)
}
}
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 pacts to strings"() {
given:
List<Contract> contracts = ContractVerifierDslConverter.
convertAsCollection(new File("/"),
new File("src/test/resources/contracts/grouped/shouldWorkWithBeer.groovy"))
and:
Collection<Pact> pacts = converter.convertTo(contracts)
when:
Map<String, byte[]> strings = converter.store(pacts)
then:
strings.size() == 1
strings.keySet().first().
startsWith("10-04-pact-consumer_10-05-pact-producer_")
strings.keySet().first().endsWith(".json")
JSONAssert.assertEquals(
new File("src/test/resources/contracts/grouped/shouldWorkWithBeer.json").text,
new String(strings.values().first()), false)
}
def "should convert from pact v2 to two SC contracts"() {
given:
Collection<Contract> expectedContracts = [
Contract.make {
description("get all users for max a user with an id named 'user' exists")
request {
method(GET())
url("/idm/user")
}
response {
status(200)
headers {
header(contentType(), applicationJsonUtf8())
}
body([[
[email : "rddtGwwWMEhnkAPEmsyE",
id : "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
userName: "AJQrokEGPAVdOHprQpKP"]
]])
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id',
byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
maxOccurrence(5)
})
jsonPath('$[0][*].userName', byType())
}
}
},
Contract.make {
description("get all users for min a user with an id named 'user' exists")
request {
method(GET())
url("/idm/user")
}
response {
status(200)
headers {
header(contentType(), applicationJsonUtf8())
}
body([[
[email : "DPvAfkCZpOBZWzKYiDMC",
id : "95d0371b-bf30-4943-90a8-8bb1967c4cb2",
userName: "GIUlVKoiLdHLYNKGbcSy"]
]])
bodyMatchers {
jsonPath('$[0][*].email', byType())
jsonPath('$[0][*].id',
byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
jsonPath('$[0]', byType() {
minOccurrence(5)
})
jsonPath('$[0][*].userName', byType())
}
}
}
]
when:
Collection<Contract> contracts = converter.convertFrom(pactv2Json)
then:
contracts == expectedContracts
}
def "should convert from pact v3 to three SC contracts"() {
given:
Collection<Contract> expectedContracts = [
Contract.make {
description("java test interaction with a DSL array body")
request {
method(GET())
url("/")
headers {
header(contentType(), applicationJsonUtf8())
header("Some-Header",
$(c(regex("[a-zA-Z]{9}")), p("someValue")))
header("someHeaderWithJsonContent", '{"issue":"#595"}')
}
}
response {
status(200)
headers {
header(contentType(), applicationJsonUtf8())
header("Some-Header",
$(c("someValue"), p(regex("[a-zA-Z]{9}"))))
header("someHeaderWithJsonContent", '{"issue":"#595"}')
}
body([
[
"dob" : "07/19/2016",
"id" : 8958464620,
"name" : "Rogger the Dogger",
"timestamp" : "2016-07-19T12:14:39",
"nullValue" : null,
"aNumber" : 1234567890,
"positiveInteger" : 1234567890,
"negativeInteger" : -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
],
[
"dob" : "07/19/2016",
"id" : 4143398442,
"name" : "Cat in the Hat",
"timestamp" : "2016-07-19T12:14:39",
"nullValue" : null,
"aNumber" : 1234567890,
"positiveInteger" : 1234567890,
"negativeInteger" : -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
]
])
bodyMatchers {
jsonPath('$[0].id', byType())
jsonPath('$[1].id', byType())
jsonPath('$[*].nullValue', byNull())
jsonPath('$[*].aNumber', byRegex(number()))
jsonPath('$[*].positiveInteger', byRegex(anInteger()))
jsonPath('$[*].negativeInteger', byRegex(anInteger()))
jsonPath('$[*].positiveDecimalNumber', byRegex(aDouble()))
jsonPath('$[*].negativeDecimalNumber', byRegex(aDouble()))
}
}
},
Contract.make {
description("test interaction with a array body with templates")
request {
method(GET())
url("/")
}
response {
status(200)
headers {
header(contentType(), applicationJsonUtf8())
}
body([
[
"dob" : "2016-07-19",
"id" : 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
],
[
"dob" : "2016-07-19",
"id" : 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
],
[
"dob" : "2016-07-19",
"id" : 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
]
])
bodyMatchers {
jsonPath('$[2].name', byType())
jsonPath('$[0].id', byType())
jsonPath('$[1].id', byType())
jsonPath('$[2].id', byType())
jsonPath('$[1].name', byType())
jsonPath('$[0].name', byType())
jsonPath('$[0].dob', byDate())
}
}
},
Contract.make {
description("test interaction with an array like matcher")
request {
method(GET())
url("/")
}
response {
status(200)
headers {
header(contentType(), applicationJsonUtf8())
}
body([
"data": [
"array1": [[
"dob" : "2016-07-19",
"id" : 1600309982,
"name": "FVsWAGZTFGPLhWjLuBOd"
]],
"array2": [[
"address": "127.0.0.1",
"name" : "jvxrzduZnwwxpFYrQnpd"
]],
"array3": [[
[
"itemCount": 652571349
]
]]
],
"id" : 7183997828
])
bodyMatchers {
jsonPath('$.data.array3[0]', byType() {
maxOccurrence(5)
})
jsonPath('$.data.array1', byType() {
minOccurrence(0)
})
jsonPath('$.data.array2', byType() {
minOccurrence(1)
})
jsonPath('$.id', byType())
jsonPath('$.data.array2[*].name', byType())
jsonPath('$.data.array2[*].address',
byRegex("(\\d{1,3}\\.)+\\d{1,3}"))
jsonPath('$.data.array1[*].name', byType())
jsonPath('$.data.array1[*].id', byType())
}
}
}
]
when:
Collection<Contract> contracts = converter.convertFrom(pactv3Json)
then:
contracts == expectedContracts
}
@Issue("#889")
def "should not throw an exception when parsing Pact file with different matchers"() {
when:
converter.convertFrom(pactv3Issue889Json)
then:
noExceptionThrown()
}
def "should convert from pact v3 messaging to one SC message contract"() {
given:
Collection<Contract> expectedContracts = [
Contract.make {
description()
label 'message sent to activemq:output'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo 'activemq:output'
body([
bookName: "foo"
])
headers {
header('BOOK-NAME', 'foo')
header(messagingContentType(), applicationJson())
}
bodyMatchers {
jsonPath('$.bookName', byType())
}
}
}
]
when:
Collection<Contract> contracts = converter.convertFrom(pactv3MessagingJson)
then:
contracts == expectedContracts
}
def "should fail to convert a pact v3 contract with unsupported rule logic"() {
when:
converter.convertFrom(pactv3UnsupportedRuleLogicJson)
then:
def e = thrown(UnsupportedOperationException)
e.message.
contains("Currently only the AND combination rule logic is supported")
}
@Issue("1043")
def "should generate a test from pact and not contain a check for an empty array"() {
given:
File output = Files.createTempDirectory("pact").toFile()
output.mkdirs()
when:
new TestGenerator(new ContractVerifierConfigProperties(contractsDslDir: pact1043Json.parentFile, generatedTestSourcesDir: output, generatedTestResourcesDir: output, basePackageForTests: "example")).generate()
then:
File generatedTest = new File(output, "example/ContractVerifierTest.java")
String generatedTestText = generatedTest.text
!generatedTestText.contains('''assertThatJson(parsedJson).array().array("['authors']").isEmpty()''')
}
@Issue("1134")
def "should work properly with cookies when reading a pact file as contract"() {
given:
File output = Files.createTempDirectory("pact").toFile()
output.mkdirs()
when:
new TestGenerator(new ContractVerifierConfigProperties(contractsDslDir: pact1134Json.parentFile, generatedTestSourcesDir: output, generatedTestResourcesDir: output, basePackageForTests: "example")).generate()
then:
File generatedTest = new File(output, "example/ContractVerifierTest.java")
String generatedTestText = generatedTest.text
generatedTestText.contains('''cookie("a", "1")''')
generatedTestText.contains('''cookie("b", "2")''')
generatedTestText.contains('''assertThat(response.cookie("c")).isNotNull()''')
generatedTestText.contains('''assertThat(response.cookie("c")).isEqualTo("1")''')
generatedTestText.contains('''assertThat(response.cookie("d")).isNotNull()''')
generatedTestText.contains('''assertThat(response.cookie("d")).isEqualTo("2")''')
}
@Issue("1134")
def "should work properly with cookies when converting contract to pact"() {
given:
Collection<Contract> expectedContracts = [
Contract.make {
description("A successful Api GET call get")
request {
method(GET())
url("/books")
// cause Pact stores cookies in headers
headers {
}
cookies {
cookie("a", "1")
cookie("b", "2")
}
}
response {
status(200)
// cause Pact stores cookies in headers
headers {
}
cookies {
cookie("c", "1")
cookie("d", "2")
}
}
}
]
when:
Collection<Contract> contracts = converter.convertFrom(pact1134Json)
then:
contracts == expectedContracts
}
@Issue("1277")
def "should work properly with json body"() {
when:
converter.convertTo([
Contract.make {
request {
method 'PUT'
url '/api/admins/1'
body('''{
"username" : "username",
"password" : "password",
"roles" : [ "ADMIN" ]
}''')
headers {
header('''Content-Type''', '''application/json;charset=UTF-8''')
}
}
response {
status 200
body('''{
"admin" : {
"adminId" : 1,
"username" : "username",
"roles" : [ "ADMIN" ]
}
}''')
headers {
header('''Content-Type''', '''application/json;charset=UTF-8''')
}
}
}
])
then:
noExceptionThrown()
}
}
// file creator
/*
pacts.entrySet().each {
new File("target/${it.key.replace("groovy", "json")}").text = JsonOutput.toJson(it.value.toMap(PactSpecVersion.V3))
}
*/

View File

@@ -1,51 +0,0 @@
/*
* Copyright 2020-2020 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 org.springframework.cloud.contract.verifier.spec.pact;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import org.assertj.core.api.BDDAssertions;
import org.junit.jupiter.api.Test;
class PactMetaDataTests {
YAMLMapper mapper = new YAMLMapper();
@Test
void should_parse_the_metadata_entry() throws JsonProcessingException {
// @formatter:off
String yamlEntry = ""
+ "pact:\n"
+ " providerStates:\n"
+ " - name: state1\n"
+ " params:\n"
+ " id: 1\n"
+ " value: value1\n"
+ " - name: state2\n"
+ " params:\n"
+ " id: 2\n"
+ " value: value2";
// @formatter:on
PactMetaData metadata = PactMetaData
.fromMetadata(this.mapper.readerForMapOf(Object.class).readValue(yamlEntry));
String serialized = this.mapper.writer().forType(PactMetaData.class).writeValueAsString(metadata);
BDDAssertions.then(serialized).isEqualToNormalizingPunctuationAndWhitespace(yamlEntry.replace("pact:\n", ""));
}
}

View File

@@ -1,106 +0,0 @@
/*
* Copyright 2013-2020 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.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

@@ -1,153 +0,0 @@
{
"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-jvm": {
"version": "4.3.15"
}
}
}

View File

@@ -1,80 +0,0 @@
/*
* Copyright 2013-2020 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
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'POST'
url '/'
body([
someInteger : $(c(anyInteger()), p(1234567890)),
someDecimal : $(c(anyDouble()), p(123.123)),
someHex : $(c(anyHex()), p('DEADC0DE')),
someAlphaNumeric:
$(c(anyAlphaNumeric()), p('Some alpha numeric string with 1234567890')),
someUUID : $(c(anyUuid()), p('00000000-0000-0000-0000-000000000000')),
someDate : $(c(anyDate()), p('2018-03-26')),
someTime : $(c(anyTime()), p('13:37:00')),
someDateTime : $(c(anyDateTime()), p('2018-03-26 13:37:00')),
someBoolean : $(c(anyBoolean()), p('true')),
someRegex : $(c(regex('[0-9]{10}')), p(1234567890))
])
headers {
contentType('application/json')
}
}
response {
status OK()
body([
someInteger : $(c(1234567890), p(anyInteger())),
someDecimal : $(c(123.123), p(anyDouble())),
someHex : $(c('DEADC0DE'), p(anyHex())),
someAlphaNumeric:
$(c('Some alpha numeric string with 1234567890'), p(anyAlphaNumeric())),
someUUID : $(c('00000000-0000-0000-0000-000000000000'), p(anyUuid())),
someDate : $(c('2018-03-26'), p(anyDate())),
someTime : $(c('13:37:00'), p(anyTime())),
someDateTime : $(c('2018-03-26 13:37:00'), p(anyDateTime())),
someBoolean : $(c('true'), p(anyBoolean())),
someRegex : $(c(1234567890), p(regex('[0-9]{10}')))
])
headers {
contentType('application/json')
}
}
metadata([
pact: [
providerStates:
[
[
name: "someState1",
params: [
id: 1,
value: "someValue1"
]
],
[
name: "someState2",
params: [
id: 2,
value: "someValue2"
]
]
]
]
])
}

View File

@@ -1,296 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "POST",
"path": "/",
"headers": {
"Content-Type": "application/json"
},
"body": {
"someInteger": 1234567890,
"someDecimal": 123.123,
"someHex": "DEADC0DE",
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDate": "2018-03-26",
"someTime": "13:37:00",
"someDateTime": "2018-03-26 13:37:00",
"someBoolean": "true",
"someRegex": 1234567890
},
"generators": {
"body": {
"$.someInteger": {
"type": "RandomInt",
"min": 0,
"max": 2147483647
},
"$.someDecimal": {
"type": "RandomDecimal",
"digits": 10
},
"$.someHex": {
"type": "RandomHexadecimal",
"digits": 10
},
"$.someAlphaNumeric": {
"type": "RandomString",
"size": 10
},
"$.someUUID": {
"type": "Uuid"
},
"$.someDate": {
"type": "Date"
},
"$.someTime": {
"type": "Time"
},
"$.someDateTime": {
"type": "DateTime"
},
"$.someBoolean": {
"type": "RandomBoolean"
},
"$.someRegex": {
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine": "AND"
}
},
"body": {
"$.someHex": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someAlphaNumeric": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someUUID": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someDate": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someTime": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someDateTime": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someBoolean": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"someInteger": 1234567890,
"someDecimal": 123.123,
"someHex": "DEADC0DE",
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDate": "2018-03-26",
"someTime": "13:37:00",
"someDateTime": "2018-03-26 13:37:00",
"someBoolean": "true",
"someRegex": 1234567890
},
"generators": {
"body": {
"$.someInteger": {
"type": "RandomInt",
"min": 0,
"max": 2147483647
},
"$.someDecimal": {
"type": "RandomDecimal",
"digits": 10
},
"$.someHex": {
"type": "RandomHexadecimal",
"digits": 10
},
"$.someAlphaNumeric": {
"type": "RandomString",
"size": 10
},
"$.someUUID": {
"type": "Uuid"
},
"$.someDate": {
"type": "Date"
},
"$.someTime": {
"type": "Time"
},
"$.someDateTime": {
"type": "DateTime"
},
"$.someBoolean": {
"type": "RandomBoolean"
},
"$.someRegex": {
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine": "AND"
}
},
"body": {
"$.someHex": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someAlphaNumeric": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someUUID": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someDate": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someTime": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someDateTime": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.someBoolean": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
},
"providerStates": [
{
"name": "someState1",
"params": {
"id": 1,
"value": "someValue1"
}
},
{
"name": "someState2",
"params": {
"id": 2,
"value": "someValue2"
}
}
]
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "4.3.15"
}
}
}

View File

@@ -1,82 +0,0 @@
/*
* Copyright 2013-2020 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
org.springframework.cloud.contract.spec.Contract.make {
request { // (1)
method 'PUT' // (2)
url '/fraudcheck' // (3)
body([ // (4)
clientId : $(c(regex('[0-9]{10}')), p("8532032713")),
loanAmount: 99999
])
headers { // (5)
contentType('application/vnd.fraud.v1+json')
}
}
response { // (6)
status OK() // (7)
body([ // (8)
fraudCheckStatus: "FRAUD",
rejectionReason : "Amount too high"
])
headers { // (9)
contentType('application/vnd.fraud.v1+json')
}
}
}
/*
Since we don't want to force on the user to hardcode values of fields that are dynamic
(timestamps, database ids etc.), one can parametrize those entries. If you wrap your field's
value in a `$(...)` or `value(...)` and provide a dynamic value of a field then
the concrete value will be generated for you. If you want to be really explicit about
which side gets which value you can do that by using the `value(consumer(...), producer(...))` notation.
That way what's present in the `consumer` section will end up in the produced stub. What's
there in the `producer` will end up in the autogenerated test. If you provide only the
regular expression side without the concrete value then Spring Cloud Contract will generate one for you.
From the Consumer perspective, when shooting a request in the integration test:
(1) - If the consumer sends a request
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `clientId` that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`
(6) - then the response will be sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`
From the Producer perspective, in the autogenerated producer-side test:
(1) - A request will be sent to the producer
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `clientId` that will have a generated value that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`
(6) - then the test will assert if the response has been sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` matching `application/vnd.fraud.v1+json.*`
*/

View File

@@ -1,104 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "PUT",
"path": "/fraudcheck",
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"clientId": "8532032713",
"loanAmount": 99999
},
"generators": {
"body": {
"$.clientId": {
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine": "AND"
}
},
"body": {
"$.clientId": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"fraudCheckStatus": "FRAUD",
"rejectionReason": "Amount too high"
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine": "AND"
}
},
"body": {
"$.fraudCheckStatus": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.rejectionReason": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "4.3.15"
}
}
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright 2013-2020 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
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url '/fraudcheck'
body("""
{
"clientId":"${
value(consumer(regex('[0-9]{10}')), producer('1234567890'))
}",
"loanAmount":123.123
}
"""
)
headers {
contentType("application/vnd.fraud.v1+json")
}
}
response {
status OK()
body(
fraudCheckStatus: "OK",
rejectionReason: $(consumer(null),
producer(execute('assertThatRejectionReasonIsNull($it)')))
)
headers {
contentType("application/vnd.fraud.v1+json")
}
}
}

View File

@@ -1,96 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "PUT",
"path": "/fraudcheck",
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"clientId": "1234567890",
"loanAmount": 123.123
},
"generators": {
"body": {
"$.clientId": {
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine": "AND"
}
},
"body": {
"$.clientId": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"fraudCheckStatus": "OK",
"rejectionReason": null
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine": "AND"
}
},
"body": {
"$.fraudCheckStatus": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "4.3.15"
}
}
}

View File

@@ -1,53 +0,0 @@
/*
* Copyright 2013-2020 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.Contract
[
Contract.make {
request {
name "should count all frauds"
method GET()
url '/frauds'
}
response {
status OK()
body([
count: 200
])
headers {
contentType("application/vnd.fraud.v1+json")
}
}
},
Contract.make {
request {
method GET()
url '/drunks'
}
response {
status OK()
body([
count: 100
])
headers {
contentType("application/vnd.fraud.v1+json")
}
}
}
]

View File

@@ -1,76 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "GET",
"path": "/frauds"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"count": 200
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine": "AND"
}
}
}
}
},
{
"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"
}
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "4.3.15"
}
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2013-2020 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.Contract
[
Contract.make {
label 'some_label'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('activemq:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
messagingContentType(applicationJson())
}
}
}
]

View File

@@ -1,45 +0,0 @@
{
"consumer": {
"name": "Consumer"
},
"provider": {
"name": "Provider"
},
"messages": [
{
"description": "message sent to activemq:output",
"metaData": {
"BOOK-NAME": "foo",
"contentType": "application/json"
},
"contents": {
"bookName": "foo"
},
"providerStates": [
{
"name": "bookReturnedTriggered()"
}
],
"matchingRules": {
"body": {
"$.bookName": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "4.3.15"
}
}
}

View File

@@ -1,39 +0,0 @@
{
"provider": {
"name": "cookie-service"
},
"consumer": {
"name": "cookie-consumer"
},
"interactions": [
{
"description": "A successful Api GET call",
"request": {
"method": "GET",
"path": "/books",
"headers": {
"Cookie": "a=1;b=2"
}
},
"response": {
"status": 200,
"headers": {
"Cookie": "c=1;d=2"
}
},
"providerStates": [
{
"name": "get"
}
]
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -1,115 +0,0 @@
{
"provider": {
"name": "book-catalog-service"
},
"consumer": {
"name": "books-client-catalog-rest-consumer"
},
"interactions": [
{
"description": "A successful Api GET call",
"request": {
"method": "GET",
"path": "/books",
"headers": {
"Content-Type": "application/json;charset=UTF-8"
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"body": [
{
"isbn": "978-3-86680-192-9",
"id": 100,
"title": "A book",
"authors": [
{
"firstName": "string",
"lastName": "string"
}
]
}
],
"matchingRules": {
"body": {
"$[0].id": {
"matchers": [
{
"match": "integer"
}
],
"combine": "AND"
},
"$[0].title": {
"matchers": [
{
"match": "regex",
"regex": ".*"
}
],
"combine": "AND"
},
"$[0].authors[0].firstName": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$[0].authors[0].lastName": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$[0].isbn": {
"matchers": [
{
"match": "regex",
"regex": "[0-9]{3}-[0-9]{1}-[0-9]{5}-[0-9]{3}-[0-9]{1}"
}
],
"combine": "AND"
}
}
},
"generators": {
"body": {
"$[0].id": {
"type": "RandomInt",
"min": 0,
"max": 2147483647
},
"$[0].authors[0].firstName": {
"type": "RandomString",
"size": 20
},
"$[0].authors[0].lastName": {
"type": "RandomString",
"size": 20
}
}
}
},
"providerStates": [
{
"name": "get"
}
]
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -1,62 +0,0 @@
{
"provider": {
"name": "Alice Service"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "a retrieve Mallory request",
"provider_state": "a user with username 'username' and password 'password' exists",
"request": {
"method": "GET",
"path": "/mallory",
"query": "name=ron&status=good",
"body": {
"id": "123",
"method": "create"
},
"headers": {
"Content-Type": "application/json"
},
"matchingRules": {
"$.body.id": {
"match": "regex",
"regex": "[0-9]{3}"
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": [
[
{
"email": "rddtGwwWMEhnkAPEmsyE",
"id": "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
"userName": "AJQrokEGPAVdOHprQpKP"
}
]
],
"matchingRules": {
"$.body[0][*].email": {
"match": "type"
},
"$.body[0][*].id": {
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
},
"$.body[0]": {
"max": 5,
"match": "type"
},
"$.body[0][*].userName": {
"match": "type"
}
}
}
}
]
}

View File

@@ -1,46 +0,0 @@
{
"consumer": {
"name": "ui"
},
"provider": {
"name": "userservice"
},
"interactions": [
{
"description": "a request to POST a person",
"providerState": "provider accepts a new person",
"request": {
"method": "POST",
"path": "/user-service/users",
"headers": {
"Content-Type": "application/json"
},
"body": {
"firstName": "Arthur",
"lastName": "Dent"
}
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"body": {
"id": 42,
"firstName": "Arthur",
"lastName": "Dent"
},
"matchingRules": {
"$.body": {
"match": "type"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "2.0.0"
}
}
}

View File

@@ -1,94 +0,0 @@
{
"provider": {
"name": "266_provider"
},
"consumer": {
"name": "test_consumer"
},
"interactions": [
{
"description": "get all users for max",
"request": {
"method": "GET",
"path": "/idm/user"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"body": [
[
{
"email": "rddtGwwWMEhnkAPEmsyE",
"id": "eb0f8c17-c06a-479e-9204-14f7c95b63a6",
"userName": "AJQrokEGPAVdOHprQpKP"
}
]
],
"matchingRules": {
"$.body[0][*].email": {
"match": "type"
},
"$.body[0][*].id": {
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
},
"$.body[0]": {
"max": 5,
"match": "type"
},
"$.body[0][*].userName": {
"match": "type"
}
}
},
"providerState": "a user with an id named 'user' exists"
},
{
"description": "get all users for min",
"request": {
"method": "GET",
"path": "/idm/user"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"body": [
[
{
"email": "DPvAfkCZpOBZWzKYiDMC",
"id": "95d0371b-bf30-4943-90a8-8bb1967c4cb2",
"userName": "GIUlVKoiLdHLYNKGbcSy"
}
]
],
"matchingRules": {
"$.body[0][*].email": {
"match": "type"
},
"$.body[0][*].id": {
"regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
},
"$.body[0]": {
"min": 5,
"match": "type"
},
"$.body[0][*].userName": {
"match": "type"
}
}
},
"providerState": "a user with an id named 'user' exists"
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "3.2.11"
}
}
}

View File

@@ -1,330 +0,0 @@
{
"provider": {
"name": "test_provider_array"
},
"consumer": {
"name": "test_consumer_array"
},
"interactions": [
{
"description": "java test interaction with a DSL array body",
"request": {
"method": "GET",
"path": "/",
"headers": {
"Content-Type": "application/json;charset=UTF-8",
"Some-Header": "someValue",
"someHeaderWithJsonContent": "{\"issue\":\"#595\"}"
},
"matchingRules": {
"header": {
"Some-Header": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z]{9}"
}
]
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json;charset=UTF-8",
"Some-Header": "someValue",
"someHeaderWithJsonContent": "{\"issue\":\"#595\"}"
},
"body": [
{
"dob": "07/19/2016",
"id": 8958464620,
"name": "Rogger the Dogger",
"timestamp": "2016-07-19T12:14:39",
"nullValue": null,
"aNumber": 1234567890,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
},
{
"dob": "07/19/2016",
"id": 4143398442,
"name": "Cat in the Hat",
"timestamp": "2016-07-19T12:14:39",
"nullValue": null,
"aNumber": 1234567890,
"positiveInteger": 1234567890,
"negativeInteger": -1234567890,
"positiveDecimalNumber": 123.4567890,
"negativeDecimalNumber": -123.4567890
}
],
"matchingRules": {
"header": {
"Some-Header": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z]{9}"
}
]
}
},
"body": {
"$[0].id": {
"matchers": [
{
"match": "type"
}
]
},
"$[1].id": {
"matchers": [
{
"match": "type"
}
]
},
"$[*].nullValue": {
"matchers": [
{
"match": "null"
}
]
},
"$[*].aNumber": {
"matchers": [
{
"match": "number"
}
]
},
"$[*].positiveInteger": {
"matchers": [
{
"match": "integer"
}
]
},
"$[*].negativeInteger": {
"matchers": [
{
"match": "integer"
}
]
},
"$[*].positiveDecimalNumber": {
"matchers": [
{
"match": "decimal"
}
]
},
"$[*].negativeDecimalNumber": {
"matchers": [
{
"match": "decimal"
}
]
}
}
}
}
},
{
"description": "test interaction with a array body with templates",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"body": [
{
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
},
{
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
},
{
"dob": "2016-07-19",
"id": 1943791933,
"name": "ZSAICmTmiwgFFInuEuiK"
}
],
"matchingRules": {
"body": {
"$[2].name": {
"matchers": [
{
"match": "type"
}
]
},
"$[0].id": {
"matchers": [
{
"match": "type"
}
]
},
"$[1].id": {
"matchers": [
{
"match": "type"
}
]
},
"$[2].id": {
"matchers": [
{
"match": "type"
}
]
},
"$[1].name": {
"matchers": [
{
"match": "type"
}
]
},
"$[0].name": {
"matchers": [
{
"match": "type"
}
]
},
"$[0].dob": {
"matchers": [
{
"date": "yyyy-MM-dd"
}
]
}
}
}
}
},
{
"description": "test interaction with an array like matcher",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"body": {
"data": {
"array1": [
{
"dob": "2016-07-19",
"id": 1600309982,
"name": "FVsWAGZTFGPLhWjLuBOd"
}
],
"array2": [
{
"address": "127.0.0.1",
"name": "jvxrzduZnwwxpFYrQnpd"
}
],
"array3": [
[
{
"itemCount": 652571349
}
]
]
},
"id": 7183997828
},
"matchingRules": {
"body": {
"$.data.array3[0]": {
"matchers": [
{
"max": 5,
"match": "type"
}
]
},
"$.data.array1": {
"matchers": [
{
"min": 0,
"match": "type"
}
]
},
"$.data.array2": {
"matchers": [
{
"min": 1,
"match": "type"
}
]
},
"$.id": {
"matchers": [
{
"match": "type"
}
]
},
"$.data.array2[*].name": {
"matchers": [
{
"match": "type"
}
]
},
"$.data.array2[*].address": {
"matchers": [
{
"regex": "(\\d{1,3}\\.)+\\d{1,3}"
}
]
},
"$.data.array1[*].name": {
"matchers": [
{
"match": "type"
}
]
},
"$.data.array1[*].id": {
"matchers": [
{
"match": "type"
}
]
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.2.11"
}
}
}

View File

@@ -1,83 +0,0 @@
{
"consumer": {
"name": "order-manager"
},
"provider": {
"name": "lineitem-worker"
},
"messages": [
{
"description": "A message sent to order-exchange",
"metaData": {
"sentTo": "order-exchange"
},
"contents": {
"lineItemId": "string",
"orderId": "XXX-XXXXXX-000000-0000C",
"timestamp": 1550500138678,
"marketId": "MRKTDE",
"status": "PROCESSED"
},
"providerStates": [
{
"name": "statusMessageCreated()"
}
],
"matchingRules": {
"body": {
"$.timestamp": {
"matchers": [
{
"match": "number"
}
],
"combine": "AND"
},
"$.lineItemId": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.orderId": {
"matchers": [
{
"match": "regex",
"regex": "[A-Z]{3}-[A-Z]{6}-[0-9]{6}-[0-9A-F]{5}"
}
],
"combine": "AND"
},
"$.marketId": {
"matchers": [
{
"match": "regex",
"regex": "[A-Z]{6}"
}
],
"combine": "AND"
},
"$.status": {
"matchers": [
{
"match": "regex",
"regex": "[A-Z,_]*"
}
],
"combine": "AND"
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -1,46 +0,0 @@
{
"consumer": {
"name": "Consumer"
},
"provider": {
"name": "Provider"
},
"messages": [
{
"description": "message sent to activemq:output",
"metaData": {
"sentTo": "activemq:output",
"BOOK-NAME": "foo",
"contentType": "application/json"
},
"contents": {
"bookName": "foo"
},
"providerStates": [
{
"name": "bookReturnedTriggered()"
}
],
"matchingRules": {
"body": {
"$.bookName": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -1,51 +0,0 @@
{
"provider": {
"name": "test_unsupported_rule_logic"
},
"consumer": {
"name": "test_unsupported_rule_logic"
},
"interactions": [
{
"description": "test unsupported rule logic",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"body": [
{
"optionalField": 1234567890
}
],
"matchingRules": {
"body": {
"$[*].optionalField": {
"matchers": [
{
"match": "integer"
},
{
"match": "null"
}
],
"combine": "OR"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.2.11"
}
}
}