Added support for Stub Runner and Pact Broker

fixes gh-191
commit 533f1d3d0e8f6dcd37677bfef555fc2ea86bc1b1
Author: Tim Ysewyn <Tim.Ysewyn@me.com>
Date:   Wed Mar 7 15:56:48 2018 +0100

    Upgraded pact-jvm-model to 3.5.13
This commit is contained in:
Marcin Grzejszczak
2018-04-02 22:50:40 +02:00
parent de529e222e
commit 6402e14f3c
82 changed files with 953 additions and 395 deletions

View File

@@ -1382,6 +1382,7 @@ org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.converter.YamlContractConverter
----
[[pact-converter]]
==== Pact Converter
Spring Cloud Contract includes support for https://docs.pact.io/[Pact] representation of
@@ -1675,3 +1676,76 @@ properties
|1000
|Number of millis to wait between attempts to push the commits to `origin`
|===
[[pact-stub-downloader]]
=== Using the Pact Stub Downloader
Whenever the `repositoryRoot` starts with a Pact protocol
(starts with `pact://`), the stub downloader will try
to fetch the Pact contract definitions from the Pact Broker.
Whatever is set after `pact://` will be parsed as the Pact Broker URL.
Either via environment variables, system properties, properties set
inside the plugin or contracts repository configuration you can
tweak the downloader's behaviour. Below you can find the list of
properties
.SCM Stub Downloader properties
|===
|Type of a property |Name of the property | Description
|
* `pactbroker.host` (plugin prop)
* `stubrunner.properties.pactbroker.host` (system prop)
* `STUBRUNNER_PROPERTIES_PACTBROKER_HOST` (env prop)
|Host from URL passed to `repositoryRoot`
|What is the URL of Pact Broker
|
* `pactbroker.port` (plugin prop)
* `stubrunner.properties.pactbroker.port` (system prop)
* `STUBRUNNER_PROPERTIES_PACTBROKER_PORT` (env prop)
|Port from URL passed to `repositoryRoot`
|What is 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`
|What is 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 `+`
|What tags 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`
|What kind of authentication 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)
|
|Username used to connect to the Pact Broker
|
* `pactbroker.auth.password` (plugin prop)
* `stubrunner.properties.pactbroker.auth.password` (system prop)
* `STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_PASSWORD` (env prop)
|
|Password used to connect 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 will be a combination of `groupId:artifactId`. If `false`, just `artifactId` is used
|===

View File

@@ -1,6 +1,5 @@
:introduction_url: https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/{branch}
:samples_branch: 2.0.x
:samples_branch: 2.0.x
== Spring Cloud Contract FAQ
@@ -707,6 +706,195 @@ to find stub definitions and contracts. E.g. for `com.example:foo:1.0.0` the pat
* Stub servers will be started and fed with mappings
* Messaging definitions will be read and used in the messaging tests
=== Can I use the Pact Broker?
When using http://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 one can fetch Pact files from the Pact Broker to generate
tests and stubs.
As a prerequisite the Pact Converter and Pact Stub Downloader
are required. You have to add it via the `spring-cloud-contract-pact` dependency.
You can read more about it in the <<pact-converter>> section.
IMPORTANT: Pact follows the Consumer Contract convention. That means
that the Consumer creates the Pact definitions first, then
shares the files with the Producer. Those expectations are generated
from the Consumer's code and can break the Producer if the expectation
is not met.
==== Pact Consumer
The consumer uses Pact framework to generate Pact files. The
Pact files are sent to the Pact Broker. An example of such
setup can be found https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/{samples_branch}/consumer_pact[here].
==== Producer
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 via the URL that contains
the `pact://` protocol. It's enough to pass the URL to the
Pact Broker.
.Maven
[source,xml,indent=0]
----
<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>
----
.Gradle
[source,gradle,indent=0]
----
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 will be downloaded from the Pact Broker
* Spring Cloud Contract will convert the Pact files into tests and stubs
* The JAR with the stubs gets automatically created as usual
==== Pact Consumer (Producer Contract approach)
In the scenario where you don't want to do Consumer Contract approach
(for every single consumer define the expectations) but you'd prefer
to do Producer Contracts (the producer provides the contracts and
publishes stubs), it's enough to use Spring Cloud Contract with
Stub Runner option.
First, remember to add Stub Runner and Spring Cloud Contract Pact module
as test dependencies.
.Maven
[source,xml,indent=0]
----
<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>
----
.Gradle
[source,gradle,indent=0]
----
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, just pass the URL of the Pact Broker to `repositoryRoot`, prefixed
with `pact://` protocol. E.g. `pact://http://localhost:8085`
[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 will be downloaded from the Pact Broker
* Spring Cloud Contract will convert the Pact files into stub definitions
* The stub servers will be started and fed with stubs
For more information about Pact support you can go to
the <<pact-stub-downloader>> section.
=== How can I debug the request/response being sent by the generated tests client?
The generated tests all boil down to RestAssured in some form or fashion which relies on https://hc.apache.org/httpcomponents-client-ga/[Apache HttpClient]. HttpClient has a facility called https://hc.apache.org/httpcomponents-client-ga/logging.html#Wire_Logging[wire logging] which logs the entire request and response to HttpClient. Spring Boot has a logging https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html[common application property] for doing this sort of thing, just add this to your application properties

11
pom.xml
View File

@@ -123,6 +123,17 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-provider-junit_2.12</artifactId>
<version>${pact.version}</version>
<exclusions>
<exclusion>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-model</artifactId>

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath />
</parent>

View File

@@ -7,7 +7,7 @@ buildscript {
maven { url "http://repo.spring.io/release" }
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
}
}

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath />
</parent>

View File

@@ -1,2 +1,3 @@
server:
port: 6565
logging.level.org.springframework.cloud.contract: DEBUG

View File

@@ -10,7 +10,7 @@ buildscript {
// end::repos[]
dependencies {
classpath 'org.codehaus.groovy:groovy-all:2.5.0-beta-1'
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${findProperty('verifierVersion') ?: verifierVersion}"
}
}

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath />
</parent>

View File

@@ -7,7 +7,7 @@ buildscript {
maven { url "http://repo.spring.io/release" }
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
}
}

View File

@@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

View File

@@ -7,7 +7,7 @@ buildscript {
maven { url "http://repo.spring.io/release" }
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${findProperty('verifierVersion') ?: verifierVersion}"
classpath "com.jayway.restassured:rest-assured:2.9.0"
classpath "com.jayway.restassured:spring-mock-mvc:2.9.0"

View File

@@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

View File

@@ -7,7 +7,7 @@ buildscript {
maven { url "http://repo.spring.io/release" }
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
}
}
@@ -39,7 +39,7 @@ dependencies {
testCompile "org.springframework.cloud:spring-cloud-starter-contract-stub-runner"
//tag::pact_dependency[]
testCompile "org.springframework.cloud:spring-cloud-contract-spec-pact"
testCompile "org.springframework.cloud:spring-cloud-contract-pact"
testCompile 'au.com.dius:pact-jvm-model:3.5.13'
//end::pact_dependency[]
}

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath />
</parent>
@@ -46,7 +46,7 @@
<!-- tag::pact_dependency[] -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-pact</artifactId>
<artifactId>spring-cloud-contract-pact</artifactId>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -7,10 +7,10 @@ buildscript {
maven { url "http://repo.spring.io/release" }
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${findProperty('verifierVersion') ?: verifierVersion}"
//tag::pact_dependency[]
classpath "org.springframework.cloud:spring-cloud-contract-spec-pact:${findProperty('verifierVersion') ?: verifierVersion}"
classpath "org.springframework.cloud:spring-cloud-contract-pact:${findProperty('verifierVersion') ?: verifierVersion}"
classpath 'au.com.dius:pact-jvm-model:3.5.13'
//end::pact_dependency[]
}

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath />
</parent>
@@ -77,7 +77,7 @@
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-pact</artifactId>
<artifactId>spring-cloud-contract-pact</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
<dependency>

View File

@@ -7,7 +7,7 @@ buildscript {
maven { url "http://repo.spring.io/release" }
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
}
}

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath />
</parent>

View File

@@ -10,7 +10,7 @@ buildscript {
maven { url "http://repo.spring.io/plugins-staging-local/" }
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
}
}

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath />
</parent>

View File

@@ -7,7 +7,7 @@ buildscript {
maven { url "http://repo.spring.io/release" }
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
}
}

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath />
</parent>

View File

@@ -10,7 +10,7 @@ buildscript {
maven { url "http://repo.spring.io/plugins-staging-local/" }
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
}
}

View File

@@ -7,7 +7,7 @@ buildscript {
maven { url "http://repo.spring.io/release" }
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
}
}

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath />
</parent>

View File

@@ -9,7 +9,7 @@ buildscript {
}
// end::repos[]
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.BUILD-SNAPSHOT"
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE"
classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${findProperty('verifierVersion') ?: verifierVersion}"
}
}

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.RELEASE</version>
<relativePath />
</parent>

View File

@@ -43,7 +43,7 @@
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-pact</artifactId>
<artifactId>spring-cloud-contract-pact</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@@ -255,7 +255,7 @@ public class AetherStubDownloader implements StubDownloader {
}
private static File unpackStubJarToATemporaryFolder(URI stubJarUri) {
File tmpDirWhereStubsWillBeUnzipped = TemporaryFileStorage.unpackStubJarToATemporaryFolder(TEMP_DIR_PREFIX);
File tmpDirWhereStubsWillBeUnzipped = TemporaryFileStorage.createTempDir(TEMP_DIR_PREFIX);
log.info("Unpacking stub from JAR [URI: " + stubJarUri + "]");
unzipTo(new File(stubJarUri), tmpDirWhereStubsWillBeUnzipped);
TemporaryFileStorage.add(tmpDirWhereStubsWillBeUnzipped);

View File

@@ -81,11 +81,17 @@ public class ContractDownloader {
} else {
log.info("Will pick a pattern from group id and artifact id");
if (hasGavInPath(contractsDirectory)) {
if (log.isDebugEnabled()) {
log.debug("Group & artifact in path");
}
contractsDirectory = contractsSubDirIfPresent(contractsDirectory);
// we're already under proper folder (for the given version)
// we're already under proper folder (for the given group and artifact)
pattern = fileToPattern(contractsDirectory);
includedAntPattern = "**/";
} else {
if (log.isDebugEnabled()) {
log.debug("No group & artifact in path");
}
pattern = groupArtifactToPattern(contractsDirectory);
includedAntPattern = wrapWithAntPattern(slashSeparatedGroupId() + "/" + this.projectArtifactId);
}

View File

@@ -122,7 +122,7 @@ class GitContractsRepo {
File file = CACHED_LOCATIONS.get(repo);
GitStubDownloaderProperties properties = new GitStubDownloaderProperties(repo, this.options);
if (file == null) {
File tmpDirWhereStubsWillBeUnzipped = TemporaryFileStorage.unpackStubJarToATemporaryFolder(TEMP_DIR_PREFIX);
File tmpDirWhereStubsWillBeUnzipped = TemporaryFileStorage.createTempDir(TEMP_DIR_PREFIX);
GitRepo gitRepo = new GitRepo(tmpDirWhereStubsWillBeUnzipped, properties);
file = gitRepo.cloneProject(properties.url);
gitRepo.checkout(file, properties.branch);

View File

@@ -24,7 +24,7 @@ import org.springframework.util.StringUtils;
*/
public class StubConfiguration {
private static final String STUB_COLON_DELIMITER = ":";
private static final String DEFAULT_VERSION = "+";
static final String DEFAULT_VERSION = "+";
public static final String DEFAULT_CLASSIFIER = "stubs";
final String groupId;

View File

@@ -46,6 +46,15 @@ class StubRunnerPropertyUtils {
return StringUtils.hasText(value) && Boolean.parseBoolean(value);
}
/**
* For options, system props and env vars returns {@code true}
* when property is set
*/
static boolean hasProperty(Map<String, String> options, String propName) {
String value = getProperty(options, propName);
return StringUtils.hasText(value);
}
/**
* Tries to pick a value from options, for Env vars takes the prop name, converts
* dots to underscores and applies upper case

View File

@@ -97,7 +97,7 @@ class TemporaryFileStorage {
}
}
static File unpackStubJarToATemporaryFolder(String tempDirPrefix) {
static File createTempDir(String tempDirPrefix) {
try {
return createTempDirectory(tempDirPrefix)
.toFile();

View File

@@ -82,6 +82,34 @@ class StubRunnerPropertyUtilsSpec extends Specification {
"foo.bar-baz" | null | "" | "bc" | "bc" | "stubrunner.properties.foo.bar-baz" | "STUBRUNNER_PROPERTIES_FOO_BAR_BAZ"
}
@RestoreSystemProperties
def "should return [#expectedResult] when prop is set for [#queriedProp] and system is [#systemProperty] and env [#envVariable]"() {
given:
def sysProp = systemProperty
def envVar = envVariable
PropertyFetcher fetcher = new PropertyFetcher() {
@Override
String systemProp(String prop) {
return sysProp
}
@Override
String envVar(String prop) {
return envVar
}
}
StubRunnerPropertyUtils.FETCHER = fetcher
expect:
expectedResult == StubRunnerPropertyUtils.hasProperty(map, queriedProp)
where:
queriedProp | map | systemProperty | envVariable | expectedResult
"foo.bar-baz" | ["foo.bar-baz": "faz"] | "ab" | "bc" | true
"foo.bar-baz" | [:] | "ab" | "bc" | true
"foo.bar-baz" | [:] | "" | "bc" | true
"foo.bar-baz" | null | "" | "bc" | true
"foo.bar-baz" | null | null | null | false
}
def cleanupSpec() {
StubRunnerPropertyUtils.FETCHER = new PropertyFetcher()
}

View File

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

View File

@@ -415,7 +415,7 @@
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-pact</artifactId>
<artifactId>spring-cloud-contract-pact</artifactId>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -90,7 +90,7 @@ class MavenContractsDownloader {
contractDownloader().updatePropertiesWithInclusion(downloadedContractsDir, config);
return downloadedContractsDir;
} else if (shouldDownloadContracts()) {
this.log.info("Download dependency is provided - will download contract jars");
this.log.info("Download dependency is provided - will retrieve contracts from a remote location");
File downloadedContracts = contractDownloader().unpackedDownloadedContracts(config);
this.project.getProperties().setProperty(CONTRACTS_DIRECTORY_PROP, downloadedContracts.getAbsolutePath());
return downloadedContracts;

View File

@@ -46,7 +46,7 @@
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-pact</artifactId>
<artifactId>spring-cloud-contract-pact</artifactId>
<version>${spring.cloud.contract.version}</version>
</dependency>
<dependency>

View File

@@ -8,10 +8,10 @@
<version>2.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-contract-spec-pact</artifactId>
<artifactId>spring-cloud-contract-pact</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Contract Spec Pact</name>
<description>Spring Cloud Contract Spec Pact</description>
<name>Spring Cloud Contract Pact</name>
<description>Spring Cloud Contract Pact</description>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
@@ -19,7 +19,11 @@
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-verifier</artifactId>
<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>
@@ -37,6 +41,10 @@
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-consumer-java8_2.12</artifactId>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-provider-junit_2.12</artifactId>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
@@ -52,6 +60,11 @@
<artifactId>spock-global-unroll</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
@@ -62,8 +75,13 @@
<execution>
<goals>
<goal>addSources</goal>
<goal>addTestSources</goal>
<goal>generateStubs</goal>
<goal>compile</goal>
<goal>testGenerateStubs</goal>
<goal>testCompile</goal>
<goal>removeStubs</goal>
<goal>removeTestStubs</goal>
</goals>
</execution>
</executions>

View File

@@ -0,0 +1,376 @@
/*
* Copyright 2013-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.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.model.Pact;
import au.com.dius.pact.model.PactSpecVersion;
import au.com.dius.pact.provider.junit.loader.PactBroker;
import au.com.dius.pact.provider.junit.loader.PactBrokerAuth;
import au.com.dius.pact.provider.junit.loader.PactBrokerLoader;
import au.com.dius.pact.provider.junit.loader.PactLoader;
import au.com.dius.pact.provider.junit.sysprops.SystemPropertyResolver;
import au.com.dius.pact.provider.junit.sysprops.ValueResolver;
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.jetbrains.annotations.NotNull;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.cloud.contract.verifier.converter.StubGenerator;
import org.springframework.cloud.contract.verifier.converter.StubGeneratorProvider;
import org.springframework.cloud.contract.verifier.file.ContractMetadata;
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.util.StringUtils;
/**
* Allows downloading of Pact files from the Pact Broker.
*
* @author Marcin Grzejszczak
* @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
*/
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.isEmpty(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 final StubRunnerOptions stubRunnerOptions;
private final boolean deleteStubsAfterTest;
private final ObjectMapper objectMapper;
private static final String PROVIDER_NAME_WITH_GROUP_ID = "pactbroker.provider-name-with-group-id";
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()) {
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");
}
StubGeneratorProvider provider = new StubGeneratorProvider();
Collection<StubGenerator> stubGenerators = provider
.converterForName(ARTIFICIAL_NAME_ENDING_WITH_GROOVY);
if (log.isDebugEnabled()) {
log.debug("Found following matching stub generators " + stubGenerators);
}
for (StubGenerator stubGenerator : stubGenerators) {
Map<Contract, String> map = stubGenerator
.convertContents(file.getName(),
new ContractMetadata(file.toPath(), false,
contracts.size(), null, contracts));
for (Map.Entry<Contract, String> entry : map.entrySet()) {
String value = entry.getValue();
File mapping = new File(mappingsFolder,
StringUtils.stripFilenameExtension(file.getName()) + "_" +
Math.abs(entry.getKey().hashCode()) + ".json");
storeFile(mapping.toPath(), value.getBytes());
}
}
}
private void storeFile(Path path, byte[] contents) {
try {
Files.write(path, contents);
if (log.isDebugEnabled()) {
log.debug("Stored file [" + path.toString() + "]");
}
}
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();
}
@NotNull PactLoader pactBrokerLoader(ValueResolver resolver,
List<String> tags) throws IOException {
Resource repo = this.stubRunnerOptions.getStubRepositoryRoot();
String schemeSpecificPart = schemeSpecificPart(repo.getURI());
URI pactBrokerUrl = URI.create(schemeSpecificPart);
return new PactBrokerLoader(new PactBroker() {
@Override public Class<? extends Annotation> annotationType() {
return PactBroker.class;
}
@Override public String host() {
return resolver.resolveValue("pactbroker.host:" + pactBrokerUrl.getHost());
}
@Override public String port() {
return resolver.resolveValue("pactbroker.port:" + pactBrokerUrl.getPort());
}
@Override public String protocol() {
return resolver.resolveValue("pactbroker.protocol:" + pactBrokerUrl.getScheme());
}
@Override public String[] tags() {
return tags.toArray(new String[0]);
}
@Override public boolean failIfNoPactsFound() {
return true;
}
@Override public PactBrokerAuth authentication() {
return new PactBrokerAuth() {
@Override public Class<? extends Annotation> annotationType() {
return PactBrokerAuth.class;
}
@Override public String scheme() {
return resolver.resolveValue("pactbroker.auth.scheme:basic");
}
@Override public String username() {
return resolver.resolveValue("pactbroker.auth.username:");
}
@Override public String password() {
return resolver.resolveValue("pactbroker.auth.password:");
}
};
}
@Override public Class<? extends ValueResolver> valueResolver() {
return SystemPropertyResolver.class;
}
});
}
private String schemeSpecificPart(URI uri) {
String part = uri.getSchemeSpecificPart();
if (StringUtils.isEmpty(part)) {
return part;
}
return part.startsWith("//") ? part.substring(2) : part;
}
@NotNull 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 = new SystemPropertyResolver();
StubRunnerOptions options;
FromPropsThenFromSysEnv(StubRunnerOptions options) {
this.options = options;
}
@Override public String resolveValue(String expression) {
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);
}
@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);
}
}
// 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

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,114 @@
package org.springframework.cloud.contract.stubrunner
import java.nio.file.Files
import au.com.dius.pact.model.Pact
import au.com.dius.pact.model.PactSource
import au.com.dius.pact.provider.junit.loader.PactLoader
import au.com.dius.pact.provider.junit.sysprops.ValueResolver
import com.github.tomakehurst.wiremock.stubbing.StubMapping
import org.jetbrains.annotations.NotNull
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter
/**
* @author Marcin Grzejszczak
*/
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) {
@NotNull @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)
// }
// }
}

View File

@@ -1,2 +0,0 @@
org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter

View File

@@ -1,68 +0,0 @@
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'POST'
url '/'
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',
someNullValue: null
])
headers {
contentType('application/json')
header("Some-Header", $(c(regex('[a-zA-Z]{9}')), p('someValue')))
header("Header-Without-Matcher", 'someValue')
}
bodyMatchers {
jsonPath('$.someInteger', byRegex(anInteger()))
jsonPath('$.someDecimal', byRegex(aDouble()))
jsonPath('$.someHex', byRegex('[a-fA-F0-9]+'))
jsonPath('$.someAlphaNumeric', byRegex(alphaNumeric()))
jsonPath('$.someUUID', byRegex(uuid()))
jsonPath('$.someDate', byDate())
jsonPath('$.someTime', byTime())
jsonPath('$.someDateTime', byTimestamp())
jsonPath('$.someBoolean', byRegex(anyBoolean()))
}
}
response {
status OK()
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
])
headers {
contentType('application/json')
header("Some-Header", $(c('someValue'), p(regex('[a-zA-Z]{9}'))))
header("Header-Without-Matcher", 'someValue')
}
bodyMatchers {
jsonPath('$.someInteger', byRegex(anInteger()))
jsonPath('$.someDecimal', byRegex(aDouble()))
jsonPath('$.someHex', byRegex('[a-fA-F0-9]+'))
jsonPath('$.someAlphaNumeric', byRegex(alphaNumeric()))
jsonPath('$.someUUID', byRegex(uuid()))
jsonPath('$.someDate', byDate())
jsonPath('$.someTime', byTime())
jsonPath('$.someDateTime', byTimestamp())
jsonPath('$.someBoolean', byRegex(anyBoolean()))
jsonPath('$.someNullValue', byNull())
}
}
}

View File

@@ -1,276 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "POST",
"path": "/",
"headers": {
"Content-Type": "application/json",
"Header-Without-Matcher": "someValue",
"Some-Header": "someValue"
},
"body": {
"someHex": "DEADC0DE",
"someTime": "13:37:00",
"someNullValue": null,
"someInteger": 1234567890,
"someBoolean": "true",
"someDate": "2018-03-26",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDecimal": 123.123,
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someDateTime": "2018-03-26 13:37:00"
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine": "AND"
},
"Some-Header": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z]{9}"
}
],
"combine": "AND"
}
},
"body": {
"$.someInteger": {
"matchers": [
{
"match": "integer"
}
],
"combine": "AND"
},
"$.someDecimal": {
"matchers": [
{
"match": "decimal"
}
],
"combine": "AND"
},
"$.someHex": {
"matchers": [
{
"match": "regex",
"regex": "[a-fA-F0-9]+"
}
],
"combine": "AND"
},
"$.someAlphaNumeric": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z0-9]+"
}
],
"combine": "AND"
},
"$.someUUID": {
"matchers": [
{
"match": "regex",
"regex": "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
}
],
"combine": "AND"
},
"$.someDate": {
"matchers": [
{
"match": "date",
"date": "yyyy-MM-dd"
}
],
"combine": "AND"
},
"$.someTime": {
"matchers": [
{
"match": "time",
"time": "HH:mm:ss"
}
],
"combine": "AND"
},
"$.someDateTime": {
"matchers": [
{
"match": "timestamp",
"timestamp": "yyyy-MM-dd HH:mm:ssZZZ"
}
],
"combine": "AND"
},
"$.someBoolean": {
"matchers": [
{
"match": "regex",
"regex": "(true|false)"
}
],
"combine": "AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json",
"Header-Without-Matcher": "someValue",
"Some-Header": "someValue"
},
"body": {
"someHex": "DEADC0DE",
"someTime": "13:37:00",
"someInteger": 1234567890,
"someBoolean": "true",
"someRegex": 1234567890,
"someDate": "2018-03-26",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDecimal": 123.123,
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someDateTime": "2018-03-26 13:37:00"
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine": "AND"
},
"Some-Header": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z]{9}"
}
],
"combine": "AND"
}
},
"body": {
"$.someInteger": {
"matchers": [
{
"match": "integer"
}
],
"combine": "AND"
},
"$.someDecimal": {
"matchers": [
{
"match": "decimal"
}
],
"combine": "AND"
},
"$.someHex": {
"matchers": [
{
"match": "regex",
"regex": "[a-fA-F0-9]+"
}
],
"combine": "AND"
},
"$.someAlphaNumeric": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z0-9]+"
}
],
"combine": "AND"
},
"$.someUUID": {
"matchers": [
{
"match": "regex",
"regex": "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
}
],
"combine": "AND"
},
"$.someDate": {
"matchers": [
{
"match": "date",
"date": "yyyy-MM-dd"
}
],
"combine": "AND"
},
"$.someTime": {
"matchers": [
{
"match": "time",
"time": "HH:mm:ss"
}
],
"combine": "AND"
},
"$.someDateTime": {
"matchers": [
{
"match": "timestamp",
"timestamp": "yyyy-MM-dd HH:mm:ssZZZ"
}
],
"combine": "AND"
},
"$.someBoolean": {
"matchers": [
{
"match": "regex",
"regex": "(true|false)"
}
],
"combine": "AND"
},
"$.someNullValue": {
"matchers": [
{
"match": "null"
}
],
"combine": "AND"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}

View File

@@ -21,6 +21,8 @@ import java.nio.file.Path
import java.util.concurrent.atomic.AtomicInteger
import groovy.transform.PackageScope
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import wiremock.com.google.common.collect.ListMultimap
import org.springframework.cloud.contract.spec.ContractVerifierException
@@ -42,6 +44,7 @@ import static org.springframework.cloud.contract.verifier.util.NamesUtil.toLastD
*/
class TestGenerator {
private static final Log log = LogFactory.getLog(TestGenerator)
private static final String DEFAULT_CLASS_PREFIX = "ContractVerifier"
private static final String DEFAULT_TEST_PACKAGE = "org.springframework.cloud.contract.verifier.tests"
@@ -111,6 +114,9 @@ class TestGenerator {
private void processIncludedDirectory(
final String includedDirectoryRelativePath, Collection<ContractMetadata> contracts, final String basePackageNameForClass) {
if (log.isDebugEnabled()) {
log.debug("Collected contracts with metadata ${contracts}")
}
if (contracts.size()) {
def className = afterLast(includedDirectoryRelativePath.toString(), File.separator) + resolveNameSuffix()
def convertedClassName = convertIllegalPackageChars(className)

View File

@@ -87,7 +87,7 @@ class ContractFileScanner {
* and try to convert via pluggable Contract Converters any possible contracts
*/
private void appendRecursively(File baseDir, ListMultimap<Path, ContractMetadata> result) {
List<ContractConverter> converters = SpringFactoriesLoader.loadFactories(ContractConverter, null)
List<ContractConverter> converters = converters()
if (log.isTraceEnabled()) {
log.trace("Found the following contract converters ${converters}")
}
@@ -122,13 +122,18 @@ class ContractFileScanner {
}
}
protected List<ContractConverter> converters() {
return SpringFactoriesLoader.loadFactories(ContractConverter, null)
}
private void addContractToTestGeneration(List<ContractConverter> converters, ListMultimap<Path, ContractMetadata> result,
File[] files, File file, int index) {
boolean converted = false
if (!file.isDirectory()) {
for (ContractConverter converter : converters) {
if (converter.isAccepted(file)) {
addContractToTestGeneration(result, files, file, index, converter.convertFrom(file))
Collection<Contract> contracts = tryConvert(converter, file)
if (contracts) {
addContractToTestGeneration(result, files, file, index, contracts)
converted = true
break
}
@@ -142,6 +147,21 @@ class ContractFileScanner {
}
}
private Collection<Contract> tryConvert(ContractConverter converter, File file) {
boolean accepted = converter.isAccepted(file)
if (!accepted) {
return null
}
try {
return converter.convertFrom(file)
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("Exception occurred while trying to convert the file", e)
}
return null
}
}
private void addContractToTestGeneration(ListMultimap<Path, ContractMetadata> result, File[] files, File file,
int index, Collection<Contract> convertedContract) {
Path path = file.toPath()
@@ -154,6 +174,9 @@ class ContractFileScanner {
files.size(), order, convertedContract)
if (log.isDebugEnabled()) {
log.debug("Creating a contract entry for path [" + path + "] and metadata [" + metadata + "]")
}
if (convertedContract) {
}
result.put(parent, metadata)
}

View File

@@ -21,6 +21,9 @@ import spock.lang.Specification
import java.nio.file.Path
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.ContractConverter
/**
* @author Jakub Kubrynski, codearte.io
*/
@@ -77,7 +80,27 @@ class ContractFileScannerSpec extends Specification {
def "should find contract files with converters"() {
given:
File baseDir = new File(this.getClass().getResource("/directory/with/mixed").toURI())
ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null)
ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null) {
@Override
protected List<ContractConverter> converters() {
return [new ContractConverter() {
@Override
boolean isAccepted(File file) {
return file.name.endsWith(".json")
}
@Override
Collection<Contract> convertFrom(File file) {
throw new RuntimeException("boom")
}
@Override
Object convertTo(Collection contract) {
throw new RuntimeException("boom")
}
}]
}
}
when:
ListMultimap<Path, ContractMetadata> result = scanner.findContracts()
then:

View File

@@ -0,0 +1,23 @@
{
"request": {
"method": "PUT",
"url": "/loanApplication",
"headers": {
"Content-Type": {
"equalTo": "application/vnd.loanapplicationservice.v1+json"
}
},
"bodyPatterns": [
{
"matches": "\\{\"clientPesel\":\"1234567890\",\"loanAmount\":123.123\\}"
}
]
},
"response": {
"status": 200,
"body": "{\"loanApplicationStatus\":\"LOAN_APPLIED\",\"loanApplicationId\":\"${123123123:$anyInt($it)}\",\"rejectionReason\":null}",
"headers": {
"Content-Type": "application/vnd.loanapplicationservice.v1+json"
}
}
}

View File

@@ -66,8 +66,7 @@ public abstract class WireMockSpring {
}
initialized = true;
}
WireMockConfiguration config = new WireMockConfiguration();
return config;
return new WireMockConfiguration();
}
}