diff --git a/docs/src/main/asciidoc/_attributes.adoc b/docs/src/main/asciidoc/_attributes.adoc index 1a246251b8..925ac63727 100644 --- a/docs/src/main/asciidoc/_attributes.adoc +++ b/docs/src/main/asciidoc/_attributes.adoc @@ -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 diff --git a/docs/src/main/asciidoc/_project-features-contract.adoc b/docs/src/main/asciidoc/_project-features-contract.adoc index 474f1880d2..dce7260c16 100644 --- a/docs/src/main/asciidoc/_project-features-contract.adoc +++ b/docs/src/main/asciidoc/_project-features-contract.adoc @@ -193,6 +193,7 @@ The following sections describe the most common top-level elements: * <> * <> * <> +* <> [[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 diff --git a/docs/src/main/asciidoc/advanced.adoc b/docs/src/main/asciidoc/advanced.adoc index 0c148c5c2a..ddc08a349f 100644 --- a/docs/src/main/asciidoc/advanced.adoc +++ b/docs/src/main/asciidoc/advanced.adoc @@ -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 diff --git a/docs/src/test/resources/contract.yml b/docs/src/test/resources/contract.yml index 5322f630b8..71a3101bd3 100644 --- a/docs/src/test/resources/contract.yml +++ b/docs/src/test/resources/contract.yml @@ -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" + } + } diff --git a/samples/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java b/samples/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java index cb21cbf199..ad400366fb 100644 --- a/samples/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java +++ b/samples/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java @@ -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 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 = 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 = 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 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; + } } diff --git a/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java b/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java index 08e43be336..514cce9909 100644 --- a/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java +++ b/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java @@ -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 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"); + } + +} \ No newline at end of file diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldReturnFraudStats.groovy b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldReturnFraudStats.groovy index 0e09a6f879..8c72e966a9 100644 --- a/samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldReturnFraudStats.groovy +++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldReturnFraudStats.groovy @@ -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[] ] diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/yml/fraud/shouldReturnFraudStats.yml b/samples/standalone/dsl/http-server/src/test/resources/contracts/yml/fraud/shouldReturnFraudStats.yml index 6502368fd1..0dfd76ecd5 100644 --- a/samples/standalone/dsl/http-server/src/test/resources/contracts/yml/fraud/shouldReturnFraudStats.yml +++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/yml/fraud/shouldReturnFraudStats.yml @@ -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 diff --git a/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/Contract.java b/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/Contract.java index c53fd32beb..2d59f51de8 100644 --- a/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/Contract.java +++ b/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/Contract.java @@ -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 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 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 metadata) { + this.metadata = metadata; + } + + public Map 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 diff --git a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/ContractDsl.kt b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/ContractDsl.kt index d424bfc2af..4315f902aa 100644 --- a/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/ContractDsl.kt +++ b/specs/spring-cloud-contract-spec-kotlin/src/main/kotlin/org/springframework/cloud/contract/spec/ContractDsl.kt @@ -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 = 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) { + metadata = metadata.plus(map) + } + + /** + * The metadata. + * @param metadata metadata to set + */ + fun metadata(vararg metadata: Pair) = 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 + } } diff --git a/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/ContractTests.kt b/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/ContractTests.kt index 5f153fa182..0f1eb46c25 100644 --- a/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/ContractTests.kt +++ b/specs/spring-cloud-contract-spec-kotlin/src/test/kotlin/org/springframework/cloud/contract/spec/ContractTests.kt @@ -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 { diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java index 17f109a67b..292db7c7da 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java @@ -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; diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGenerator.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGenerator.java index 7602743943..16ee89a7f4 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGenerator.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGenerator.java @@ -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 - type of stub mapping * @since 1.1.0 */ -public interface StubGenerator { +public interface StubGenerator { /** * @param fileName - file name @@ -45,6 +48,35 @@ public interface StubGenerator { */ Map 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 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 diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubPostProcessor.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubPostProcessor.java new file mode 100644 index 0000000000..97c34a1487 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubPostProcessor.java @@ -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 type of stub mapping + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +public interface StubPostProcessor { + + /** + * List of registered stub post processors. + */ + List 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); + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DefaultWireMockStubPostProcessor.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DefaultWireMockStubPostProcessor.java new file mode 100644 index 0000000000..706b2dc6a3 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DefaultWireMockStubPostProcessor.java @@ -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 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 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 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())); + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverter.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverter.java index 15c853d851..ad74ac6b34 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverter.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverter.java @@ -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 diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.java index 1fc0b9ea26..d03c814569 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.java @@ -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 { @Override public String generateOutputFileNameForInput(String inputFileName) { diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/WireMockStubPostProcessor.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/WireMockStubPostProcessor.java new file mode 100644 index 0000000000..0bd63abf26 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/WireMockStubPostProcessor.java @@ -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 { + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/java/org/springframework/cloud/contract/verifier/wiremock/DefaultWireMockStubPostProcessorTests.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/java/org/springframework/cloud/contract/verifier/wiremock/DefaultWireMockStubPostProcessorTests.java new file mode 100644 index 0000000000..dfa2e49dd3 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/java/org/springframework/cloud/contract/verifier/wiremock/DefaultWireMockStubPostProcessorTests.java @@ -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"); + } + +} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy index 24bc67c4d2..a9a06d7300 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy @@ -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) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.java index da8c6641fb..5bc4962e47 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.java @@ -54,6 +54,8 @@ public class YamlContract { public boolean inProgress; + public Map metadata; + public static class Request { public String method; diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy index eecd1470e5..e2201fd358 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy @@ -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) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubStrategy.groovy index c47f308e49..1a3228fd8c 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubStrategy.groovy @@ -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 } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy index e93b7be4b6..fde7728c63 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy @@ -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 strings = converter.store([ diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockGroovyDslSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockGroovyDslSpec.groovy index 130a78067b..95d08fa9e2 100755 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockGroovyDslSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockGroovyDslSpec.groovy @@ -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 entity = callWithOptionalAndEmpty(port) then: diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubVerifier.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubVerifier.groovy index 9dd35f4f51..bdf4785900 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubVerifier.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockStubVerifier.groovy @@ -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) { diff --git a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_with_tags.java b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_with_tags.java index 027846c894..e32b0b17ce 100644 --- a/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_with_tags.java +++ b/spring-cloud-contract-verifier/src/test/resources/contractsToCompile/contract_rest_with_tags.java @@ -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> { // end::in_progress[] + static Object metadata = Collections.singletonList( + // tag::metadata[] + Contract.make(c -> { + Map map = new HashMap<>(); + map.put("wiremock", "{ \"response\" : { \"fixedDelayMilliseconds\" : 2000 } }"); + c.metadata(map); + })); + + // end::metadata[] + @Override public Collection get() { return Collections.singletonList(Contract.make(c -> {