diff --git a/docs/src/main/asciidoc/_project-features-stubrunner.adoc b/docs/src/main/asciidoc/_project-features-stubrunner.adoc index 5ecd29abf9..ec66a0a7d2 100644 --- a/docs/src/main/asciidoc/_project-features-stubrunner.adoc +++ b/docs/src/main/asciidoc/_project-features-stubrunner.adoc @@ -374,7 +374,7 @@ group and artifact ID, as the following example shows: ==== [source,java,indent=0] ---- -include::{stubrunner_core_path}/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleJUnitTest.java[tags=classrule] +include::{stubrunner_core_path}/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit4/StubRunnerRuleJUnitTest.java[tags=classrule] ---- ==== @@ -410,13 +410,13 @@ The following examples provide more detail about using Stub Runner: [source,groovy,indent=0,subs="verbatim,attributes",role="primary"] .Spock ---- -include::{stubrunner_core_path}/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleSpec.groovy[tags=classrule] +include::{stubrunner_core_path}/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit4/StubRunnerRuleSpec.groovy[tags=classrule] ---- [source,java,indent=0,subs="verbatim,attributes",role="secondary"] .Junit 4 ---- -include::{stubrunner_core_path}/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleJUnitTest.java[tags=test] +include::{stubrunner_core_path}/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit4/StubRunnerRuleJUnitTest.java[tags=test] ---- [source,java,indent=0,subs="verbatim,attributes",role="secondary"] @@ -456,7 +456,7 @@ and then pass the port for the last downloaded stub. The following example shows ==== [source,java,indent=0] ---- -include::{stubrunner_core_path}/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleCustomPortJUnitTest.java[tags=classrule_with_port] +include::{stubrunner_core_path}/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit4/StubRunnerRuleCustomPortJUnitTest.java[tags=classrule_with_port] ---- ==== @@ -465,7 +465,7 @@ For the preceding example, the following test is valid: ==== [source,java,indent=0] ---- -include::{stubrunner_core_path}/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit/StubRunnerRuleCustomPortJUnitTest.java[tags=test_with_port] +include::{stubrunner_core_path}/src/test/groovy/org/springframework/cloud/contract/stubrunner/junit4/StubRunnerRuleCustomPortJUnitTest.java[tags=test_with_port] ---- ==== diff --git a/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy b/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy new file mode 100644 index 0000000000..71e254a0bd --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy @@ -0,0 +1,283 @@ +/* + * 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.stubrunner.messaging.camel + +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import org.apache.activemq.spring.ActiveMQConnectionFactory +import org.apache.camel.CamelContext +import org.apache.camel.ConsumerTemplate +import org.apache.camel.Exchange +import org.apache.camel.ProducerTemplate +import org.apache.camel.component.activemq.ActiveMQComponent +import org.apache.camel.component.jms.JmsConfiguration +import org.apache.camel.impl.engine.DefaultShutdownStrategy +import spock.lang.Ignore +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.context.SpringBootContextLoader +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.stubrunner.StubFinder +import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration + +/** + * @author Marcin Grzejszczak + */ +@ContextConfiguration(classes = Config, loader = SpringBootContextLoader) +@SpringBootTest(properties = "debug=true") +@AutoConfigureStubRunner +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@Ignore +class CamelStubRunnerSpec extends Specification { + + //TODO: fix extremely slow test that causes pipeline to fail and then unignore + + @Autowired + StubFinder stubFinder + @Autowired + CamelContext camelContext + ConsumerTemplate consumerTemplate + ProducerTemplate producerTemplate + + def setup() { + consumerTemplate = camelContext.createConsumerTemplate() + producerTemplate = camelContext.createProducerTemplate() + } + + def cleanup() { + // ensure that message were taken from the queue + consumerTemplate.receive('jms:output', 100) + consumerTemplate.receive('jms:input', 100) + this.producerTemplate.stop() + this.consumerTemplate.stop() + def strategy = new DefaultShutdownStrategy(this.camelContext) + strategy.timeout = 1 + this.camelContext.shutdownStrategy = strategy + } + + def 'should not trigger a message that does not match input'() { + when: + producerTemplate. + sendBodyAndHeaders('jms:input', new BookReturned('notmatching'), [wrong: 'header_value']) + then: + Exchange receivedMessage = consumerTemplate.receive('jms:output', 100) + and: + receivedMessage == null + } + + def 'should download the stub and register a route for it'() { + when: + // tag::client_send[] + producerTemplate. + sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header']) + // end::client_send[] + then: + // tag::client_receive[] + Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000) + // end::client_receive[] + and: + // tag::client_receive_message[] + receivedMessage != null + assertThatBodyContainsBookNameFoo(receivedMessage.in.body) + receivedMessage.in.headers.get('BOOK-NAME') == 'foo' + // end::client_receive_message[] + } + + def 'should trigger a message by label'() { + when: + // tag::client_trigger[] + stubFinder.trigger('return_book_1') + // end::client_trigger[] + then: + // tag::client_trigger_receive[] + Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000) + // end::client_trigger_receive[] + and: + // tag::client_trigger_message[] + receivedMessage != null + assertThatBodyContainsBookNameFoo(receivedMessage.in.body) + receivedMessage.in.headers.get('BOOK-NAME') == 'foo' + // end::client_trigger_message[] + } + + def 'should trigger a label for the existing groupId:artifactId'() { + when: + // tag::trigger_group_artifact[] + stubFinder. + trigger('org.springframework.cloud.contract.verifier.stubs:camelService', 'return_book_1') + // end::trigger_group_artifact[] + then: + Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000) + and: + receivedMessage != null + assertThatBodyContainsBookNameFoo(receivedMessage.in.body) + receivedMessage.in.headers.get('BOOK-NAME') == 'foo' + } + + def 'should trigger a label for the existing artifactId'() { + when: + // tag::trigger_artifact[] + stubFinder.trigger('camelService', 'return_book_1') + // end::trigger_artifact[] + then: + Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000) + and: + receivedMessage != null + assertThatBodyContainsBookNameFoo(receivedMessage.in.body) + receivedMessage.in.headers.get('BOOK-NAME') == 'foo' + } + + def 'should throw an exception when missing label is passed'() { + when: + stubFinder.trigger('missing label') + then: + thrown(IllegalArgumentException) + } + + def 'should throw an exception when missing label and artifactid is passed'() { + when: + stubFinder.trigger('some:service', 'return_book_1') + then: + thrown(IllegalArgumentException) + } + + def 'should trigger messages by running all triggers'() { + when: + // tag::trigger_all[] + stubFinder.trigger() + // end::trigger_all[] + then: + Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000) + and: + receivedMessage != null + assertThatBodyContainsBookNameFoo(receivedMessage.in.body) + receivedMessage.in.headers.get('BOOK-NAME') == 'foo' + } + + def 'should trigger a label with no output message'() { + when: + // tag::trigger_no_output[] + producerTemplate. + sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header']) + // end::trigger_no_output[] + then: + noExceptionThrown() + } + + private boolean assertThatBodyContainsBookNameFoo(Object payload) { + String objectAsString = payload instanceof String ? payload : + JsonOutput.toJson(payload) + def json = new JsonSlurper().parseText(objectAsString) + return json.bookName == 'foo' + } + + @Configuration + @ComponentScan + @EnableAutoConfiguration + static class Config { + + @Bean + ActiveMQConnectionFactory activeMQConnectionFactory(@Value('${activemq.url:vm://localhost?broker.persistent=false}') String url) { + ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerURL: url) + try { + factory.trustAllPackages = true + } + catch (Throwable e) { + } + return factory + } + + @Bean + JmsConfiguration jmsConfiguration(ActiveMQConnectionFactory activeMQConnectionFactory) { + return new JmsConfiguration(connectionFactory: activeMQConnectionFactory) + } + + @Bean + ActiveMQComponent activeMQComponent(JmsConfiguration jmsConfiguration) { + return new ActiveMQComponent(configuration: jmsConfiguration) + } + } + + + Contract dsl = + // tag::sample_dsl[] + Contract.make { + label 'return_book_1' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('jms:output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } + } + // end::sample_dsl[] + + Contract dsl2 = + // tag::sample_dsl_2[] + Contract.make { + label 'return_book_2' + input { + messageFrom('jms:input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('jms:output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + // end::sample_dsl_2[] + + Contract dsl3 = + // tag::sample_dsl_3[] + Contract.make { + label 'delete_book' + input { + messageFrom('jms:delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + // end::sample_dsl_3[] +} diff --git a/tests/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy b/tests/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy new file mode 100644 index 0000000000..27a18223f4 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy @@ -0,0 +1,226 @@ +/* + * 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.stubrunner.messaging.stream + +import java.util.concurrent.TimeUnit + +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import spock.lang.Ignore +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.autoconfigure.ImportAutoConfiguration +import org.springframework.boot.test.context.SpringBootContextLoader +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.stubrunner.StubFinder +import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner +import org.springframework.cloud.contract.verifier.messaging.MessageVerifier +import org.springframework.cloud.contract.verifier.messaging.boot.AutoConfigureMessageVerifier +import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.Message +import org.springframework.test.context.ContextConfiguration + +/** + * @author Marcin Grzejszczak + */ +@ContextConfiguration(classes = Config, loader = SpringBootContextLoader) +@SpringBootTest(properties = "debug=true") +@AutoConfigureStubRunner +@AutoConfigureMessageVerifier +//@IgnoreIf({ os.windows }) +@Ignore("Wait until the feature of runtime message sending and polling is available") +class StreamStubRunnerSpec extends Specification { + + @Autowired + StubFinder stubFinder + @Autowired + MessageVerifier> messaging + + def 'should download the stub and register a route for it'() { + when: + // tag::client_send[] + messaging.send(new BookReturned('foo'), [sample: 'header'], 'bookStorage') + // end::client_send[] + then: + // tag::client_receive[] + Message receivedMessage = messaging.receive('returnBook') + // end::client_receive[] + and: + // tag::client_receive_message[] + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + // end::client_receive_message[] + } + + def 'should trigger a message by label'() { + when: + // tag::client_trigger[] + stubFinder.trigger('return_book_1') + // end::client_trigger[] + then: + // tag::client_trigger_receive[] + Message receivedMessage = messaging.receive('returnBook') + // end::client_trigger_receive[] + and: + // tag::client_trigger_message[] + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + // end::client_trigger_message[] + } + + def 'should trigger a label for the existing groupId:artifactId'() { + when: + // tag::trigger_group_artifact[] + stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:streamService', 'return_book_1') + // end::trigger_group_artifact[] + then: + Message receivedMessage = messaging.receive('returnBook') + and: + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + } + + def 'should trigger a label for the existing artifactId'() { + when: + // tag::trigger_artifact[] + stubFinder.trigger('streamService', 'return_book_1') + // end::trigger_artifact[] + then: + Message receivedMessage = messaging.receive('returnBook') + and: + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + } + + def 'should throw exception when missing label is passed'() { + when: + stubFinder.trigger('missing label') + then: + thrown(IllegalArgumentException) + } + + def 'should throw exception when missing label and artifactid is passed'() { + when: + stubFinder.trigger('some:service', 'return_book_1') + then: + thrown(IllegalArgumentException) + } + + def 'should trigger messages by running all triggers'() { + when: + // tag::trigger_all[] + stubFinder.trigger() + // end::trigger_all[] + then: + Message receivedMessage = messaging.receive('returnBook') + and: + receivedMessage != null + assertJsons(receivedMessage.payload) + receivedMessage.headers.get('BOOK-NAME') == 'foo' + } + + def 'should trigger a label with no output message'() { + when: + // tag::trigger_no_output[] + messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete') + // end::trigger_no_output[] + then: + noExceptionThrown() + } + + def 'should not trigger a message that does not match input'() { + when: + messaging.send(new BookReturned('not_matching'), [wrong: 'header_value'], 'bookStorage') + then: + Message receivedMessage = messaging.receive('returnBook', 100, TimeUnit.MILLISECONDS) + and: + receivedMessage == null + } + + private boolean assertJsons(Object payload) { + String objectAsString = payload instanceof String ? payload : + payload instanceof byte[] ? new String(payload) + : JsonOutput.toJson(payload) + def json = new JsonSlurper().parseText(objectAsString) + return json.bookName == 'foo' + } + + Contract dsl = + // tag::sample_dsl[] + Contract.make { + label 'return_book_1' + input { triggeredBy('bookReturnedTriggered()') } + outputMessage { + sentTo('returnBook') + body('''{ "bookName" : "foo" }''') + headers { header('BOOK-NAME', 'foo') } + } + } + // end::sample_dsl[] + + Contract dsl2 = + // tag::sample_dsl_2[] + Contract.make { + label 'return_book_2' + input { + messageFrom('bookStorage') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { header('sample', 'header') } + } + outputMessage { + sentTo('returnBook') + body([ + bookName: 'foo' + ]) + headers { header('BOOK-NAME', 'foo') } + } + } + // end::sample_dsl_2[] + + Contract dsl3 = + // tag::sample_dsl_3[] + Contract.make { + label 'delete_book' + input { + messageFrom('delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { header('sample', 'header') } + assertThat('bookWasDeleted()') + } + } + // end::sample_dsl_3[] + + @ImportAutoConfiguration(TestChannelBinderConfiguration.class) + @Configuration + @EnableAutoConfiguration + protected static class Config { + + } + +} \ No newline at end of file