committed by
GitHub
parent
91b7b95557
commit
242c8208a9
@@ -30,7 +30,7 @@
|
||||
:standalone_pact_path: {samples_path}/standalone/dsl
|
||||
:standalone_restdocs_path: {samples_path}/standalone/restdocs
|
||||
:tests_path: {core_path}/tests
|
||||
:samples_branch: 2.2.x
|
||||
:samples_branch: 3.0.x
|
||||
:samples_url: https://raw.githubusercontent.com/spring-cloud-samples/spring-cloud-contract-samples/{samples_branch}
|
||||
:samples_code: https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/{samples_branch}/
|
||||
:doc_samples: {core_path}/samples/wiremock-jetty
|
||||
|
||||
@@ -193,6 +193,7 @@ The following sections describe the most common top-level elements:
|
||||
* <<contract-dsl-ignoring-contracts>>
|
||||
* <<contract-dsl-in-progress>>
|
||||
* <<contract-dsl-passing-values-from-files>>
|
||||
* <<contract-dsl-metadata>>
|
||||
|
||||
[[contract-dsl-description]]
|
||||
==== Description
|
||||
@@ -437,6 +438,40 @@ include::{contract_kotlin_spec_path}/src/test/resources/contracts/shouldWorkWith
|
||||
IMPORTANT: You should use this approach whenever you want to work with binary payloads,
|
||||
both for HTTP and messaging.
|
||||
|
||||
[[contract-dsl-metadata]]
|
||||
==== Metadata
|
||||
|
||||
You can add `metadata` to your contract. Via the metadata you can pass in configuration to extensions. Below you can find
|
||||
an example of using the `wiremock` key and value being WireMock's `StubMapping` object. Spring Cloud Contract is able to
|
||||
patch parts of your generated stub mapping with your custom code. You may want to do that in order to add webhooks, custom
|
||||
delays or integrate with third party WireMock extensions.
|
||||
|
||||
====
|
||||
[source,groovy,indent=0,role="primary"]
|
||||
.groovy
|
||||
----
|
||||
include::{standalone_samples_path}/http-server/src/test/resources/contracts/fraud/shouldReturnFraudStats.groovy[tags=metadata,indent=0]
|
||||
----
|
||||
|
||||
[source,yaml,indent=0,role="secondary"]
|
||||
.yml
|
||||
----
|
||||
include::{standalone_samples_path}/http-server/src/test/resources/contracts/yml/fraud/shouldReturnFraudStats.yml[tags=metadata,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=metadata,indent=0]
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.kotlin
|
||||
----
|
||||
include::{contract_kotlin_spec_path}/src/test/kotlin/org/springframework/cloud/contract/spec/ContractTests.kt[tags=metadata,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-http]]
|
||||
== Contracts for HTTP
|
||||
|
||||
|
||||
@@ -168,6 +168,54 @@ include::{wiremock_tests}/src/test/java/org/springframework/cloud/contract/wirem
|
||||
----
|
||||
====
|
||||
|
||||
[[customization-wiremock-from-metadata]]
|
||||
=== Customization of WireMock via Metadata
|
||||
|
||||
With version 3.0.0 you're able to set `metadata` in your contracts. If you set an entry with key equal to `wiremock` and the value
|
||||
will be a valid WireMock's `StubMapping` JSON / map or an actual `StubMapping` object, Spring Cloud Contract will patch the generated
|
||||
stub with part of your customization. Let's look at the following example
|
||||
|
||||
[source,yaml,indent=0]
|
||||
----
|
||||
include::{standalone_samples_path}/http-server/src/test/resources/contracts/yml/fraud/shouldReturnFraudStats.yml[tags=metadata,indent=0]
|
||||
----
|
||||
|
||||
In the `metadata` section we've set an entry with key `wiremock` and its value is a JSON `StubMapping` that sets a delay in the generated stub. Such code allowed us to get the following merged WireMock JSON stub.
|
||||
|
||||
[source,json,indent=0]
|
||||
----
|
||||
{
|
||||
"id" : "ebae49e2-a2a3-490c-a57f-ba28e26b81ea",
|
||||
"request" : {
|
||||
"url" : "/yamlfrauds",
|
||||
"method" : "GET"
|
||||
},
|
||||
"response" : {
|
||||
"status" : 200,
|
||||
"body" : "{\"count\":200}",
|
||||
"headers" : {
|
||||
"Content-Type" : "application/json"
|
||||
},
|
||||
"fixedDelayMilliseconds" : 2000,
|
||||
"transformers" : [ "response-template" ]
|
||||
},
|
||||
"uuid" : "ebae49e2-a2a3-490c-a57f-ba28e26b81ea"
|
||||
}
|
||||
----
|
||||
|
||||
The current implementation allows to manipulate only the stub side (we don't change the generated test). Also, what does not get changed
|
||||
are the whole request and body and headers of the response.
|
||||
|
||||
[[customization-wiremock-from-metadata-custom-processor]]
|
||||
==== Customization of WireMock via Metadata and a Custom Processor
|
||||
|
||||
If you want to apply a custom WireMock `StubMapping` post processing, you can under `META-INF/spring.factories` under the
|
||||
`org.springframework.cloud.contract.verifier.converter.StubProcessor` key register your own implementation of a stub processor. For your convenience we've created an interface called `org.springframework.cloud.contract.verifier.wiremock.WireMockStubPostProcessor` that is dedicated to WireMock.
|
||||
|
||||
You'll have to implement methods to inform Spring Cloud Contract whether the post processor is applicable for a given contract and how should the post processing look like.
|
||||
|
||||
IMPORTANT: On the consumer side, when using Stub Runner, remember to pass the custom `HttpServerStubConfigurer` implementation (e.g. the one that extends `WireMockHttpServerStubConfigurer`) where you'll register a custom extension of your choosing. If you don't do so, even you have a custom WireMock extension on the classpath, WireMock will not notice it, won't apply it and will print out a warning statement that the given extension was not found.
|
||||
|
||||
[[customization-pluggable-architecture]]
|
||||
== Using the Pluggable Architecture
|
||||
|
||||
|
||||
@@ -57,3 +57,15 @@ response:
|
||||
regex: bar
|
||||
- key: foo3
|
||||
predefined:
|
||||
metadata:
|
||||
wiremock:
|
||||
"postServeActions": {
|
||||
"webhook": {
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"method": "POST",
|
||||
"body": "{ \"result\": \"SUCCESS\" }",
|
||||
"url": "http://localhost:56299/callback"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ public class LoanApplicationService {
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
private int port = 6565;
|
||||
|
||||
private String prefix = "";
|
||||
|
||||
@Autowired
|
||||
public LoanApplicationService(RestTemplateBuilder builder) {
|
||||
@@ -61,13 +63,17 @@ public class LoanApplicationService {
|
||||
|
||||
// tag::client_call_server[]
|
||||
ResponseEntity<FraudServiceResponse> response = restTemplate.exchange(
|
||||
"http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
|
||||
"http://localhost:" + port + fraudCheck(), HttpMethod.PUT,
|
||||
new HttpEntity<>(request, httpHeaders), FraudServiceResponse.class);
|
||||
// end::client_call_server[]
|
||||
|
||||
return response.getBody();
|
||||
}
|
||||
|
||||
private String fraudCheck() {
|
||||
return "/" + prefix + "fraudcheck";
|
||||
}
|
||||
|
||||
private LoanApplicationResult buildResponseFromFraudResult(
|
||||
FraudServiceResponse response) {
|
||||
LoanApplicationStatus applicationStatus = null;
|
||||
@@ -86,7 +92,7 @@ public class LoanApplicationService {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||
ResponseEntity<Response> response = restTemplate.exchange(
|
||||
"http://localhost:" + port + "/frauds", HttpMethod.GET,
|
||||
"http://localhost:" + port + "/" + prefix + "frauds", HttpMethod.GET,
|
||||
new HttpEntity<>(httpHeaders), Response.class);
|
||||
return response.getBody().getCount();
|
||||
}
|
||||
@@ -95,7 +101,7 @@ public class LoanApplicationService {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||
ResponseEntity<Response> response = restTemplate.exchange(
|
||||
"http://localhost:" + port + "/drunks", HttpMethod.GET,
|
||||
"http://localhost:" + port + "/"+ prefix + "drunks", HttpMethod.GET,
|
||||
new HttpEntity<>(httpHeaders), Response.class);
|
||||
return response.getBody().getCount();
|
||||
}
|
||||
@@ -105,7 +111,7 @@ public class LoanApplicationService {
|
||||
httpHeaders.add("Cookie", "name=foo");
|
||||
httpHeaders.add("Cookie", "name2=bar");
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
"http://localhost:" + port + "/frauds/name", HttpMethod.GET,
|
||||
"http://localhost:" + port + "/" + prefix + "frauds/name", HttpMethod.GET,
|
||||
new HttpEntity<>(httpHeaders), String.class);
|
||||
return response.getBody();
|
||||
}
|
||||
@@ -114,4 +120,7 @@ public class LoanApplicationService {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public void setPrefix(String prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,31 +20,35 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
|
||||
import com.example.loan.model.Client;
|
||||
import com.example.loan.model.LoanApplication;
|
||||
import com.example.loan.model.LoanApplicationResult;
|
||||
import com.example.loan.model.LoanApplicationStatus;
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.response.ResponseOptions;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import org.assertj.core.api.BDDAssertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
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.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -52,7 +56,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
|
||||
@AutoConfigureStubRunner(ids = {
|
||||
"com.example:http-server-dsl:0.0.1:stubs" }, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
|
||||
"com.example:http-server-dsl:0.0.1:stubs"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
|
||||
public class LoanApplicationServiceTests {
|
||||
|
||||
// end::autoconfigure_stubrunner[]
|
||||
@@ -65,6 +69,7 @@ public class LoanApplicationServiceTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.service.setPrefix("");
|
||||
this.service.setPort(this.stubPort);
|
||||
}
|
||||
|
||||
@@ -112,6 +117,15 @@ public class LoanApplicationServiceTests {
|
||||
assertThat(count).isEqualTo(100);
|
||||
}
|
||||
|
||||
// metadata
|
||||
@Test
|
||||
public void shouldFailToSuccessfullyGetAllDrunksDueToTimeout() {
|
||||
LoanApplicationService service = new LoanApplicationService(new RestTemplateBuilder().setReadTimeout(Duration.ofSeconds(1)));
|
||||
service.setPort(this.stubPort);
|
||||
// when:
|
||||
BDDAssertions.thenThrownBy(service::countDrunks).hasMessageContaining("Read timed out");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSuccessfullyGetCookies() {
|
||||
// when:
|
||||
@@ -124,7 +138,7 @@ public class LoanApplicationServiceTests {
|
||||
public void shouldSuccessfullyWorkWithMultipart() {
|
||||
// given:
|
||||
RequestSpecification request = RestAssured.given()
|
||||
.baseUri("http://localhost:"+ stubPort + "/")
|
||||
.baseUri("http://localhost:" + stubPort + "/")
|
||||
.header("Content-Type", "multipart/form-data")
|
||||
.multiPart("file1", "filename1", "content1".getBytes())
|
||||
.multiPart("file2", "filename1", "content2".getBytes()).multiPart("test",
|
||||
@@ -153,7 +167,7 @@ public class LoanApplicationServiceTests {
|
||||
// when:
|
||||
ResponseEntity<byte[]> exchange = new RestTemplate()
|
||||
.exchange(
|
||||
RequestEntity.put(URI.create("http://localhost:"+ stubPort + "/1"))
|
||||
RequestEntity.put(URI.create("http://localhost:" + stubPort + "/1"))
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.body(Files.readAllBytes(request.toPath())),
|
||||
byte[].class);
|
||||
@@ -166,4 +180,52 @@ public class LoanApplicationServiceTests {
|
||||
assertThat(exchange.getBody()).isEqualTo(Files.readAllBytes(response.toPath()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSuccessfullyApplyForLoanForYaml() {
|
||||
this.service.setPrefix("yaml");
|
||||
// given:
|
||||
LoanApplication application = new LoanApplication(new Client("1234567890"),
|
||||
123.123);
|
||||
// when:
|
||||
LoanApplicationResult loanApplication = service.loanApplication(application);
|
||||
// then:
|
||||
assertThat(loanApplication.getLoanApplicationStatus())
|
||||
.isEqualTo(LoanApplicationStatus.LOAN_APPLIED);
|
||||
assertThat(loanApplication.getRejectionReason()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldBeRejectedDueToAbnormalLoanAmountForYaml() {
|
||||
this.service.setPrefix("yaml");
|
||||
// given:
|
||||
LoanApplication application = new LoanApplication(new Client("1234567890"),
|
||||
99999);
|
||||
// when:
|
||||
LoanApplicationResult loanApplication = service.loanApplication(application);
|
||||
// then:
|
||||
assertThat(loanApplication.getLoanApplicationStatus())
|
||||
.isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED);
|
||||
assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSuccessfullyGetAllFraudsForYaml() {
|
||||
this.service.setPrefix("yaml");
|
||||
// when:
|
||||
int count = service.countAllFrauds();
|
||||
// then:
|
||||
assertThat(count).isGreaterThanOrEqualTo(200);
|
||||
}
|
||||
|
||||
// metadata
|
||||
@Test
|
||||
public void shouldFailToSuccessfullyGetAllDrunksDueToTimeoutForYaml() {
|
||||
LoanApplicationService service = new LoanApplicationService(new RestTemplateBuilder().setReadTimeout(Duration.ofSeconds(1)));
|
||||
service.setPort(this.stubPort);
|
||||
service.setPrefix("yaml");
|
||||
// when:
|
||||
BDDAssertions.thenThrownBy(service::countAllFrauds).hasMessageContaining("Read timed out");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import org.springframework.cloud.contract.spec.Contract
|
||||
}
|
||||
}
|
||||
},
|
||||
// tag::metadata[]
|
||||
Contract.make {
|
||||
request {
|
||||
method GET()
|
||||
@@ -49,5 +50,14 @@ import org.springframework.cloud.contract.spec.Contract
|
||||
contentType("application/json")
|
||||
}
|
||||
}
|
||||
metadata([wiremock: '''\
|
||||
{
|
||||
"response" : {
|
||||
"fixedDelayMilliseconds": 2000
|
||||
}
|
||||
}
|
||||
'''
|
||||
])
|
||||
}
|
||||
// end::metadata[]
|
||||
]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
# tag::metadata[]
|
||||
name: "should count all frauds"
|
||||
request:
|
||||
method: GET
|
||||
@@ -9,6 +10,14 @@ response:
|
||||
count: 200
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
metadata:
|
||||
wiremock: >
|
||||
{
|
||||
"response" : {
|
||||
"fixedDelayMilliseconds": 2000
|
||||
}
|
||||
}
|
||||
# end::metadata[]
|
||||
---
|
||||
request:
|
||||
method: GET
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.contract.spec;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -95,6 +97,11 @@ public class Contract {
|
||||
*/
|
||||
private boolean inProgress;
|
||||
|
||||
/**
|
||||
* Mapping of metadata. Can be used for external integrations.
|
||||
*/
|
||||
private Map<String, Object> metadata = new HashMap<>();
|
||||
|
||||
public Contract() {
|
||||
|
||||
}
|
||||
@@ -258,6 +265,14 @@ public class Contract {
|
||||
consumer.call();
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends all entries to the existing metadata mapping.
|
||||
* @param map metadata to set
|
||||
*/
|
||||
public void metadata(Map<String, Object> map) {
|
||||
this.metadata.putAll(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the contract should be ignored or not.
|
||||
*/
|
||||
@@ -360,6 +375,14 @@ public class Contract {
|
||||
this.inProgress = inProgress;
|
||||
}
|
||||
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
@@ -376,13 +399,14 @@ public class Contract {
|
||||
&& Objects.equals(description, contract.description)
|
||||
&& Objects.equals(name, contract.name)
|
||||
&& Objects.equals(input, contract.input)
|
||||
&& Objects.equals(metadata, contract.metadata)
|
||||
&& Objects.equals(outputMessage, contract.outputMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(priority, request, response, label, description, name, input,
|
||||
outputMessage, ignored);
|
||||
outputMessage, metadata, ignored);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -25,115 +25,135 @@ import org.springframework.cloud.contract.spec.internal.*
|
||||
@ContractDslMarker
|
||||
class ContractDsl {
|
||||
|
||||
companion object {
|
||||
fun contract(dsl: ContractDsl.() -> Unit): Contract = ContractDsl().apply(dsl).get()
|
||||
}
|
||||
companion object {
|
||||
fun contract(dsl: ContractDsl.() -> Unit): Contract = ContractDsl().apply(dsl).get()
|
||||
}
|
||||
|
||||
/**
|
||||
* You can set the level of priority of this contract. If there are two contracts
|
||||
* mapped for example to the same endpoint, then the one with greater priority should
|
||||
* take precedence. A priority of 1 is highest and takes precedence over a priority of
|
||||
* 2.
|
||||
*/
|
||||
var priority: Int? = null
|
||||
/**
|
||||
* You can set the level of priority of this contract. If there are two contracts
|
||||
* mapped for example to the same endpoint, then the one with greater priority should
|
||||
* take precedence. A priority of 1 is highest and takes precedence over a priority of
|
||||
* 2.
|
||||
*/
|
||||
var priority: Int? = null
|
||||
|
||||
/**
|
||||
* The label by which you'll reference the contract on the message consumer side.
|
||||
*/
|
||||
var label: String? = null
|
||||
/**
|
||||
* The label by which you'll reference the contract on the message consumer side.
|
||||
*/
|
||||
var label: String? = null
|
||||
|
||||
/**
|
||||
* Description of a contract. May be used in the documentation generation.
|
||||
*/
|
||||
var description: String? = null
|
||||
/**
|
||||
* Description of a contract. May be used in the documentation generation.
|
||||
*/
|
||||
var description: String? = null
|
||||
|
||||
/**
|
||||
* Name of the generated test / stub. If not provided then the file name will be used.
|
||||
* If you have multiple contracts in a single file and you don't provide this value
|
||||
* then a prefix will be added to the file with the index number while iterating over
|
||||
* the collection of contracts.
|
||||
*
|
||||
* Remember to have a unique name for every single contract. Otherwise you might
|
||||
* generate tests that have two identical methods or you will override the stubs.
|
||||
*/
|
||||
var name: String? = null
|
||||
/**
|
||||
* Name of the generated test / stub. If not provided then the file name will be used.
|
||||
* If you have multiple contracts in a single file and you don't provide this value
|
||||
* then a prefix will be added to the file with the index number while iterating over
|
||||
* the collection of contracts.
|
||||
*
|
||||
* Remember to have a unique name for every single contract. Otherwise you might
|
||||
* generate tests that have two identical methods or you will override the stubs.
|
||||
*/
|
||||
var name: String? = null
|
||||
|
||||
/**
|
||||
* Whether the contract should be ignored or not.
|
||||
*/
|
||||
var ignored: Boolean = false
|
||||
/**
|
||||
* Whether the contract should be ignored or not.
|
||||
*/
|
||||
var ignored: Boolean = false
|
||||
|
||||
/**
|
||||
* Whether the contract is in progress. It's not ignored, but the feature is not yet
|
||||
* finished. Used together with the {@code generateStubs} option.
|
||||
*/
|
||||
var inProgress: Boolean = false
|
||||
/**
|
||||
* Whether the contract is in progress. It's not ignored, but the feature is not yet
|
||||
* finished. Used together with the {@code generateStubs} option.
|
||||
*/
|
||||
var inProgress: Boolean = false
|
||||
|
||||
/**
|
||||
* The HTTP request part of the contract.
|
||||
*/
|
||||
var request: Request? = null
|
||||
/**
|
||||
* The HTTP request part of the contract.
|
||||
*/
|
||||
var request: Request? = null
|
||||
|
||||
/**
|
||||
* The HTTP response part of the contract.
|
||||
*/
|
||||
var response: Response? = null
|
||||
/**
|
||||
* The HTTP response part of the contract.
|
||||
*/
|
||||
var response: Response? = null
|
||||
|
||||
/**
|
||||
* The input side of a messaging contract.
|
||||
*/
|
||||
var input: Input? = null
|
||||
/**
|
||||
* The input side of a messaging contract.
|
||||
*/
|
||||
var input: Input? = null
|
||||
|
||||
/**
|
||||
* The output side of a messaging contract.
|
||||
*/
|
||||
var outputMessage: OutputMessage? = null
|
||||
/**
|
||||
* The output side of a messaging contract.
|
||||
*/
|
||||
var outputMessage: OutputMessage? = null
|
||||
|
||||
/**
|
||||
* The HTTP request part of the contract.
|
||||
* @param configurer lambda to configure the HTTP request
|
||||
*/
|
||||
fun request(configurer: RequestDsl.() -> Unit) {
|
||||
request = RequestDsl().apply(configurer).get()
|
||||
}
|
||||
/**
|
||||
* Mapping of metadata. Can be used for external integrations.
|
||||
*/
|
||||
var metadata: Map<String, Any> = HashMap()
|
||||
|
||||
/**
|
||||
* The HTTP response part of the contract.
|
||||
* @param configurer lambda to configure the HTTP response
|
||||
*/
|
||||
fun response(configurer: ResponseDsl.() -> Unit) {
|
||||
response = ResponseDsl().apply(configurer).get()
|
||||
}
|
||||
/**
|
||||
* The HTTP request part of the contract.
|
||||
* @param configurer lambda to configure the HTTP request
|
||||
*/
|
||||
fun request(configurer: RequestDsl.() -> Unit) {
|
||||
request = RequestDsl().apply(configurer).get()
|
||||
}
|
||||
|
||||
/**
|
||||
* The input part of the contract.
|
||||
* @param configurer lambda to configure the input message
|
||||
*/
|
||||
fun input(configurer: InputDsl.() -> Unit) {
|
||||
input = InputDsl().apply(configurer).get()
|
||||
}
|
||||
/**
|
||||
* The HTTP response part of the contract.
|
||||
* @param configurer lambda to configure the HTTP response
|
||||
*/
|
||||
fun response(configurer: ResponseDsl.() -> Unit) {
|
||||
response = ResponseDsl().apply(configurer).get()
|
||||
}
|
||||
|
||||
/**
|
||||
* The output part of the contract.
|
||||
* @param configurer lambda to configure the output message
|
||||
*/
|
||||
fun outputMessage(configurer: OutputMessageDsl.() -> Unit) {
|
||||
outputMessage = OutputMessageDsl().apply(configurer).get()
|
||||
}
|
||||
/**
|
||||
* The input part of the contract.
|
||||
* @param configurer lambda to configure the input message
|
||||
*/
|
||||
fun input(configurer: InputDsl.() -> Unit) {
|
||||
input = InputDsl().apply(configurer).get()
|
||||
}
|
||||
|
||||
private fun get(): Contract {
|
||||
val contract = Contract()
|
||||
priority?.also { contract.priority = priority }
|
||||
label?.also { contract.label = label }
|
||||
description?.also { contract.description = description }
|
||||
name?.also { contract.name = name }
|
||||
contract.ignored = ignored
|
||||
contract.inProgress = inProgress
|
||||
request?.also { contract.request = request }
|
||||
response?.also { contract.response = response }
|
||||
input?.also { contract.input = input }
|
||||
outputMessage?.also { contract.outputMessage = outputMessage }
|
||||
return contract
|
||||
}
|
||||
/**
|
||||
* The output part of the contract.
|
||||
* @param configurer lambda to configure the output message
|
||||
*/
|
||||
fun outputMessage(configurer: OutputMessageDsl.() -> Unit) {
|
||||
outputMessage = OutputMessageDsl().apply(configurer).get()
|
||||
}
|
||||
|
||||
/**
|
||||
* The metadata.
|
||||
* @param map metadata to set
|
||||
*/
|
||||
fun metadata(map: Map<String, Any>) {
|
||||
metadata = metadata.plus(map)
|
||||
}
|
||||
|
||||
/**
|
||||
* The metadata.
|
||||
* @param metadata metadata to set
|
||||
*/
|
||||
fun metadata(vararg metadata: Pair<String, Any>) = metadata(metadata.toMap())
|
||||
|
||||
private fun get(): Contract {
|
||||
val contract = Contract()
|
||||
priority?.also { contract.priority = priority }
|
||||
label?.also { contract.label = label }
|
||||
description?.also { contract.description = description }
|
||||
name?.also { contract.name = name }
|
||||
contract.metadata = metadata
|
||||
contract.ignored = ignored
|
||||
contract.inProgress = inProgress
|
||||
request?.also { contract.request = request }
|
||||
response?.also { contract.response = response }
|
||||
input?.also { contract.input = input }
|
||||
outputMessage?.also { contract.outputMessage = outputMessage }
|
||||
return contract
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -305,6 +305,27 @@ then:
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should set contract metadata`() {
|
||||
val contract =
|
||||
// tag::metadata[]
|
||||
contract {
|
||||
metadata("wiremock" to """
|
||||
{
|
||||
"response" : {
|
||||
"fixedDelayMilliseconds": 2000
|
||||
}
|
||||
}""")
|
||||
}
|
||||
// end::metadata[]
|
||||
|
||||
assertDoesNotThrow {
|
||||
Contract.assertContract(contract)
|
||||
}.also {
|
||||
assertThat(contract.metadata).isNotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should make equals and hashcode work properly for URL`() {
|
||||
val a: Contract = contract {
|
||||
|
||||
@@ -32,6 +32,7 @@ import groovy.json.JsonOutput;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.DslProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Headers;
|
||||
@@ -86,15 +87,9 @@ class StubRunnerExecutor implements StubFinder {
|
||||
}
|
||||
return runningStubs();
|
||||
}
|
||||
try {
|
||||
HttpServerStubConfigurer configurer = stubRunnerOptions
|
||||
.getHttpServerStubConfigurer().newInstance();
|
||||
startStubServers(configurer, stubRunnerOptions, stubConfiguration,
|
||||
repository);
|
||||
}
|
||||
catch (InstantiationException | IllegalAccessException ex) {
|
||||
log.error("Failed to instantiate the HTTP stub configurer", ex);
|
||||
}
|
||||
HttpServerStubConfigurer configurer = BeanUtils
|
||||
.instantiateClass(stubRunnerOptions.getHttpServerStubConfigurer());
|
||||
startStubServers(configurer, stubRunnerOptions, stubConfiguration, repository);
|
||||
RunningStubs runningCollaborators = runningStubs();
|
||||
log.info("All stubs are now running " + runningCollaborators.toString());
|
||||
return runningCollaborators;
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
package org.springframework.cloud.contract.verifier.converter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.verifier.file.ContractMetadata;
|
||||
@@ -24,9 +26,10 @@ import org.springframework.cloud.contract.verifier.file.ContractMetadata;
|
||||
/**
|
||||
* Converts contracts into their stub representation.
|
||||
*
|
||||
* @param <T> - type of stub mapping
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public interface StubGenerator {
|
||||
public interface StubGenerator<T> {
|
||||
|
||||
/**
|
||||
* @param fileName - file name
|
||||
@@ -45,6 +48,35 @@ public interface StubGenerator {
|
||||
*/
|
||||
Map<Contract, String> convertContents(String rootName, ContractMetadata content);
|
||||
|
||||
/**
|
||||
* Post process a generated stub mapping.
|
||||
* @param stubMapping - mapping of a stub
|
||||
* @param contract - contract for which stub was generated
|
||||
* @return the converted stub mapping
|
||||
*/
|
||||
default T postProcessStubMapping(T stubMapping, Contract contract) {
|
||||
List<StubPostProcessor> processors = StubPostProcessor.PROCESSORS.stream()
|
||||
.filter(p -> p.isApplicable(contract)).collect(Collectors.toList());
|
||||
if (processors.isEmpty()) {
|
||||
return defaultStubMappingPostProcessing(stubMapping, contract);
|
||||
}
|
||||
T stub = stubMapping;
|
||||
for (StubPostProcessor processor : processors) {
|
||||
stub = (T) processor.postProcess(stub, contract);
|
||||
}
|
||||
return stub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub mapping to chose when no post processors where found on the classpath.
|
||||
* @param stubMapping - mapping of a stub
|
||||
* @param contract - contract for which stub was generated
|
||||
* @return the converted stub mapping
|
||||
*/
|
||||
default T defaultStubMappingPostProcessing(T stubMapping, Contract contract) {
|
||||
return stubMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inputFileName - name of the input file
|
||||
* @return the name of the converted stub file. If you have multiple contracts in a
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.converter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
|
||||
/**
|
||||
* Post processor of stub mappings.
|
||||
*
|
||||
* @param <T> type of stub mapping
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface StubPostProcessor<T> {
|
||||
|
||||
/**
|
||||
* List of registered stub post processors.
|
||||
*/
|
||||
List<StubPostProcessor> PROCESSORS = SpringFactoriesLoader
|
||||
.loadFactories(StubPostProcessor.class, null);
|
||||
|
||||
/**
|
||||
* @param stubMapping - generated stub mapping
|
||||
* @param contract - contract for which the mapping was generated
|
||||
* @return modified stub mapping
|
||||
*/
|
||||
default T postProcess(T stubMapping, Contract contract) {
|
||||
return stubMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param contract - contract for which the mapping was generated
|
||||
* @return {@code true} if this post process should be applied
|
||||
*/
|
||||
boolean isApplicable(Contract contract);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.wiremock;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.tomakehurst.wiremock.common.Metadata;
|
||||
import com.github.tomakehurst.wiremock.extension.Parameters;
|
||||
import com.github.tomakehurst.wiremock.http.ChunkedDribbleDelay;
|
||||
import com.github.tomakehurst.wiremock.http.DelayDistribution;
|
||||
import com.github.tomakehurst.wiremock.http.Fault;
|
||||
import com.github.tomakehurst.wiremock.http.ResponseDefinition;
|
||||
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
|
||||
class DefaultWireMockStubPostProcessor implements WireMockStubPostProcessor {
|
||||
|
||||
private static final List<Class> APPLICABLE_CLASSES = Arrays.asList(String.class,
|
||||
StubMapping.class, Map.class);
|
||||
|
||||
public static final String WIREMOCK_METADATA_ENTRY = "wiremock";
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public StubMapping postProcess(StubMapping stubMapping, Contract contract) {
|
||||
Object wiremock = getWiremockEntry(contract);
|
||||
StubMapping stubMappingFromMetadata = stubMappingFromMetadata(wiremock);
|
||||
stubMapping.setResponse(mergedResponse(stubMapping, stubMappingFromMetadata));
|
||||
if (stubMappingFromMetadata.getPostServeActions() != null) {
|
||||
setPostServeActions(stubMapping, stubMappingFromMetadata);
|
||||
}
|
||||
if (stubMappingFromMetadata.getMetadata() != null) {
|
||||
setMetadata(stubMapping, stubMappingFromMetadata);
|
||||
}
|
||||
return stubMapping;
|
||||
}
|
||||
|
||||
public void setPostServeActions(StubMapping stubMapping,
|
||||
StubMapping stubMappingFromMetadata) {
|
||||
Map<String, Parameters> postServeActions = stubMapping.getPostServeActions();
|
||||
postServeActions = postServeActions != null ? postServeActions : new HashMap<>();
|
||||
postServeActions.putAll(stubMappingFromMetadata.getPostServeActions());
|
||||
stubMapping.setPostServeActions(postServeActions);
|
||||
}
|
||||
|
||||
public void setMetadata(StubMapping stubMapping,
|
||||
StubMapping stubMappingFromMetadata) {
|
||||
Metadata metadata = stubMapping.getMetadata();
|
||||
metadata = metadata != null ? metadata : new Metadata();
|
||||
metadata.putAll(stubMappingFromMetadata.getPostServeActions());
|
||||
stubMapping.setMetadata(metadata);
|
||||
}
|
||||
|
||||
public ResponseDefinition mergedResponse(StubMapping stubMapping,
|
||||
StubMapping stubMappingFromMetadata) {
|
||||
ResponseDefinition responseDefinition = new ResponseDefinition(
|
||||
stubMapping.getResponse().getStatus(),
|
||||
stubMapping.getResponse().getStatusMessage(),
|
||||
stubMapping.getResponse().getBody(),
|
||||
stubMapping.getResponse().getJsonBody(),
|
||||
stubMapping.getResponse().getBase64Body(),
|
||||
stubMapping.getResponse().getBodyFileName(),
|
||||
stubMapping.getResponse().getHeaders(),
|
||||
stubMapping.getResponse().getAdditionalProxyRequestHeaders(),
|
||||
fixedDelayMilliseconds(stubMapping, stubMappingFromMetadata),
|
||||
delayDistribution(stubMapping, stubMappingFromMetadata),
|
||||
chunkedDribbleDelay(stubMapping, stubMappingFromMetadata),
|
||||
proxyBaseUrl(stubMapping, stubMappingFromMetadata),
|
||||
fault(stubMapping, stubMappingFromMetadata),
|
||||
transformers(stubMapping, stubMappingFromMetadata),
|
||||
transformerParameters(stubMapping, stubMappingFromMetadata),
|
||||
wasConfigured(stubMapping, stubMappingFromMetadata));
|
||||
return responseDefinition;
|
||||
}
|
||||
|
||||
public Boolean wasConfigured(StubMapping stubMapping,
|
||||
StubMapping stubMappingFromMetadata) {
|
||||
return stubMappingFromMetadata.getResponse().isFromConfiguredStub() != null
|
||||
? stubMappingFromMetadata.getResponse().isFromConfiguredStub()
|
||||
: stubMapping.getResponse().isFromConfiguredStub();
|
||||
}
|
||||
|
||||
public Parameters transformerParameters(StubMapping stubMapping,
|
||||
StubMapping stubMappingFromMetadata) {
|
||||
return stubMappingFromMetadata.getResponse().getTransformerParameters() != null
|
||||
? stubMappingFromMetadata.getResponse().getTransformerParameters()
|
||||
: stubMapping.getResponse().getTransformerParameters();
|
||||
}
|
||||
|
||||
public List<String> transformers(StubMapping stubMapping,
|
||||
StubMapping stubMappingFromMetadata) {
|
||||
return stubMappingFromMetadata.getResponse().getTransformers() != null
|
||||
? stubMappingFromMetadata.getResponse().getTransformers()
|
||||
: stubMapping.getResponse().getTransformers();
|
||||
}
|
||||
|
||||
public Fault fault(StubMapping stubMapping, StubMapping stubMappingFromMetadata) {
|
||||
return stubMappingFromMetadata.getResponse().getFault() != null
|
||||
? stubMappingFromMetadata.getResponse().getFault()
|
||||
: stubMapping.getResponse().getFault();
|
||||
}
|
||||
|
||||
public String proxyBaseUrl(StubMapping stubMapping,
|
||||
StubMapping stubMappingFromMetadata) {
|
||||
return stubMappingFromMetadata.getResponse().getProxyBaseUrl() != null
|
||||
? stubMappingFromMetadata.getResponse().getProxyBaseUrl()
|
||||
: stubMapping.getResponse().getProxyBaseUrl();
|
||||
}
|
||||
|
||||
public ChunkedDribbleDelay chunkedDribbleDelay(StubMapping stubMapping,
|
||||
StubMapping stubMappingFromMetadata) {
|
||||
return stubMappingFromMetadata.getResponse().getChunkedDribbleDelay() != null
|
||||
? stubMappingFromMetadata.getResponse().getChunkedDribbleDelay()
|
||||
: stubMapping.getResponse().getChunkedDribbleDelay();
|
||||
}
|
||||
|
||||
public DelayDistribution delayDistribution(StubMapping stubMapping,
|
||||
StubMapping stubMappingFromMetadata) {
|
||||
return stubMappingFromMetadata.getResponse().getDelayDistribution() != null
|
||||
? stubMappingFromMetadata.getResponse().getDelayDistribution()
|
||||
: stubMapping.getResponse().getDelayDistribution();
|
||||
}
|
||||
|
||||
public Integer fixedDelayMilliseconds(StubMapping stubMapping,
|
||||
StubMapping stubMappingFromMetadata) {
|
||||
return stubMappingFromMetadata.getResponse().getFixedDelayMilliseconds() != null
|
||||
? stubMappingFromMetadata.getResponse().getFixedDelayMilliseconds()
|
||||
: stubMapping.getResponse().getFixedDelayMilliseconds();
|
||||
}
|
||||
|
||||
private Object getWiremockEntry(Contract contract) {
|
||||
return contract.getMetadata().get(WIREMOCK_METADATA_ENTRY);
|
||||
}
|
||||
|
||||
private StubMapping stubMappingFromMetadata(Object wiremock) {
|
||||
if (wiremock instanceof String) {
|
||||
return StubMapping.buildFrom((String) wiremock);
|
||||
}
|
||||
else if (wiremock instanceof StubMapping) {
|
||||
return (StubMapping) wiremock;
|
||||
}
|
||||
else if (wiremock instanceof Map) {
|
||||
try {
|
||||
return StubMapping
|
||||
.buildFrom(this.objectMapper.writeValueAsString(wiremock));
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException(
|
||||
"Failed to build StubMapping for map [" + wiremock + "]", e);
|
||||
}
|
||||
}
|
||||
throw new UnsupportedOperationException(
|
||||
"Unsupported type for wiremock metadata extension");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Contract contract) {
|
||||
boolean contains = contract.getMetadata().containsKey(WIREMOCK_METADATA_ENTRY);
|
||||
if (!contains) {
|
||||
return false;
|
||||
}
|
||||
Object wiremock = getWiremockEntry(contract);
|
||||
return APPLICABLE_CLASSES.stream()
|
||||
.anyMatch(aClass -> aClass.isAssignableFrom(wiremock.getClass()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,6 +24,8 @@ import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockStubStrategy;
|
||||
import org.springframework.cloud.contract.verifier.file.ContractMetadata;
|
||||
@@ -39,7 +41,31 @@ public class DslToWireMockClientConverter extends DslToWireMockConverter {
|
||||
|
||||
private String convertASingleContract(String rootName, ContractMetadata contract,
|
||||
Contract dsl) {
|
||||
return new WireMockStubStrategy(rootName, contract, dsl).toWireMockClientStub();
|
||||
StubMapping stubMapping = new WireMockStubStrategy(rootName, contract, dsl)
|
||||
.toWireMockClientStub();
|
||||
StubMapping mapping = postProcessStubMapping(stubMapping, dsl);
|
||||
if (mapping == null) {
|
||||
return "";
|
||||
}
|
||||
return mapping.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StubMapping postProcessStubMapping(StubMapping stubMapping,
|
||||
Contract contract) {
|
||||
StubMapping mapping = super.postProcessStubMapping(stubMapping, contract);
|
||||
// apply the default WireMock processor as the last one
|
||||
return defaultStubMappingPostProcessing(mapping, contract);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StubMapping defaultStubMappingPostProcessing(StubMapping stubMapping,
|
||||
Contract contract) {
|
||||
DefaultWireMockStubPostProcessor processor = new DefaultWireMockStubPostProcessor();
|
||||
if (processor.isApplicable(contract)) {
|
||||
return processor.postProcess(stubMapping, contract);
|
||||
}
|
||||
return stubMapping;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.contract.verifier.wiremock;
|
||||
|
||||
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.StubGenerator;
|
||||
|
||||
/**
|
||||
@@ -23,7 +25,7 @@ import org.springframework.cloud.contract.verifier.converter.StubGenerator;
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public abstract class DslToWireMockConverter implements StubGenerator {
|
||||
public abstract class DslToWireMockConverter implements StubGenerator<StubMapping> {
|
||||
|
||||
@Override
|
||||
public String generateOutputFileNameForInput(String inputFileName) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.wiremock;
|
||||
|
||||
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.StubPostProcessor;
|
||||
|
||||
/**
|
||||
* Post processor of WireMock stub mappings.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface WireMockStubPostProcessor extends StubPostProcessor<StubMapping> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.wiremock;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.tomakehurst.wiremock.extension.Parameters;
|
||||
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
|
||||
class DefaultWireMockStubPostProcessorTests {
|
||||
|
||||
// @formatter:off
|
||||
private static final String STUB_MAPPING = "{\n" + " \"request\": {\n"
|
||||
+ " \"method\": \"GET\",\n" + " \"url\": \"/ping\"\n"
|
||||
+ " },\n" + " \"response\": {\n" + " \"status\": 200,\n"
|
||||
+ " \"body\": \"pong\",\n" + " \"headers\": {\n"
|
||||
+ " \"Content-Type\": \"text/plain\"\n" + " }\n" + " }\n"
|
||||
+ "}";
|
||||
|
||||
private static final String POST_SERVE_ACTION = "{ \"postServeActions\": {\n"
|
||||
+ " \"webhook\": {\n" + " \"headers\": {\n"
|
||||
+ " \"Content-Type\": \"application/json\"\n" + " },\n"
|
||||
+ " \"method\": \"POST\",\n"
|
||||
+ " \"body\": \"{ \\\"result\\\": \\\"SUCCESS\\\" }\",\n"
|
||||
+ " \"url\": \"http://localhost:56299/callback\"\n" + " }\n"
|
||||
+ " } }";
|
||||
|
||||
private static final String RESPONSE_DELAY = "{\n"
|
||||
+ " \"response\": {\n"
|
||||
+ " \"delayDistribution\": {\n"
|
||||
+ " \"type\": \"lognormal\",\n"
|
||||
+ " \"median\": 80,\n"
|
||||
+ " \"sigma\": 0.4\n"
|
||||
+ " }\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
|
||||
@Test
|
||||
void should_not_be_applicable_for_missing_metadata_entry() {
|
||||
then(new DefaultWireMockStubPostProcessor().isApplicable(new Contract()))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_not_be_applicable_for_invalid_metadata_entry() {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock", 5);
|
||||
|
||||
then(new DefaultWireMockStubPostProcessor().isApplicable(contract)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_be_applicable_for_valid_metadata_entry() {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock", "foo");
|
||||
|
||||
then(new DefaultWireMockStubPostProcessor().isApplicable(contract)).isTrue();
|
||||
|
||||
contract.getMetadata().put("wiremock", new StubMapping());
|
||||
|
||||
then(new DefaultWireMockStubPostProcessor().isApplicable(contract)).isTrue();
|
||||
|
||||
contract.getMetadata().put("wiremock", new HashMap<>());
|
||||
|
||||
then(new DefaultWireMockStubPostProcessor().isApplicable(contract)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_merge_stub_mappings_when_stub_mapping_is_string() {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock", POST_SERVE_ACTION);
|
||||
StubMapping stubMapping = StubMapping.buildFrom(STUB_MAPPING);
|
||||
|
||||
StubMapping result = new DefaultWireMockStubPostProcessor()
|
||||
.postProcess(stubMapping, contract);
|
||||
|
||||
thenPostServerActionWasSet(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_merge_stub_mappings_when_stub_mapping_is_stub_mapping() {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock", StubMapping.buildFrom(POST_SERVE_ACTION));
|
||||
StubMapping stubMapping = StubMapping.buildFrom(STUB_MAPPING);
|
||||
|
||||
StubMapping result = new DefaultWireMockStubPostProcessor()
|
||||
.postProcess(stubMapping, contract);
|
||||
|
||||
thenPostServerActionWasSet(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_merge_stub_mappings_when_stub_mapping_is_map()
|
||||
throws JsonProcessingException {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock",
|
||||
new ObjectMapper().readValue(POST_SERVE_ACTION, HashMap.class));
|
||||
StubMapping stubMapping = StubMapping.buildFrom(STUB_MAPPING);
|
||||
|
||||
StubMapping result = new DefaultWireMockStubPostProcessor()
|
||||
.postProcess(stubMapping, contract);
|
||||
|
||||
thenPostServerActionWasSet(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_merge_stub_mappings_when_stub_mapping_is_string_and_contains_response() {
|
||||
Contract contract = new Contract();
|
||||
contract.getMetadata().put("wiremock", RESPONSE_DELAY);
|
||||
StubMapping stubMapping = StubMapping.buildFrom(STUB_MAPPING);
|
||||
|
||||
StubMapping result = new DefaultWireMockStubPostProcessor()
|
||||
.postProcess(stubMapping, contract);
|
||||
|
||||
then(result.getRequest().getMethod().getName()).isEqualTo("GET");
|
||||
then(result.getResponse().getStatus()).isEqualTo(200);
|
||||
then(result.getResponse().getBody()).isEqualTo("pong");
|
||||
then(result.getResponse().getHeaders().size()).isEqualTo(1);
|
||||
then(result.getResponse().getDelayDistribution()).isNotNull();
|
||||
}
|
||||
|
||||
private void thenPostServerActionWasSet(StubMapping result) {
|
||||
then(result.getRequest().getMethod().getName()).isEqualTo("GET");
|
||||
then(result.getResponse().getStatus()).isEqualTo(200);
|
||||
then(result.getResponse().getBody()).isEqualTo("pong");
|
||||
then(result.getPostServeActions()).containsKey("webhook");
|
||||
Parameters webhook = result.getPostServeActions().get("webhook");
|
||||
then(webhook.getString("method")).isEqualTo("POST");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -61,6 +61,7 @@ class ContractsToYaml {
|
||||
yamlContract.inProgress = contract.inProgress
|
||||
yamlContract.description = contract.description
|
||||
yamlContract.label = contract.label
|
||||
yamlContract.metadata = contract.metadata
|
||||
request(contract, yamlContract)
|
||||
response(yamlContract, contract)
|
||||
input(contract, yamlContract)
|
||||
|
||||
@@ -54,6 +54,8 @@ public class YamlContract {
|
||||
|
||||
public boolean inProgress;
|
||||
|
||||
public Map<String, Object> metadata;
|
||||
|
||||
public static class Request {
|
||||
|
||||
public String method;
|
||||
|
||||
@@ -102,6 +102,9 @@ class YamlToContracts {
|
||||
if (yamlContract.inProgress) {
|
||||
inProgress()
|
||||
}
|
||||
if (yamlContract.metadata) {
|
||||
metadata(yamlContract.metadata)
|
||||
}
|
||||
if (yamlContract.request?.method) {
|
||||
request {
|
||||
method(yamlContract.request?.method)
|
||||
|
||||
@@ -51,9 +51,9 @@ class WireMockStubStrategy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts {@link ContractMetadata} to String version of {@link StubMapping}
|
||||
* Converts {@link ContractMetadata} to {@link StubMapping}
|
||||
*/
|
||||
String toWireMockClientStub() {
|
||||
StubMapping toWireMockClientStub() {
|
||||
StubMapping stubMapping = new StubMapping()
|
||||
RequestPattern request = wireMockRequestStubStrategy.buildClientRequestContent()
|
||||
ResponseDefinition response = wireMockResponseStubStrategy.
|
||||
@@ -66,11 +66,11 @@ class WireMockStubStrategy {
|
||||
stubMapping.response = response
|
||||
|
||||
if (!request || !response) {
|
||||
return ''
|
||||
return null
|
||||
}
|
||||
|
||||
if (groovyDsl.ignored || contract.ignored) {
|
||||
return ''
|
||||
return null
|
||||
}
|
||||
|
||||
if (contract.order != null) {
|
||||
@@ -80,6 +80,6 @@ class WireMockStubStrategy {
|
||||
stubMapping.newScenarioState = STEP_PREFIX + (contract.order + 1)
|
||||
}
|
||||
}
|
||||
return StubMapping.buildJsonStringFor(stubMapping)
|
||||
return stubMapping
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,6 +714,7 @@ name: "post1"
|
||||
priority: null
|
||||
ignored: false
|
||||
inProgress: false
|
||||
metadata: null
|
||||
'''
|
||||
String expectedYaml2 = '''\
|
||||
---
|
||||
@@ -756,6 +757,7 @@ name: "post2"
|
||||
priority: null
|
||||
ignored: false
|
||||
inProgress: false
|
||||
metadata: null
|
||||
'''
|
||||
when:
|
||||
Map<String, byte[]> strings = converter.store([
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.contract.verifier.dsl.wiremock
|
||||
import com.github.tomakehurst.wiremock.WireMockServer
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration
|
||||
import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer
|
||||
import com.github.tomakehurst.wiremock.stubbing.StubMapping
|
||||
import groovy.json.JsonBuilder
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
@@ -2003,7 +2004,7 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie
|
||||
when:
|
||||
def json = toWireMockClientJsonStub(groovyDsl)
|
||||
then:
|
||||
json == ''
|
||||
json == null
|
||||
}
|
||||
|
||||
@Issue('#30')
|
||||
@@ -2031,7 +2032,7 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie
|
||||
def json = new WireMockStubStrategy("Test", new ContractMetadata(null, true, 0, null, groovyDsl), groovyDsl).
|
||||
toWireMockClientStub()
|
||||
then:
|
||||
json == ''
|
||||
json == null
|
||||
}
|
||||
|
||||
@Issue('#237')
|
||||
@@ -2982,7 +2983,7 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie
|
||||
}
|
||||
}
|
||||
and:
|
||||
String wireMockStub = new WireMockStubStrategy("Test",
|
||||
StubMapping wireMockStub = new WireMockStubStrategy("Test",
|
||||
new ContractMetadata(null, false, 0, null, contractDsl), contractDsl)
|
||||
.toWireMockClientStub()
|
||||
|
||||
@@ -2992,7 +2993,7 @@ class WireMockGroovyDslSpec extends Specification implements WireMockStubVerifie
|
||||
server.start()
|
||||
and:
|
||||
stubMappingIsValidWireMockStub(wireMockStub)
|
||||
server.addStubMapping(WireMockStubMapping.buildFrom(wireMockStub))
|
||||
server.addStubMapping(wireMockStub)
|
||||
when:
|
||||
ResponseEntity<String> entity = callWithOptionalAndEmpty(port)
|
||||
then:
|
||||
|
||||
@@ -28,11 +28,16 @@ trait WireMockStubVerifier {
|
||||
|
||||
void stubMappingIsValidWireMockStub(String mappingDefinition) {
|
||||
StubMapping stubMapping = WireMockStubMapping.buildFrom(mappingDefinition)
|
||||
stubMapping.request.bodyPatterns.findAll { it.isPresent() && it instanceof RegexPattern }.every {
|
||||
stubMappingIsValidWireMockStub(stubMapping)
|
||||
}
|
||||
|
||||
void stubMappingIsValidWireMockStub(StubMapping mappingDefinition) {
|
||||
mappingDefinition.request.bodyPatterns.findAll { it.isPresent() && it instanceof RegexPattern }.every {
|
||||
Pattern.compile(it.getValue())
|
||||
}
|
||||
assert !mappingDefinition.contains('org.springframework.cloud.contract.spec.internal')
|
||||
assert !mappingDefinition.contains('cursor')
|
||||
String definition = mappingDefinition.toString()
|
||||
assert !definition.contains('org.springframework.cloud.contract.spec.internal')
|
||||
assert !definition.contains('cursor')
|
||||
}
|
||||
|
||||
void stubMappingIsValidWireMockStub(Contract contractDsl) {
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
*/
|
||||
|
||||
// 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;
|
||||
@@ -58,6 +61,16 @@ class contract_rest_with_tags implements Supplier<Collection<Contract>> {
|
||||
|
||||
// end::in_progress[]
|
||||
|
||||
static Object metadata = Collections.singletonList(
|
||||
// tag::metadata[]
|
||||
Contract.make(c -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("wiremock", "{ \"response\" : { \"fixedDelayMilliseconds\" : 2000 } }");
|
||||
c.metadata(map);
|
||||
}));
|
||||
|
||||
// end::metadata[]
|
||||
|
||||
@Override
|
||||
public Collection<Contract> get() {
|
||||
return Collections.singletonList(Contract.make(c -> {
|
||||
|
||||
Reference in New Issue
Block a user