diff --git a/README.adoc b/README.adoc index b9b903aa3e..915c988e57 100644 --- a/README.adoc +++ b/README.adoc @@ -709,7 +709,7 @@ org.springframework.cloud.contract.spec.Contract.make { method 'PUT' // (2) url '/fraudcheck' // (3) body([ // (4) - clientId: $(regex('[0-9]{10}')), + "client.id": $(regex('[0-9]{10}')), loanAmount: 99999 ]) headers { // (5) @@ -720,7 +720,7 @@ org.springframework.cloud.contract.spec.Contract.make { status 200 // (7) body([ // (8) fraudCheckStatus: "FRAUD", - rejectionReason: "Amount too high" + "rejection.reason": "Amount too high" ]) headers { // (9) contentType('application/vnd.fraud.v1+json') diff --git a/spring-cloud-contract-stub-runner/README.adoc b/spring-cloud-contract-stub-runner/README.adoc index 3daf8e3903..912a71101d 100644 --- a/spring-cloud-contract-stub-runner/README.adoc +++ b/spring-cloud-contract-stub-runner/README.adoc @@ -100,6 +100,7 @@ After that rule gets executed Stub Runner connects to your Maven repository and - unzip them to a temporary folder - start a WireMock server for each Maven dependency on a random port from the provided range of ports / provided port - feed the WireMock server with all JSON files that are valid WireMock definitions +- can also send messages (remember to pass an implementation of `MessageVerifier` interface) Stub Runner uses https://wiki.eclipse.org/Aether[Eclipse Aether] mechanism to download the Maven dependencies. Check their https://wiki.eclipse.org/Aether[docs] for more information. @@ -127,6 +128,10 @@ include::src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/Stu Check the *Common properties for JUnit and Spring* for more information on how to apply global configuration of Stub Runner. +IMPORTANT: To use the JUnit rule together with messaging you have to provide an implementation of the +`MessageVerifier` interface to the rule builder (e.g. `rule.messageVerifier(new MyMessageVerifier())`). +If you don't do this then whenever you try to send a message an exception will be thrown. + ==== Maven settings The stub downloader honors Maven settings for a different local repository folder. diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.java index dffc2f7d4a..bab75e66d3 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.java @@ -32,7 +32,11 @@ public class BatchStubRunnerFactory { private final MessageVerifier contractVerifierMessaging; public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions) { - this(stubRunnerOptions, new AetherStubDownloader(stubRunnerOptions), new NoOpStubMessages()); + this(stubRunnerOptions, new NoOpStubMessages()); + } + + public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, MessageVerifier verifier) { + this(stubRunnerOptions, new AetherStubDownloader(stubRunnerOptions), verifier); } public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, StubDownloader stubDownloader) { 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 6c58621e11..1eb0c80ea7 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 @@ -16,6 +16,8 @@ package org.springframework.cloud.contract.stubrunner; +import groovy.json.JsonOutput; + import java.net.URL; import java.util.ArrayList; import java.util.Collection; @@ -36,8 +38,6 @@ import org.springframework.cloud.contract.verifier.messaging.MessageVerifier; import org.springframework.cloud.contract.verifier.messaging.noop.NoOpStubMessages; import org.springframework.cloud.contract.verifier.util.BodyExtractor; -import groovy.json.JsonOutput; - /** * Runs stubs for a particular {@link StubServer} */ @@ -165,7 +165,7 @@ class StubRunnerExecutor implements StubFinder { private boolean triggerForDsls(Collection dsls, String labelName) { Collection matchingDsls = new ArrayList<>(); for (Contract contract : dsls) { - if (labelName.equals(contract.getLabel())) { + if (labelName.equals(contract.getLabel()) && contract.getOutputMessage() != null) { matchingDsls.add(contract); } } @@ -173,7 +173,7 @@ class StubRunnerExecutor implements StubFinder { return false; } for (Contract contract : matchingDsls) { - sendMessageIfApplicable(contract); + sendMessage(contract); } return true; } @@ -182,10 +182,17 @@ class StubRunnerExecutor implements StubFinder { public boolean trigger() { Collection matchingContracts = new ArrayList<>(); for (Collection it : getContracts().values()) { - matchingContracts.addAll(it); + for (Contract contract : it) { + if (contract.getOutputMessage() != null) { + matchingContracts.add(contract); + } + } + } + if (matchingContracts.isEmpty()) { + return false; } for (Contract contract : matchingContracts) { - sendMessageIfApplicable(contract); + sendMessage(contract); } return true; } @@ -205,11 +212,8 @@ class StubRunnerExecutor implements StubFinder { return labels; } - private void sendMessageIfApplicable(Contract groovyDsl) { + private void sendMessage(Contract groovyDsl) { OutputMessage outputMessage = groovyDsl.getOutputMessage(); - if (outputMessage == null) { - return; - } DslProperty body = outputMessage.getBody(); Headers headers = outputMessage.getHeaders(); this.contractVerifierMessaging.send( diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java index 41746bea8e..bd7986bee7 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRule.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.junit.rules.TestRule; import org.junit.runner.Description; @@ -33,6 +34,7 @@ import org.springframework.cloud.contract.stubrunner.StubConfiguration; import org.springframework.cloud.contract.stubrunner.StubFinder; import org.springframework.cloud.contract.stubrunner.StubRunnerOptions; import org.springframework.cloud.contract.stubrunner.StubRunnerOptionsBuilder; +import org.springframework.cloud.contract.verifier.messaging.MessageVerifier; /** * JUnit class rule that allows you to download the provided stubs. @@ -45,6 +47,7 @@ public class StubRunnerRule implements TestRule, StubFinder { private final StubRunnerOptionsBuilder stubRunnerOptionsBuilder = new StubRunnerOptionsBuilder(defaultStubRunnerOptions()); private BatchStubRunner stubFinder; + private MessageVerifier verifier = new ExceptionThrowingMessageVerifier(); @Override public Statement apply(final Statement base, Description description) { @@ -58,7 +61,8 @@ public class StubRunnerRule implements TestRule, StubFinder { private void before() { StubRunnerRule.this.stubFinder = new BatchStubRunnerFactory( - StubRunnerRule.this.stubRunnerOptionsBuilder.build()).buildBatchStubRunner(); + StubRunnerRule.this.stubRunnerOptionsBuilder.build(), + StubRunnerRule.this.verifier).buildBatchStubRunner(); StubRunnerRule.this.stubFinder.runStubs(); } }; @@ -81,6 +85,16 @@ public class StubRunnerRule implements TestRule, StubFinder { return builder.build(); } + /** + * Pass the {@link MessageVerifier} that this rule should use. + * If you don't pass anything a {@link ExceptionThrowingMessageVerifier} will be used. + * That means that an exception will be thrown whenever you try to do sth messaging related. + */ + public StubRunnerRule messageVerifier(MessageVerifier messageVerifier) { + this.verifier = messageVerifier; + return this; + } + /** * Override all options * @@ -209,17 +223,29 @@ public class StubRunnerRule implements TestRule, StubFinder { @Override public boolean trigger(String ivyNotation, String labelName) { - return this.stubFinder.trigger(ivyNotation, labelName); + boolean result = this.stubFinder.trigger(ivyNotation, labelName); + if (!result) { + throw new IllegalStateException("Failed to trigger a message with notation [" + ivyNotation + "] and label [" + labelName + "]"); + } + return result; } @Override public boolean trigger(String labelName) { - return this.stubFinder.trigger(labelName); + boolean result = this.stubFinder.trigger(labelName); + if (!result) { + throw new IllegalStateException("Failed to trigger a message with label [" + labelName + "]"); + } + return result; } @Override public boolean trigger() { - return this.stubFinder.trigger(); + boolean result = this.stubFinder.trigger(); + if (!result) { + throw new IllegalStateException("Failed to trigger a message"); + } + return result; } @Override @@ -227,4 +253,27 @@ public class StubRunnerRule implements TestRule, StubFinder { return this.stubFinder.labels(); } + + static class ExceptionThrowingMessageVerifier implements MessageVerifier { + + private static final String EXCEPTION_MESSAGE = "Please provide a custom MessageVerifier to use this feature"; + + @Override public void send(Object message, String destination) { + throw new UnsupportedOperationException(EXCEPTION_MESSAGE); + } + + @Override public Object receive(String destination, long timeout, + TimeUnit timeUnit) { + throw new UnsupportedOperationException(EXCEPTION_MESSAGE); + } + + @Override public Object receive(String destination) { + throw new UnsupportedOperationException(EXCEPTION_MESSAGE); + } + + @Override public void send(Object payload, Map headers, String destination) { + throw new UnsupportedOperationException(EXCEPTION_MESSAGE); + } + } + } diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutorSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutorSpec.groovy index 75b97eea90..4ca3d6c9ed 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutorSpec.groovy +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerExecutorSpec.groovy @@ -116,6 +116,21 @@ class StubRunnerExecutorSpec extends Specification { executor.shutdown() } + def 'should return false if no messages are found'() { + given: + def stubConf = new StubConfiguration('asd', 'asd', 'asd', '') + StubRunnerExecutor executor = new StubRunnerExecutor(portScanner) + when: + executor.runStubs(stubRunnerOptions, + new StubRepository(new File('src/test/resources/repository/httpcontract')), stubConf) + then: + !executor.trigger() + !executor.trigger("missing", "label") + !executor.trigger("label") + cleanup: + executor.shutdown() + } + Map stubIdsWithPortsFromString(String stubIdsToPortMapping) { return stubIdsToPortMapping.split(',').collectEntries { String entry -> return StubsParser.fromStringWithPort(entry) diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleCustomMsgVerifierSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleCustomMsgVerifierSpec.groovy new file mode 100644 index 0000000000..3a65d3dcdc --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleCustomMsgVerifierSpec.groovy @@ -0,0 +1,85 @@ +/* + * Copyright 2013-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.junit + +import org.junit.AfterClass +import org.junit.BeforeClass +import org.junit.ClassRule +import org.springframework.cloud.contract.verifier.messaging.MessageVerifier +import spock.lang.Shared +import spock.lang.Specification + +import java.util.concurrent.TimeUnit + +/** + * @author Marcin Grzejszczak + */ +class StubRunnerRuleCustomMsgVerifierSpec extends Specification { + + @BeforeClass + @AfterClass + void setupProps() { + System.clearProperty("stubrunner.repository.root") + System.clearProperty("stubrunner.classifier") + } + + @ClassRule @Shared StubRunnerRule rule = new StubRunnerRule() + .repoRoot(StubRunnerRuleCustomMsgVerifierSpec.getResource("/m2repo/repository").toURI().toString()) + .downloadStub("org.springframework.cloud.contract.verifier.stubs", "bootService") + .messageVerifier(new MyMessageVerifier()) + + def 'should use the provided message verifier in the junit rule'() { + when: + rule.trigger() + then: + IllegalStateException e = thrown(IllegalStateException) + e.message.contains("Failed to send a message with headers") + when: + rule.trigger("return_book_1") + then: + e = thrown(IllegalStateException) + e.message.contains("Failed to send a message with headers") + when: + rule.trigger("bootService", "return_book_1") + then: + e = thrown(IllegalStateException) + e.message.contains("Failed to send a message with headers") + } + + static class MyMessageVerifier implements MessageVerifier { + + @Override + void send(Object message, String destination) { + throw new IllegalStateException("Failed to send a message") + } + + @Override + Object receive(String destination, long timeout, TimeUnit timeUnit) { + throw new IllegalStateException("Failed to receive a message with timeout") + } + + @Override + Object receive(String destination) { + throw new IllegalStateException("Failed to receive a message") + } + + @Override + void send(Object payload, Map headers, String destination) { + throw new IllegalStateException("Failed to send a message with headers") + } + } +} diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleExceptionThrowingSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleExceptionThrowingSpec.groovy new file mode 100644 index 0000000000..ed99713d1f --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleExceptionThrowingSpec.groovy @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.stubrunner.junit + +import org.junit.AfterClass +import org.junit.BeforeClass +import org.junit.ClassRule +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +class StubRunnerRuleExceptionThrowingSpec extends Specification { + + @BeforeClass + @AfterClass + void setupProps() { + System.clearProperty("stubrunner.repository.root") + System.clearProperty("stubrunner.classifier") + } + + @ClassRule @Shared StubRunnerRule rule = new StubRunnerRule() + .repoRoot(StubRunnerRuleExceptionThrowingSpec.getResource("/m2repo/repository").toURI().toString()) + .downloadStub("org.springframework.cloud.contract.verifier.stubs", "bootService") + + def 'should throw exception when no message verifier was passed and message related method was triggered'() { + when: + rule.trigger() + then: + thrown(UnsupportedOperationException) + when: + rule.trigger("return_book_1") + then: + thrown(UnsupportedOperationException) + when: + rule.trigger("bootService", "return_book_1") + then: + thrown(UnsupportedOperationException) + } +} diff --git a/spring-cloud-contract-stub-runner/src/test/resources/repository/httpcontract/contract1.groovy b/spring-cloud-contract-stub-runner/src/test/resources/repository/httpcontract/contract1.groovy new file mode 100644 index 0000000000..80e123ae4d --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/resources/repository/httpcontract/contract1.groovy @@ -0,0 +1,27 @@ +org.springframework.cloud.contract.spec.Contract.make { + request { + method """PUT""" + url """/fraudcheck""" + body(""" + { + "clientPesel":"${value(consumer(regex('[0-9]{10}')), producer('1234567890'))}", + "loanAmount":99999} + """ + ) + headers { + contentType("application/vnd.fraud.v1+json") + } + + } + response { + status 200 + body( """{ + "fraudCheckStatus": "${value(c('FRAUD'), p(regex('[A-Z]{5}')))}", + "rejectionReason": "Amount too high" +}""") + headers { + contentType("application/vnd.fraud.v1+json") + } + } + +} \ No newline at end of file