Added support for writing contracts in Java

fixes gh-1161
This commit is contained in:
Marcin Grzejszczak
2019-08-12 13:40:23 +02:00
parent e26eff962e
commit 706044bfe5
32 changed files with 1677 additions and 93 deletions

View File

@@ -6,20 +6,7 @@ Spring Cloud Contract supports the DSLs written in the following languages:
* Groovy
* YAML
TIP: If you are not familiar with Groovy, do not worry - you can use Java syntax in the
Groovy DSL files as well.
If you decide to write the contract in Groovy, do not be alarmed if you have not used Groovy
before. Knowledge of the language is not really needed, as the Contract DSL uses only a
tiny subset of it (only literals, method calls, and closures). Also, the DSL is statically
typed, to make it programmer-readable without any knowledge of the DSL itself.
IMPORTANT: Remember that, inside the Groovy contract file, you have to provide the fully
qualified name to the `Contract` class and `make` static imports, such as
`org.springframework.cloud.spec.Contract.make { ... }`. You can also provide an import to
the `Contract` class (`import org.springframework.cloud.spec.Contract`) and then call
`Contract.make { ... }`.
* Java
TIP: Spring Cloud Contract supports defining multiple contracts in a single file.
@@ -37,8 +24,13 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract
----
include::{verifier_core_path}/src/test/resources/yml/contract_rest.yml[indent=0]
----
====
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest.java[tags=class,indent=0]
----
====
[TIP]
====
@@ -49,6 +41,54 @@ mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert
----
====
[[contract-groovy]]
=== Contract DSL in Groovy
If you are not familiar with Groovy, do not worry - you can use Java syntax in the
Groovy DSL files as well.
If you decide to write the contract in Groovy, do not be alarmed if you have not used Groovy
before. Knowledge of the language is not really needed, as the Contract DSL uses only a
tiny subset of it (only literals, method calls, and closures). Also, the DSL is statically
typed, to make it programmer-readable without any knowledge of the DSL itself.
IMPORTANT: Remember that, inside the Groovy contract file, you have to provide the fully
qualified name to the `Contract` class and `make` static imports, such as
`org.springframework.cloud.spec.Contract.make { ... }`. You can also provide an import to
the `Contract` class (`import org.springframework.cloud.spec.Contract`) and then call
`Contract.make { ... }`.
[[contract-java]]
=== Contract DSL in Java
To write a contract definition in Java, you need to create a class, that implements either the `Supplier<Contract>` interface for a single contract or `Supplier<Collection<Contract>>` for multiple contracts.
You can also write the contract definitions under `src/test/java` (e.g. `src/test/java/contracts`) so that you don't have to modify the classpath of your project. In this case you'll have to provide a new location of contract definitions to your Spring Cloud Contract plugin.
====
[source,xml,indent=0,subs="verbatim,attributes",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>
<contractsDirectory>src/test/java/contracts</contractsDirectory>
</configuration>
</plugin>
----
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
.Gradle
----
contracts {
contractsDslDir = new File(project.rootDir, "src/test/java/contracts")
}
----
====
[[contract-limitations]]
=== Limitations
@@ -90,6 +130,12 @@ include::{contract_spec_tests_path}/src/test/groovy/org/springframework/cloud/co
----
include::{verifier_core_path}/src/test/resources/yml/contract_rest.yml[indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_with_tags.java[tags=description,indent=0]
----
====
[[contract-dsl-name]]
@@ -119,6 +165,12 @@ include::{contract_spec_tests_path}/src/test/groovy/org/springframework/cloud/co
----
include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=name,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_with_tags.java[tags=name,indent=0]
----
====
[[contract-dsl-ignoring-contracts]]
@@ -140,6 +192,12 @@ include::{contract_spec_tests_path}/src/test/groovy/org/springframework/cloud/co
----
include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=ignored,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_with_tags.java[tags=ignored,indent=0]
----
====
[[contract-dsl-in-progress]]
@@ -164,6 +222,12 @@ include::{contract_spec_tests_path}/src/test/groovy/org/springframework/cloud/co
----
include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=in_progress,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_with_tags.java[tags=in_progress,indent=0]
----
====
You can set the value of the `failOnInProgress` Spring Cloud Contract plugin property to ensure that your build will break when at least one contract in progress remains in your sources.
@@ -199,6 +263,12 @@ include::{verifier_core_path}/src/test/resources/classpath/readFromFile.groovy[i
----
include::{verifier_core_path}/src/test/resources/yml/contract_from_file.yml[indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_from_file.java[tags=class,indent=0]
----
====
Further assume that the JSON files is as follows:
@@ -222,7 +292,7 @@ of a request or a response. The name of the file needs to be a file with locatio
relative to the folder in which the contract lays.
If you need to pass the contents of a file in binary form,
you can use the `fileAsBytes` method in Groovy DSL or a `bodyFromFileAsBytes` field in YAML.
you can use the `fileAsBytes` method in the coded DSL or a `bodyFromFileAsBytes` field in YAML.
The following example shows how to pass the contents of binary files:
@@ -238,6 +308,12 @@ include::{verifier_core_path}/src/test/resources/body_builder/worksWithPdf.groov
----
include::{verifier_core_path}/src/test/resources/yml/contract_pdf.yml[indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_rest_from_pdf.java[tags=class,indent=0]
----
====
IMPORTANT: You should use this approach whenever you want to work with binary payloads,
@@ -280,6 +356,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,i
include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=response,indent=0]
...
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=http_dsl,indent=0]
----
====
IMPORTANT: If you want to make your contract have a higher priority,
@@ -306,6 +388,12 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract
----
include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request_obligatory,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=request,indent=0]
----
====
You can specify an absolute rather than a relative `url`, but using `urlPath` is
@@ -325,6 +413,12 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract
----
include::{verifier_core_path}/src/test/resources/yml/contract_rest_with_path.yml[tags=url_path,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=url,indent=0]
----
====
`request` may contain query parameters, as the following example (which uses `urlPath`) shows:
@@ -343,6 +437,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,i
...
include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=query_params,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=urlpath,indent=0]
----
====
`request` can contain additional request headers, as the following example shows:
@@ -361,6 +461,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,i
...
include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=headers,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=headers,indent=0]
----
====
`request` may contain additional request cookies, as the following example shows:
@@ -379,6 +485,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,i
...
include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=cookies,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=cookies,indent=0]
----
====
`request` may contain a request body, as the following example shows:
@@ -397,6 +509,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=request,i
...
include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=body,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=body,indent=0]
----
====
`request` can contain multipart elements. To include multipart elements, use the
@@ -414,11 +532,17 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract
----
include::{verifier_core_path}/src/test/resources/yml/contract_multipart.yml[indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_multipart.java[tags=class,indent=0]
----
====
In the preceding example, we define parameters in either of two ways:
.Groovy DSL
.Coded DSL
* Directly, by using the map notation, where the value can be a dynamic property (such as
`formParameter: $(consumer(...), producer(...))`).
* By using the `named(...)` method that lets you set a named parameter. A named parameter
@@ -488,6 +612,12 @@ include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=response,
...
include::{verifier_core_path}/src/test/resources/yml/contract.yml[tags=response_obligatory,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=response,indent=0]
----
====
Besides status, the response may contain headers, cookies, and a body, which are
@@ -516,7 +646,7 @@ For YAML, you can use only the `matchers` section.
[[contract-dsl-dynamic-properties-in-body]]
==== Dynamic Properties inside the Body
IMPORTANT: This section is valid only for the Groovy DSL. Check out the
IMPORTANT: This section is valid only for the Coded DSL (Groovy, Java etc.). Check out the
<<contract-dsl-matchers>> section for YAML examples of a similar feature.
You can set the properties inside the body either with the `value` method or, if you use
@@ -564,14 +694,23 @@ There are several additional <<contract-dsl-regex-limitations,known limitations>
The following example shows how to use regular expressions to write a request:
[source,groovy,indent=0]
====
[source,groovy,indent=0,role="primary"]
.groovy
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=regex,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=regex,indent=0]
----
====
You can also provide only one side of the communication with a regular expression. If you
do so, then the contract engine automatically provides the generated string that matches
the provided regular expression. The following code shows an example:
the provided regular expression. The following code shows an example for Groovy:
[source,groovy,indent=0]
----
@@ -589,7 +728,7 @@ use in your contracts, as the following example shows:
include::{contract_spec_path}/src/main/java/org/springframework/cloud/contract/spec/internal/RegexPatterns.java[tags=regexps,indent=0]
----
In your contract, you can use it as follows:
In your contract, you can use it as follows (example for the Groovy DSL):
[source,groovy,indent=0]
----
@@ -605,7 +744,7 @@ All of those methods start with the `any` prefix, as follows:
include::{contract_spec_path}/src/main/java/org/springframework/cloud/contract/spec/internal/RegexCreatingProperty.java[tags=regex_creating_props,indent=0]
----
The following example shows how you can reference those methods:
The following example shows how you can reference those methods (example for Groovy DSL):
[source,groovy,indent=0]
----
@@ -637,20 +776,32 @@ optional parameters only for the following:
The following example shows how to provide optional parameters:
[source,groovy,indent=0]
====
[source,groovy,indent=0,role="primary"]
.groovy
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=optionals,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=optionals,indent=0]
----
====
By wrapping a part of the body with the `optional()` method, you create a regular
expression that must be present 0 or more times.
If you use Spock, the following test would be generated from the previous example:
====
[source,groovy,indent=0]
.groovy
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=optionals_test,indent=0]
----
====
The following stub would also be generated:
@@ -669,11 +820,21 @@ You can define a method call that runs on the server side during the test. Such
method can be added to the class defined as `baseClassForTests` in the configuration. The
following code shows an example of the contract portion of the test case:
[source,groovy,indent=0]
====
[source,groovy,indent=0,role="primary"]
.groovy
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[tags=method,indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[tags=method,indent=0]
----
====
The following code shows the base class portion of the test case:
[source,groovy,indent=0]
@@ -747,7 +908,7 @@ given name.
* `fromRequest().body(String jsonPath)`: Returns the element from the request that
matches the JSON Path.
If you use the YAML contract definition, you have to use the
If you use the YAML contract definition or the Java one, you have to use the
https://handlebarsjs.com/[Handlebars] `{{{ }}}` notation with custom Spring Cloud Contract
functions to achieve this. In that case, you can use the following options:
@@ -777,6 +938,46 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract
----
include::{verifier_core_path}/src/test/resources/yml/contract_reference_request.yml[indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
package contracts.beer.rest;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.map;
class shouldReturnStatsForAUser implements Supplier<Contract> {
@Override
public Contract get() {
return Contract.make(c -> {
c.request(r -> {
r.method("POST");
r.url("/stats");
r.body(map().entry("name", r.anyAlphaUnicode()));
r.headers(h -> {
h.contentType(h.applicationJson());
});
});
c.response(r -> {
r.status(r.OK());
r.body(map()
.entry("text",
"Dear {{{jsonPath request.body '$.name'}}} thanks for your interested in drinking beer")
.entry("quantity", r.$(r.c(5), r.p(r.anyNumber()))));
r.headers(h -> {
h.contentType(h.applicationJson());
});
});
});
}
}
----
====
Running a JUnit test generation leads to a test that resembles the following example:
@@ -896,7 +1097,7 @@ contract.
Currently, Spring Cloud Contract Verifier supports only JSON path-based matchers with the
following matching possibilities:
===== Groovy DSL
===== Coded DSL
* For the stubs (in tests on the consumer's side):
** `byEquality()`: The value taken from the consumer's request in the provided JSON path must be
@@ -1149,8 +1350,6 @@ As you can see, the assertion is malformed. Only the first element of the array
asserted. In order to fix this, you should apply the assertion to the whole `$.events`
collection and assert it with the `byCommand(...)` method.
[[contract-dsl-async]]
=== Asynchronous Support
@@ -1181,6 +1380,27 @@ org.springframework.cloud.contract.spec.Contract.make {
response:
async: true
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
class contract implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
// ...
});
c.response(r -> {
r.async();
// ...
});
}));
}
}
----
====
You can also use the `fixedDelayMilliseconds` method or property to add delay to your stubs.
@@ -1209,6 +1429,27 @@ org.springframework.cloud.contract.spec.Contract.make {
response:
fixedDelayMilliseconds: 1000
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
class contract implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
// ...
});
c.response(r -> {
r.fixedDelayMilliseconds(1000);
// ...
});
}));
}
}
----
====
[[contract-dsl-xml]]
@@ -1235,6 +1476,12 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract
----
include::{verifier_core_path}/src/test/resources/yml/contract_rest_xml.yml
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
include::{verifier_core_path}/src/test/resources/contractsToCompile/contract_xml.java[tags=class,indent=0]
----
====
The following example shows an automatically generated test for XML in the response body:
@@ -1286,6 +1533,28 @@ include::{plugins_path}/spring-cloud-contract-maven-plugin/src/test/projects/mul
----
include::{verifier_core_path}/src/test/resources/yml/multiple_contracts.yml[indent=0]
----
[source,java,indent=0,subs="verbatim,attributes",role="secondary"]
.java
----
class contract implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Arrays.asList(
Contract.make(c -> {
c.name("should post a user");
// ...
}), Contract.make(c -> {
// ...
}), Contract.make(c -> {
// ...
})
);
}
}
----
====
In the preceding example, one contract has the `name` field and the other does not. This
@@ -1395,9 +1664,5 @@ name of `scenario1` and the three following steps:
. showCart, marked as `Step1` pointing to...
. logout, marked as `Step2` (which closes the scenario).
You can find nore details about WireMock scenarios at
https://wiremock.org/docs/stateful-behaviour/[https://wiremock.org/docs/stateful-behaviour/].
Spring Cloud Contract also generates tests with a guaranteed order of execution.
// TODO: How can someone specify the order of execution in SC Contract?
// That sentence is a great lead-in to more content, which then does not appear.
You can find more details about WireMock scenarios at
https://wiremock.org/docs/stateful-behaviour/[https://wiremock.org/docs/stateful-behaviour/].

View File

@@ -35,6 +35,7 @@
<spring-cloud-netflix.version>2.2.0.BUILD-SNAPSHOT</spring-cloud-netflix.version>
<spring-cloud-consul.version>2.2.0.BUILD-SNAPSHOT</spring-cloud-consul.version>
<spring-cloud-commons.version>2.2.0.BUILD-SNAPSHOT</spring-cloud-commons.version>
<spring-cloud-function.version>3.0.0.BUILD-SNAPSHOT</spring-cloud-function.version>
<jopt-simple.version>5.0.4</jopt-simple.version>
<cglib.version>3.2.9</cglib.version>
<spock-spring.version>1.3-groovy-2.5</spock-spring.version>
@@ -61,6 +62,7 @@
<xerces.version>2.11.0</xerces.version>
<jacoco-maven-plugin.version>0.8.4</jacoco-maven-plugin.version>
<exec-maven-plugin.version>1.6.0</exec-maven-plugin.version>
<InMemoryJavaCompiler.version>1.3.0</InMemoryJavaCompiler.version>
</properties>
<modules>
@@ -88,6 +90,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-compiler</artifactId>
<version>${spring-cloud-function.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-spring-boot-starter</artifactId>

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2013-2019 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.loan;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerPort;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
// tag::autoconfigure_stubrunner[]
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {
"com.example:http-server-dsl:+:stubs:6565"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class TestControllerTests {
// end::autoconfigure_stubrunner[]
@StubRunnerPort("http-server-dsl")
int port;
@Test
public void should_work_with_java_dsl() {
// when:
ResponseEntity<String> response = new RestTemplate()
.getForEntity("http://localhost:" + this.port + "/example", String.class);
// then:
assertThat(response.getStatusCodeValue()).isEqualTo(200);
}
}

View File

@@ -24,6 +24,7 @@ import java.nio.file.Files;
import java.util.Arrays;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -60,6 +61,10 @@ public class TestController {
return new Test("ok");
}
@GetMapping("/example")
public void example() {
}
@PutMapping(value = "/1", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public byte[] response(@RequestBody byte[] requestBody) {
if (!Arrays.equals(this.request, requestBody)) {

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2013-2019 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.Before;
public class TestBase {
@Before
public void setUp() throws Exception {
RestAssuredMockMvc.standaloneSetup(new TestController());
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2013-2019 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 test;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
class MyHttpContract implements Supplier<Contract> {
@Override
public Contract get() {
return Contract.make(c -> {
c.request(r -> {
r.method(r.GET());
r.url("/example");
});
c.response(r -> {
r.status(r.OK());
});
});
}
}

View File

@@ -19,6 +19,8 @@
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="org.springframework.cloud" level="DEBUG"/>
<logger name="org.springframework.cloud.function" level="INFO"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>

View File

@@ -59,12 +59,13 @@ class RecursiveFilesConverter {
}
void processFiles() {
ContractFileScanner scanner = new ContractFileScannerBuilder().
baseDir(props.contractsDslDir)
.excluded(props.excludedFiles as Set)
.ignored([] as Set).included([] as Set)
.includeMatcher(props.includedContracts)
.build()
ContractFileScanner scanner = new ContractFileScannerBuilder()
.baseDir(props.contractsDslDir)
.excluded(props.excludedFiles as Set)
.ignored([] as Set)
.included([] as Set)
.includeMatcher(props.includedContracts)
.build()
ListMultimap<Path, ContractMetadata> contracts = scanner.findContracts()
if (log.isDebugEnabled()) {
log.debug("Found the following contracts $contracts")

View File

@@ -177,10 +177,14 @@
<artifactId>spring-boot-starter-jersey</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-compiler</artifactId>
</dependency>
<dependency>
<groupId>org.mdkt.compiler</groupId>
<artifactId>InMemoryJavaCompiler</artifactId>
<version>1.2</version>
<version>${InMemoryJavaCompiler.version}</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -242,6 +246,24 @@
<testNGArtifactName>none:none</testNGArtifactName>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/test/resources/contractsToCompile</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -123,6 +123,9 @@ class TestGenerator {
void generateTestClasses(final String basePackageName) {
ListMultimap<Path, ContractMetadata> contracts = contractFileScanner.
findContracts()
if (log.isDebugEnabled()) {
log.debug("Found the following contracts " + contracts.keySet())
}
Set<Map.Entry<Path,Collection<ContractMetadata>>> inProgress = contracts.asMap().entrySet()
.findAll { Map.Entry<Path, Collection<ContractMetadata>> entry -> entry.value.any { it.anyInProgress() }}
if (!inProgress.isEmpty() && configProperties.failOnInProgress) {

View File

@@ -41,6 +41,7 @@ class ToYamlConverter {
}
private static YamlContractConverter yamlContractConverter = new YamlContractConverter()
private static final List<ContractConverter> CONTRACT_CONVERTERS = converters()
protected static void doReplaceContractWithYaml(ContractConverter converter, File file) {
@@ -105,27 +106,7 @@ class ToYamlConverter {
List<ContractConverter> converters =
SpringFactoriesLoader.loadFactories(ContractConverter, null)
converters.add(YamlContractConverter.INSTANCE)
converters.add(GroovyContractConverter.INSTANCE)
converters.add(ContractVerifierDslConverter.INSTANCE)
return converters
}
}
class GroovyContractConverter implements ContractConverter<Collection<Contract>> {
static final GroovyContractConverter INSTANCE = new GroovyContractConverter()
@Override
boolean isAccepted(File file) {
return file.name.endsWith(".groovy")
}
@Override
Collection<Contract> convertFrom(File file) {
return ContractVerifierDslConverter.convertAsCollection(file)
}
@Override
Collection<Contract> convertTo(Collection<Contract> contract) {
return contract
}
}

View File

@@ -139,6 +139,7 @@ class ContractFileScanner {
protected List<ContractConverter> convertersWithYml() {
List<ContractConverter> converters = converters()
converters.add(ContractVerifierDslConverter.INSTANCE)
converters.add(YamlContractConverter.INSTANCE)
return converters
}
@@ -213,22 +214,7 @@ class ContractFileScanner {
}
private boolean isContractFile(File file) {
return file.isFile() && getFilenameExtension(file.toString())?.equalsIgnoreCase("groovy")
}
private static String getFilenameExtension(String path) {
if (path == null) {
return null
}
int extIndex = path.lastIndexOf('.')
if (extIndex == -1) {
return null
}
int folderIndex = path.lastIndexOf('/')
if (folderIndex > extIndex) {
return null
}
return path.substring(extIndex + 1)
return file.isFile() && ContractVerifierDslConverter.INSTANCE.isAccepted(file)
}
/**

View File

@@ -85,7 +85,7 @@ final class ContractScanner {
contractDescriptors
.addAll(ContractVerifierDslConverter
.convertAsCollection(
file.getParentFile(), file))
file.getParentFile(), file).findAll { it })
}
else if (converter != null
&& converter.isAccepted(file)) {
@@ -120,6 +120,6 @@ final class ContractScanner {
}
private static boolean isContractDescriptor(File file) {
return file.isFile() && file.getName().endsWith(".groovy")
return ContractVerifierDslConverter.INSTANCE.isAccepted(file)
}
}

View File

@@ -16,16 +16,23 @@
package org.springframework.cloud.contract.verifier.util
import java.lang.reflect.Constructor
import java.util.function.Supplier
import java.util.regex.Matcher
import java.util.regex.Pattern
import groovy.transform.CompileStatic
import groovy.util.logging.Commons
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.ContractConverter
import org.springframework.cloud.function.compiler.java.CompilationResult
import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler
import org.springframework.util.StringUtils
/**
* Converts a file or String into a {@link Contract}
* Converts a String or a Groovy or Java file into a {@link Contract}.
*
* @author Marcin Grzejszczak
*
@@ -33,7 +40,15 @@ import org.springframework.util.StringUtils
*/
@CompileStatic
@Commons
class ContractVerifierDslConverter {
class ContractVerifierDslConverter implements ContractConverter<Collection<Contract>> {
public static final ContractVerifierDslConverter INSTANCE = new ContractVerifierDslConverter()
private static final Pattern PACKAGE_PATTERN = Pattern.compile(".+?package (.+?);.+?", Pattern.DOTALL)
private static final Pattern CLASS_PATTERN = Pattern.compile(".+?class (.+?)( |\\{).+?", Pattern.DOTALL)
private static final RuntimeJavaCompiler COMPILER = new RuntimeJavaCompiler()
/**
* @deprecated - use {@link ContractVerifierDslConverter#convertAsCollection(java.io.File, java.lang.String)}
@@ -80,7 +95,7 @@ class ContractVerifierDslConverter {
ClassLoader classLoader = ContractVerifierDslConverter.getClassLoader()
try {
ClassLoader urlCl = updatedClassLoader(rootFolder, classLoader)
Object object = groovyShell(urlCl, rootFolder).evaluate(dsl)
Object object = toObject(urlCl, rootFolder, dsl)
return listOfContracts(dsl, object)
}
catch (DslParseException e) {
@@ -106,6 +121,65 @@ class ContractVerifierDslConverter {
return new GroovyShell(ContractVerifierDslConverter.classLoader, new CompilerConfiguration(sourceEncoding: 'UTF-8'))
}
private static Object toObject(ClassLoader cl, File rootFolder, File dsl) {
if (isJava(dsl)) {
try {
return parseJavaFile(dsl)
}
catch (Exception ex) {
if (log.isWarnEnabled()) {
log.warn("Exception occurred while trying to parse the file [" + dsl + "] as a contract. Will not parse it.", ex)
}
return null
}
}
return groovyShell(cl, rootFolder).evaluate(dsl)
}
private static Object parseJavaFile(File dsl) {
Constructor<?> constructor = classConstructor(dsl)
Object newInstance = constructor.newInstance()
if (!newInstance instanceof Supplier) {
if (log.isDebugEnabled()) {
log.debug("The class [" + dsl + "] is not instance of Supplier. Will not parse it as a contract")
}
return null
}
Supplier supplier = (Supplier) newInstance
return supplier.get()
}
private static Constructor<?> classConstructor(File dsl) {
String classText = dsl.text
String fqn = fqn(classText)
CompilationResult compilationResult = COMPILER
.compile(fqn, classText)
if (!compilationResult.wasSuccessful()) {
throw new IllegalStateException("Exceptions occurred while trying to compile the file " + compilationResult.compilationMessages)
}
Class<?> clazz = compilationResult.compiledClasses.find { it.name == fqn}
Constructor<?> constructor = clazz.getDeclaredConstructor()
constructor.setAccessible(true)
return constructor
}
private static boolean isJava(File dsl) {
return dsl.name.endsWith(".java")
}
private static String fqn(String classText) {
Matcher packageMatcher = PACKAGE_PATTERN.matcher(classText)
String fqn = "";
if (packageMatcher.matches()) {
fqn = packageMatcher.group(1) + "."
}
Matcher classMatcher = CLASS_PATTERN.matcher(classText)
if (!classMatcher.matches()) {
throw new IllegalAccessException("Can't parse the class name")
}
return fqn + classMatcher.group(1)
}
private static GroovyShell groovyShell(ClassLoader cl, File rootFolder) {
return new GroovyShell(cl,
new CompilerConfiguration(sourceEncoding: 'UTF-8',
@@ -123,7 +197,10 @@ class ContractVerifierDslConverter {
}
private static Collection<Contract> listOfContracts(File file, Object object) {
if (object instanceof Collection) {
if (object == null) {
return Collections.emptyList()
}
else if (isACollectionOfContracts(object)) {
return withName(file, object as Collection<Contract>)
}
else if (!object instanceof Contract) {
@@ -132,6 +209,10 @@ class ContractVerifierDslConverter {
return withName(file, [object] as Collection<Contract>)
}
private static boolean isACollectionOfContracts(object) {
return object instanceof Collection && ((Collection) object).every { it instanceof Contract }
}
private static Collection<Contract> withName(File file, Collection<Contract> contracts) {
int counter = 0
return contracts.collect {
@@ -146,4 +227,19 @@ class ContractVerifierDslConverter {
private static boolean contractNameEmpty(Contract it) {
return it != null && StringUtils.isEmpty(it.name)
}
@Override
boolean isAccepted(File file) {
return file.name.endsWith(".groovy") || file.name.endsWith(".gvy") || file.name.endsWith(".java")
}
@Override
Collection<Contract> convertFrom(File file) {
return convertAsCollection(file)
}
@Override
Collection<Contract> convertTo(Collection<Contract> contract) {
return contract
}
}

View File

@@ -22,6 +22,10 @@ import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
@@ -105,4 +109,95 @@ public final class ContractVerifierUtil {
}
}
/**
* @return a builder for map
*/
public static ContractVerifierMap map() {
return new ContractVerifierMap();
}
/**
* A map with a fluent interface.
*/
public static class ContractVerifierMap implements Map<Object, Object> {
private final Map<Object, Object> delegate = new HashMap<>();
public ContractVerifierMap entry(String key, Object value) {
put(key, value);
return this;
}
@Override
public int size() {
return this.delegate.size();
}
@Override
public boolean isEmpty() {
return this.delegate.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return this.delegate.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return this.delegate.containsValue(value);
}
@Override
public Object get(Object key) {
return this.delegate.get(key);
}
@Override
public Object put(Object key, Object value) {
return this.delegate.put(key, value);
}
@Override
public Object remove(Object key) {
return this.delegate.remove(key);
}
@Override
public void putAll(Map<? extends Object, ?> m) {
this.delegate.putAll(m);
}
@Override
public void clear() {
this.delegate.clear();
}
@Override
public Set<Object> keySet() {
return this.delegate.keySet();
}
@Override
public Collection<Object> values() {
return this.delegate.values();
}
@Override
public Set<Entry<Object, Object>> entrySet() {
return this.delegate.entrySet();
}
@Override
public boolean equals(Object o) {
return this.delegate.equals(o);
}
@Override
public int hashCode() {
return this.delegate.hashCode();
}
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.contract.verifier.builder
import org.junit.Rule
import org.mdkt.compiler.CompilationException
import spock.lang.Issue
import spock.lang.Shared
import spock.lang.Specification
@@ -237,10 +238,10 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
try {
SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test)
}
catch (ClassFormatError classFormatError) {
String output = outputCapture.toString()
assert output.contains('error: cannot find symbol')
assert output.contains('assertThatValueIsANumber(parsedJson.read("$.duck"));')
catch (CompilationException classFormatError) {
String output = classFormatError.message
assert output.contains('cannot find symbol')
assert output.contains('assertThatValueIsANumber')
}
where:
methodBuilderName | methodBuilder | rootElement

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2013-2019 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.converter
import groovy.json.JsonSlurper
import spock.lang.Shared
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.internal.ExecutionProperty
import org.springframework.cloud.contract.spec.internal.FromFileProperty
import org.springframework.cloud.contract.spec.internal.RegexProperty
import org.springframework.cloud.contract.spec.internal.Url
import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter
import org.springframework.cloud.contract.verifier.util.MapConverter
import static org.springframework.cloud.contract.spec.internal.MatchingType.COMMAND
import static org.springframework.cloud.contract.spec.internal.MatchingType.NULL
import static org.springframework.cloud.contract.spec.internal.MatchingType.REGEX
/**
* @author Marcin Grzejszczak
*/
class JavaContractConverterSpec extends Specification {
@Shared
URL javaRest = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest.java")
@Shared
File javaRestFile = new File(javaRest.toURI())
@Shared
URL javaRestWithTags = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest_with_tags.java")
@Shared
File javaRestWithTagsFile = new File(javaRestWithTags.toURI())
@Shared
URL contractBody = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest_from_file.java")
@Shared
File contractBodyFile = new File(contractBody.toURI())
@Shared
URL contractBodyBytes = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest_from_pdf.java")
@Shared
File contractBodyBytesFile = new File(contractBodyBytes.toURI())
@Shared
URL docs = JavaContractConverterSpec.getResource("/contractsToCompile/contract_docs_examples.java")
@Shared
File docsFile = new File(docs.toURI())
def "should convert Java DSL with REST to DSL for [#contractFile]"() {
when:
Collection<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"), contractFile)
then:
Contract contract = contracts.first()
contract.description == "Some description"
contract.name == "some name"
contract.priority == 8
contract.ignored == true
Url url = contract.request.url
url.clientValue == "/foo"
url.queryParameters.parameters[0].name == "a"
url.queryParameters.parameters[0].serverValue == "b"
url.queryParameters.parameters[1].name == "b"
url.queryParameters.parameters[1].serverValue == "c"
contract.request.method.clientValue == "PUT"
contract.request.headers.entries.find {
it.name == "foo" &&
((RegexProperty) it.clientValue).pattern() == "bar" && it.serverValue == "bar"
}
contract.request.headers.entries.find {
it.name == "fooReq" &&
it.serverValue == "baz"
}
MapConverter.getStubSideValues(contract.request.body) == [foo: "bar"]
contract.request.bodyMatchers.matchers[0].path() == '$.foo'
contract.request.bodyMatchers.matchers[0].matchingType() == REGEX
contract.request.bodyMatchers.matchers[0].value().pattern() == 'bar'
and:
contract.response.status.clientValue == 200
contract.response.delay.clientValue == 1000
contract.response.headers.entries.find {
it.name == "foo2" &&
((RegexProperty) it.serverValue).pattern() == "bar" && it.clientValue == "bar"
}
contract.response.headers.entries.find {
it.name == "foo3" &&
((ExecutionProperty) it.serverValue).insertValue('foo') == "andMeToo(foo)"
}
contract.response.headers.entries.find {
it.name == "fooRes" &&
it.clientValue == "baz"
}
MapConverter.getStubSideValues(contract.response.body) == [foo2: "bar", foo3: "baz", nullValue: null]
contract.response.bodyMatchers.matchers[0].path() == '$.foo2'
contract.response.bodyMatchers.matchers[0].matchingType() == REGEX
contract.response.bodyMatchers.matchers[0].value().pattern() == 'bar'
contract.response.bodyMatchers.matchers[1].path() == '$.foo3'
contract.response.bodyMatchers.matchers[1].matchingType() == COMMAND
contract.response.bodyMatchers.matchers[1].value() == new ExecutionProperty('executeMe($it)')
contract.response.bodyMatchers.matchers[2].path() == '$.nullValue'
contract.response.bodyMatchers.matchers[2].matchingType() == NULL
contract.response.bodyMatchers.matchers[2].value() == null
where:
contractFile << [javaRestFile, javaRestWithTagsFile]
}
def "should convert java with REST with body from file"() {
when:
Collection<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"), contractBodyFile)
then:
contracts.size() == 1
Contract contract = contracts.first()
new JsonSlurper().parseText(contract.request.body.clientValue.toString()) ==
new JsonSlurper().parseText('''{ "hello" : "request" }''')
and:
new JsonSlurper().parseText(contract.response.body.clientValue.toString()) ==
new JsonSlurper().parseText('''{ "hello" : "response" }''')
}
def "should convert java with REST with body as bytes"() {
when:
Collection<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"), contractBodyBytesFile)
then:
contracts.size() == 1
Contract contract = contracts.first()
contract.request.body.clientValue instanceof FromFileProperty
and:
contract.response.body.clientValue instanceof FromFileProperty
}
def "should convert java with REST for docs"() {
when:
Collection<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"), docsFile)
then:
contracts.size() == 1
}
}

View File

@@ -27,6 +27,10 @@ class ContractVerifierDslConverterSpec extends Specification {
URL single = ContractVerifierDslConverterSpec.getResource("/contract.groovy")
File singleContract = new File(single.toURI())
URL singleJava = ContractVerifierDslConverterSpec.getResource("/contractsToCompile/contract.java")
File singleContractJava = new File(singleJava.toURI())
URL singleRestJava = ContractVerifierDslConverterSpec.getResource("/contractsToCompile/contract_rest.java")
File singleContractRestJava = new File(singleRestJava.toURI())
URL multiple = ContractVerifierDslConverterSpec.getResource("/multiple_contracts.groovy")
File multipleContracts = new File(multiple.toURI())
URL invalid = ContractVerifierDslConverterSpec.getResource("/contract.yml")
@@ -51,6 +55,25 @@ class ContractVerifierDslConverterSpec extends Specification {
}
}
Contract expectedSingleContractForJava = Contract.make {
name("contract")
request {
method('PUT')
headers {
contentType(applicationJson())
}
body(""" { "status" : "OK" } """)
url("/1")
}
response {
status OK()
body(""" { "status" : "OK" } """)
headers {
contentType(textPlain())
}
}
}
Contract expectedSingleContractForText = Contract.make {
request {
method('PUT')
@@ -154,6 +177,20 @@ class ContractVerifierDslConverterSpec extends Specification {
contract == [expectedSingleContract]
}
def "should convert file to a list of Contracts when there's only one declared java contract"() {
when:
Collection<Contract> contract = ContractVerifierDslConverter.convertAsCollection(new File("/"), singleContractJava)
then:
contract == [expectedSingleContract]
}
def "should convert file to a list of Contracts for a REST contract for docs"() {
when:
Collection<Contract> contract = ContractVerifierDslConverter.convertAsCollection(new File("/"), singleContractRestJava)
then:
contract == [expectedSingleContractForJava]
}
def "should convert text to a list of Contracts when there's only one declared contract"() {
when:
Collection<Contract> contract = ContractVerifierDslConverter.convertAsCollection(new File("/"), singleContract.text)

View File

@@ -45,6 +45,7 @@ import org.springframework.cloud.contract.verifier.messaging.internal.ContractVe
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil
import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler
import org.springframework.util.ReflectionUtils
/**
* checking the syntax of produced scripts
@@ -53,7 +54,7 @@ import org.springframework.util.ReflectionUtils
@Commons
class SyntaxChecker {
Entity entity
public static final RuntimeJavaCompiler COMPILER = new RuntimeJavaCompiler()
private static final String[] DEFAULT_IMPORTS = [
Contract.name,
@@ -179,20 +180,26 @@ private void test(String test) {
String fqnClassName = "com.example.${className}"
test = test.replaceAll("class FooTest", "class " + className)
.replaceAll("import javax.ws.rs.core.Response", "import javax.ws.rs.core.Response; import javax.ws.rs.client.WebTarget;")
return InMemoryJavaCompiler.compile(fqnClassName, test)
return compileJava(fqnClassName, test)
}
private static Class<?> compileJava(String fqnClassName, String test) {
return InMemoryJavaCompiler.newInstance()
.ignoreWarnings()
.compile(fqnClassName, test)
}
private static String className(String test) {
Random random = new Random()
int first = Math.abs(random.nextInt())
int hashCode = Math.abs(test.hashCode())
StringBuffer sourceCode = new StringBuffer()
String className = "TestClass_${first}_${hashCode}"
return className
}
static boolean tryToCompileJavaWithoutImports(String fqn, String test) {
InMemoryJavaCompiler.compile(fqn, test)
compileJava(fqn, test)
return true
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2013-2019 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 example;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
class contract implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
r.method(r.PUT());
r.headers(h -> {
h.contentType(h.applicationJson());
});
r.body(" { \"status\" : \"OK\" } ");
r.url("/1");
});
c.response(r -> {
r.status(r.OK());
r.body(" { \"status\" : \"OK\" } ");
r.headers(h -> {
h.contentType(h.textPlain());
});
});
}));
}
}

View File

@@ -0,0 +1,351 @@
/*
* Copyright 2013-2019 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.
*/
//tag::class[]
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
class contract_docs_examples implements Supplier<Collection<Contract>> {
org.springframework.cloud.contract.spec.Contract httpDsl =
// tag::http_dsl[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
// Definition of HTTP request part of the contract
// (this can be a valid request or invalid depending
// on type of contract being specified).
c.request(r -> {
r.method(r.GET());
r.url("/foo");
// ...
});
// Definition of HTTP response part of the contract
// (a service implementing this contract should respond
// with following response after receiving request
// specified in "request" part above).
c.response(r -> {
r.status(200);
// ...
});
// Contract priority, which can be used for overriding
// contracts (1 is highest). Priority is optional.
c.priority(1);
});
// end::http_dsl[]
org.springframework.cloud.contract.spec.Contract methodDsl = org.springframework.cloud.contract.spec.Contract
.make(c -> {
c.request(r -> {
// tag::method[]
r.method(r.GET());
// end::method[]
r.url("/foo");
});
c.response(r -> {
r.status(200);
});
c.priority(1);
});
org.springframework.cloud.contract.spec.Contract request =
// tag::request[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// HTTP request method (GET/POST/PUT/DELETE).
r.method("GET");
// Path component of request URL is specified as follows.
r.urlPath("/users");
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::request[]
org.springframework.cloud.contract.spec.Contract url =
// tag::url[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
r.method("GET");
// Specifying `url` and `urlPath` in one contract is illegal.
r.url("http://localhost:8888/users");
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::url[]
org.springframework.cloud.contract.spec.Contract urlPaths =
// tag::urlpath[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.urlPath("/users", u -> {
// Each parameter is specified in form
// `'paramName' : paramValue` where parameter value
// may be a simple literal or one of matcher functions,
// all of which are used in this example.
u.queryParameters(q -> {
// If a simple literal is used as value
// default matcher function is used (equalTo)
q.parameter("limit", 100);
// `equalTo` function simply compares passed value
// using identity operator (==).
q.parameter("filter", r.equalTo("email"));
// `containing` function matches strings
// that contains passed substring.
q.parameter("gender", r.value(
r.consumer(r.containing("[mf]")), r.producer("mf")));
// `matching` function tests parameter
// against passed regular expression.
q.parameter("offset", r.value(
r.consumer(r.matching("[0-9]+")), r.producer(123)));
// `notMatching` functions tests if parameter
// does not match passed regular expression.
q.parameter("loginStartsWith", r.value(
r.consumer(r.notMatching(".{0,2}")), r.producer(3)));
});
});
// ...
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::urlpath[]
org.springframework.cloud.contract.spec.Contract headers =
// tag::headers[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
// Each header is added in form `'Header-Name' : 'Header-Value'`.
// there are also some helper methods
r.headers(h -> {
h.header("key", "value");
h.contentType(h.applicationJson());
});
// ...
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::headers[]
org.springframework.cloud.contract.spec.Contract cookies =
// tag::cookies[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
// Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
// there are also some helper methods
r.cookies(ck -> {
ck.cookie("key", "value");
ck.cookie("another_key", "another_value");
});
// ...
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::cookies[]
org.springframework.cloud.contract.spec.Contract body =
// tag::body[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
// Currently only JSON format of request body is supported.
// Format will be determined from a header or body's content.
r.body("{ \"login\" : \"john\", \"name\": \"John The Contract\" }");
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::body[]
org.springframework.cloud.contract.spec.Contract bodyAsXml =
// tag::bodyAsXml[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
// In this case body will be formatted as XML.
r.body(r.equalToXml(
"<user><login>john</login><name>John The Contract</name></user>"));
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::bodyAsXml[]
org.springframework.cloud.contract.spec.Contract response =
// tag::response[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
});
c.response(r -> {
// Status code sent by the server
// in response to request specified above.
r.status(r.OK());
});
});
// end::response[]
org.springframework.cloud.contract.spec.Contract regex =
// tag::regex[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
r.method("GET");
r.url(r.$(r.consumer(r.regex("\\/[0-9]{2}")), r.producer("/12")));
});
c.response(r -> {
r.status(r.OK());
r.body(ContractVerifierUtil.map().entry("id", r.$(r.anyNumber()))
.entry("surname", r.$(r.consumer("Kowalsky"),
r.producer(r.regex("[a-zA-Z]+")))));
r.headers(h -> {
h.header("Content-Type", "text/plain");
});
});
});
// end::regex[]
org.springframework.cloud.contract.spec.Contract optionals =
// tag::optionals[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.priority(1);
c.name("optionals");
c.request(r -> {
r.method("POST");
r.url("/users/password");
r.headers(h -> {
h.contentType(h.applicationJson());
});
r.body(ContractVerifierUtil.map()
.entry("email",
r.$(r.consumer(r.optional(r.regex(r.email()))),
r.producer("abc@abc.com")))
.entry("callback_url", r.$(r.consumer(r.regex(r.hostname())),
r.producer("https://partners.com"))));
});
c.response(r -> {
r.status(404);
r.headers(h -> {
h.header("Content-Type", "application/json");
});
r.body(ContractVerifierUtil.map().entry("code", r.value(
r.consumer("123123"), r.producer(r.optional("123123")))));
});
});
// end::optionals[]
org.springframework.cloud.contract.spec.Contract method =
// tag::methodBuilder[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
r.method("PUT");
r.url(r.$(r.consumer(r.regex("^/api/[0-9]{2}$")),
r.producer("/api/12")));
r.headers(h -> {
h.header("Content-Type", "application/json");
});
r.body("[{\"text\": \"Gonna see you at Warsaw\" }]");
});
c.response(r -> {
r.body(ContractVerifierUtil.map()
.entry("path",
r.$(r.consumer("/api/12"),
r.producer(r.regex("^/api/[0-9]{2}$"))))
.entry("correlationId", r.$(r.consumer("1223456"), r
.producer(r.execute("isProperCorrelationId($it)")))));
r.status(r.OK());
});
});
// end::methodBuilder[]
@Override
public Collection<Contract> get() {
return Collections.singletonList(method);
}
}
// end::class[]

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2013-2019 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.
*/
//tag::class[]
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.DslProperty;
import org.springframework.cloud.contract.spec.internal.Request;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
class contract_multipart implements Supplier<Collection<Contract>> {
private static Map<String, DslProperty> namedProps(Request r) {
Map<String, DslProperty> map = new HashMap<>();
// name of the file
map.put("name", r.$(r.c(r.regex(r.nonEmpty())), r.p("filename.csv")));
// content of the file
map.put("content", r.$(r.c(r.regex(r.nonEmpty())), r.p("file content")));
// content type for the part
map.put("contentType", r.$(r.c(r.regex(r.nonEmpty())), r.p("application/json")));
return map;
}
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
r.method("PUT");
r.url("/multipart");
r.headers(h -> {
h.contentType("multipart/form-data;boundary=AaB03x");
});
r.multipart(ContractVerifierUtil.map()
// key (parameter name), value (parameter value) pair
.entry("formParameter",
r.$(r.c(r.regex("\".+\"")),
r.p("\"formParameterValue\"")))
.entry("someBooleanParameter",
r.$(r.c(r.regex(r.anyBoolean())), r.p("true")))
// a named parameter (e.g. with `file` name) that represents file
// with
// `name` and `content`. You can also call `named("fileName",
// "fileContent")`
.entry("file", r.named(namedProps(r))));
});
c.response(r -> {
r.status(r.OK());
});
}));
}
}
// end::class[]

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2013-2019 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.
*/
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
class contract_rest implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.description("Some description");
c.name("some name");
c.priority(8);
c.ignored();
c.request(r -> {
r.url("/foo", u -> {
u.queryParameters(q -> {
q.parameter("a", "b");
q.parameter("b", "c");
});
});
r.method(r.PUT());
r.headers(h -> {
h.header("foo", r.value(r.client(r.regex("bar")), r.server("bar")));
h.header("fooReq", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo", "bar"));
r.bodyMatchers(m -> {
m.jsonPath("$.foo", m.byRegex("bar"));
});
});
c.response(r -> {
r.fixedDelayMilliseconds(1000);
r.status(r.OK());
r.headers(h -> {
h.header("foo2", r.value(r.server(r.regex("bar")), r.client("bar")));
h.header("foo3", r.value(r.server(r.execute("andMeToo($it)")),
r.client("foo33")));
h.header("fooRes", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo2", "bar")
.entry("foo3", "baz").entry("nullValue", null));
r.bodyMatchers(m -> {
m.jsonPath("$.foo2", m.byRegex("bar"));
m.jsonPath("$.foo3", m.byCommand("executeMe($it)"));
m.jsonPath("$.nullValue", m.byNull());
});
});
}));
}
}
// end::class[]

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2013-2019 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.
*/
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
class contract_rest_from_file implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
r.url("/foo");
r.method(r.GET());
r.body(r.file("request.json"));
});
c.response(r -> {
r.status(r.OK());
r.body(r.file("response.json"));
});
}));
}
}
// end::class[]

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2013-2019 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.
*/
//tag::class[]
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
class contract_rest_from_pdf implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
r.url("/1");
r.method(r.PUT());
r.body(r.fileAsBytes("request.pdf"));
r.headers(h -> {
h.contentType(h.applicationOctetStream());
});
});
c.response(r -> {
r.status(r.OK());
r.body(r.fileAsBytes("response.pdf"));
r.headers(h -> {
h.contentType(h.applicationOctetStream());
});
});
}));
}
}
// end::class[]

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2013-2019 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.
*/
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
class contract_rest_with_tags implements Supplier<Collection<Contract>> {
static Object description = Collections.singletonList(
// tag::description[]
Contract.make(c -> {
c.description("Some description");
}));
// end::description[]
static Object name = Collections.singletonList(
// tag::name[]
Contract.make(c -> {
c.name("some name");
}));
// end::name[]
static Object ignored = Collections.singletonList(
// tag::ignored[]
Contract.make(c -> {
c.ignored();
}));
// end::ignored[]
static Object in_progress = Collections.singletonList(
// tag::in_progress[]
Contract.make(c -> {
c.inProgress();
}));
// end::in_progress[]
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.description("Some description");
c.name("some name");
c.priority(8);
c.ignored();
c.inProgress();
c.request(r -> {
r.url("/foo", u -> {
u.queryParameters(q -> {
q.parameter("a", "b");
q.parameter("b", "c");
});
});
r.method(r.PUT());
r.headers(h -> {
h.header("foo", r.value(r.client(r.regex("bar")), r.server("bar")));
h.header("fooReq", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo", "bar"));
r.bodyMatchers(m -> {
m.jsonPath("$.foo", m.byRegex("bar"));
});
});
c.response(r -> {
r.fixedDelayMilliseconds(1000);
r.status(r.OK());
r.headers(h -> {
h.header("foo2", r.value(r.server(r.regex("bar")), r.client("bar")));
h.header("foo3", r.value(r.server(r.execute("andMeToo($it)")),
r.client("foo33")));
h.header("fooRes", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo2", "bar")
.entry("foo3", "baz").entry("nullValue", null));
r.bodyMatchers(m -> {
m.jsonPath("$.foo2", m.byRegex("bar"));
m.jsonPath("$.foo3", m.byCommand("executeMe($it)"));
m.jsonPath("$.nullValue", m.byNull());
});
});
}));
}
}
// end::class[]

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2013-2019 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.
*/
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
class contract_xml implements Supplier<Contract> {
@Override
public Contract get() {
return Contract.make(c -> {
c.request(r -> {
r.method(r.GET());
r.urlPath("/get");
r.headers(h -> {
h.contentType(h.applicationXml());
});
});
c.response(r -> {
r.status(r.OK());
r.headers(h -> {
h.contentType(h.applicationXml());
});
r.body("<test>\n" + "<duck type='xtype'>123</duck>\n"
+ "<alpha>abc</alpha>\n" + "<list>\n" + "<elem>abc</elem>\n"
+ "<elem>def</elem>\n" + "<elem>ghi</elem>\n" + "</list>\n"
+ "<number>123</number>\n" + "<aBoolean>true</aBoolean>\n"
+ "<date>2017-01-01</date>\n"
+ "<dateTime>2017-01-01T01:23:45</dateTime>\n"
+ "<time>01:02:34</time>\n"
+ "<valueWithoutAMatcher>foo</valueWithoutAMatcher>\n"
+ "<key><complex>foo</complex></key>\n" + "</test>");
r.bodyMatchers(m -> {
m.xPath("/test/duck/text()", m.byRegex("[0-9]{3}"));
m.xPath("/test/duck/text()", m.byCommand("equals($it)"));
m.xPath("/test/duck/xxx", m.byNull());
m.xPath("/test/duck/text()", m.byEquality());
m.xPath("/test/alpha/text()", m.byRegex(r.onlyAlphaUnicode()));
m.xPath("/test/alpha/text()", m.byEquality());
m.xPath("/test/number/text()", m.byRegex(r.number()));
m.xPath("/test/date/text()", m.byDate());
m.xPath("/test/dateTime/text()", m.byTimestamp());
m.xPath("/test/time/text()", m.byTime());
m.xPath("/test/*/complex/text()", m.byEquality());
m.xPath("/test/duck/@type", m.byEquality());
});
});
});
};
}

View File

@@ -0,0 +1,3 @@
{
"hello": "request"
}

View File

@@ -0,0 +1,3 @@
{
"hello": "response"
}

View File

@@ -9,6 +9,8 @@
<logger name="com.github.jknack.handlebars.internal" level="INFO"/>
<logger name="org.springframework.cloud.function" level="INFO"/>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>