diff --git a/docs/src/main/asciidoc/migrations.adoc b/docs/src/main/asciidoc/migrations.adoc index a93194bee9..a4ef065a02 100644 --- a/docs/src/main/asciidoc/migrations.adoc +++ b/docs/src/main/asciidoc/migrations.adoc @@ -177,9 +177,4 @@ version of the release train (and vice versa). Done via https://github.com/spring-cloud/spring-cloud-contract/issues/267[issue 267] [[cloud-verifier-1.2-2.0]] -=== 1.2.x -> 2.0.x - -==== No Camel support - -We will add back Apache Camel support only after this https://issues.apache.org/jira/browse/CAMEL-11430[issue] -gets fixed \ No newline at end of file +=== 1.2.x -> 2.0.x \ No newline at end of file diff --git a/docs/src/main/asciidoc/verifier_stubrunner_msg.adoc b/docs/src/main/asciidoc/verifier_stubrunner_msg.adoc index 725c4e8db9..1a5a68a96f 100644 --- a/docs/src/main/asciidoc/verifier_stubrunner_msg.adoc +++ b/docs/src/main/asciidoc/verifier_stubrunner_msg.adoc @@ -6,6 +6,7 @@ frameworks: * Spring Integration * Spring Cloud Stream +* Apache Camel * Spring AMQP It also provides entry points to integrate with any other solution on the market. @@ -65,6 +66,8 @@ include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/o include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=trigger_all,indent=0] ---- +include::{tests_path}/spring-cloud-contract-stub-runner-camel/README.adoc[] + include::{tests_path}/spring-cloud-contract-stub-runner-integration/README.adoc[] include::{tests_path}/spring-cloud-contract-stub-runner-stream/README.adoc[] diff --git a/pom.xml b/pom.xml index 2f524287f6..b129177de3 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,9 @@ 2016 + 5.12.1 + 2.22.1 + 2.17 3.5.13 0.0.9 2.1.0.BUILD-SNAPSHOT @@ -71,6 +74,51 @@ pom import + + org.apache.camel + camel-spring-boot-starter + ${camel.version} + + + org.apache.camel + camel-spring + ${camel.version} + + + org.apache.camel + camel-spring-boot + ${camel.version} + + + org.apache.camel + camel-core + ${camel.version} + + + org.apache.camel + camel-kafka + ${camel.version} + + + org.apache.camel + camel-jackson + ${camel.version} + + + org.apache.camel + camel-jms + ${camel.version} + + + org.apache.activemq + activemq-camel + ${activemq.version} + + + org.apache.activemq + activemq-pool + ${activemq.version} + net.sf.jopt-simple jopt-simple diff --git a/spring-cloud-contract-stub-runner/pom.xml b/spring-cloud-contract-stub-runner/pom.xml index 6bf2b2fcdc..8c578900a6 100644 --- a/spring-cloud-contract-stub-runner/pom.xml +++ b/spring-cloud-contract-stub-runner/pom.xml @@ -60,6 +60,21 @@ jopt-simple true + + org.apache.camel + camel-spring-boot-starter + true + + + org.apache.camel + camel-spring-boot + true + + + org.apache.camel + camel-jackson + true + junit junit diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelConfiguration.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelConfiguration.java new file mode 100644 index 0000000000..f5488d4eeb --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelConfiguration.java @@ -0,0 +1,66 @@ +/* + * 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.messaging.camel; + +import org.apache.camel.RoutesBuilder; +import org.apache.camel.spring.SpringRouteBuilder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.stubrunner.BatchStubRunner; +import org.springframework.cloud.contract.stubrunner.StubConfiguration; +import org.springframework.context.annotation.*; + +import java.util.Collection; +import java.util.Map; + +/** + * Camel configuration that iterates over the downloaded Groovy DSLs and registers a route + * for each DSL. + * + * @author Marcin Grzejszczak + */ +@Configuration +@ConditionalOnClass(RoutesBuilder.class) +@ConditionalOnProperty(name = "stubrunner.camel.enabled", havingValue = "true", matchIfMissing = true) +public class StubRunnerCamelConfiguration { + + @Bean + public RoutesBuilder myRouter(final BatchStubRunner batchStubRunner) { + return new SpringRouteBuilder() { + @Override + public void configure() throws Exception { + Map> contracts = batchStubRunner + .getContracts(); + for (Collection list : contracts.values()) { + for (Contract it : list) { + if (it.getInput() != null + && it.getInput().getMessageFrom() != null + && it.getOutputMessage() != null + && it.getOutputMessage().getSentTo() != null) { + from(it.getInput().getMessageFrom().getClientValue()) + .filter(new StubRunnerCamelPredicate(it)) + .process(new StubRunnerCamelProcessor(it)) + .to(it.getOutputMessage().getSentTo() + .getClientValue()); + } + } + } + } + }; + } +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java new file mode 100644 index 0000000000..f62a75e4ae --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java @@ -0,0 +1,126 @@ +/* + * 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.messaging.camel; + +import java.util.Map; +import java.util.regex.Pattern; + +import org.apache.camel.Exchange; +import org.apache.camel.Predicate; +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.BodyMatcher; +import org.springframework.cloud.contract.spec.internal.BodyMatchers; +import org.springframework.cloud.contract.spec.internal.Header; +import org.springframework.cloud.contract.verifier.util.MapConverter; +import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper; +import org.springframework.cloud.contract.verifier.util.JsonPaths; +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter; +import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import com.toomuchcoding.jsonassert.JsonAssertion; + +/** + * Passes through a message that matches the one defined in the DSL + * + * @author Marcin Grzejszczak + */ +class StubRunnerCamelPredicate implements Predicate { + + private final Contract groovyDsl; + private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper(); + + public StubRunnerCamelPredicate(Contract groovyDsl) { + this.groovyDsl = groovyDsl; + } + + @Override + public boolean matches(Exchange exchange) { + if (!headersMatch(exchange.getIn().getHeaders())) { + return false; + } + Object inputMessage = exchange.getIn().getBody(); + BodyMatchers matchers = this.groovyDsl.getInput().getBodyMatchers(); + Object dslBody = MapConverter.getStubSideValues(this.groovyDsl.getInput().getMessageBody()); + + DocumentContext parsedJson = deserialize(inputMessage); + return matchMessage(matchers, dslBody, parsedJson); + } + + private boolean matchMessage(BodyMatchers matchers, Object dslBody, DocumentContext parsedJson) { + boolean matches = true; + JsonPaths jsonPaths = getJsonPaths(matchers, dslBody); + for (MethodBufferingJsonVerifiable path : jsonPaths) { + matches &= matchesJsonPath(parsedJson, path.jsonPath()); + } + if (matchers != null && matchers.hasMatchers()) { + for (BodyMatcher matcher : matchers.jsonPathMatchers()) { + String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody); + matches &= matchesJsonPath(parsedJson, jsonPath); + } + } + return matches; + } + + private JsonPaths getJsonPaths(BodyMatchers matchers, Object dslBody) { + Object matchingInputMessage = JsonToJsonPathsConverter + .removeMatchingJsonPaths(dslBody, matchers); + return JsonToJsonPathsConverter + .transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck( + matchingInputMessage); + } + + private DocumentContext deserialize(Object inputMessage) { + DocumentContext parsedJson; + try { + parsedJson = JsonPath + .parse(this.objectMapper.writeValueAsString(inputMessage)); + } + catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot serialize to JSON", e); + } + return parsedJson; + } + + private boolean matchesJsonPath(DocumentContext parsedJson, String jsonPath) { + try { + JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean headersMatch(Map headers) { + boolean matches = true; + for (Header it : this.groovyDsl.getInput().getMessageHeaders().getEntries()) { + String name = it.getName(); + Object value = it.getClientValue(); + Object valueInHeader = headers.get(name); + matches &= matchValue(value, valueInHeader); + } + return matches; + } + + private boolean matchValue(Object value, Object valueInHeader) { + return value instanceof Pattern ? + ((Pattern) value).matcher(valueInHeader.toString()).matches() : + valueInHeader!=null && valueInHeader.equals(value); + } +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessor.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessor.java new file mode 100644 index 0000000000..e733c5a817 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessor.java @@ -0,0 +1,59 @@ +/* + * 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.messaging.camel; + +import org.springframework.cloud.contract.verifier.util.BodyExtractor; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.Processor; +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.Header; + +/** + * Sends forward a message defined in the DSL. Also removes headers from the input message + * and provides the headers from the DSL. + * + * @author Marcin Grzejszczak + */ +class StubRunnerCamelProcessor implements Processor { + + private final Contract groovyDsl; + + StubRunnerCamelProcessor(Contract groovyDsl) { + this.groovyDsl = groovyDsl; + } + + @Override + public void process(Exchange exchange) throws Exception { + Message input = exchange.getIn(); + if (this.groovyDsl.getInput().getMessageHeaders() != null) { + for (Header entry : this.groovyDsl.getInput().getMessageHeaders().getEntries()) { + input.removeHeader(entry.getName()); + } + } + if (this.groovyDsl.getOutputMessage() == null) { + return; + } + input.setBody(BodyExtractor + .extractStubValueFrom(this.groovyDsl.getOutputMessage().getBody())); + if (this.groovyDsl.getOutputMessage().getHeaders() != null) { + for (Header entry : this.groovyDsl.getOutputMessage().getHeaders().getEntries()) { + input.setHeader(entry.getName(), entry.getClientValue()); + } + } + } +} diff --git a/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories index 0f3c598395..f8e3bc096f 100644 --- a/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories @@ -8,7 +8,8 @@ org.springframework.cloud.contract.stubrunner.messaging.stream.StubRunnerStreamC org.springframework.cloud.contract.stubrunner.spring.cloud.zookeeper.StubRunnerSpringCloudZookeeperAutoConfiguration,\ org.springframework.cloud.contract.stubrunner.spring.cloud.eureka.StubRunnerSpringCloudEurekaAutoConfiguration,\ org.springframework.cloud.contract.stubrunner.spring.cloud.consul.StubRunnerSpringCloudConsulAutoConfiguration,\ -org.springframework.cloud.contract.stubrunner.messaging.StubRunnerStreamsIntegrationAutoConfiguration +org.springframework.cloud.contract.stubrunner.messaging.StubRunnerStreamsIntegrationAutoConfiguration,\ +org.springframework.cloud.contract.stubrunner.messaging.camel.StubRunnerCamelConfiguration # Test Execution Listeners org.springframework.test.context.TestExecutionListener=\ diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicateSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicateSpec.groovy new file mode 100644 index 0000000000..ba71c68044 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicateSpec.groovy @@ -0,0 +1,137 @@ +package org.springframework.cloud.contract.stubrunner.messaging.camel + +import org.apache.camel.Exchange +import org.apache.camel.Message +import org.springframework.cloud.contract.spec.Contract +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +class StubRunnerCamelPredicateSpec extends Specification { + Exchange exchange = Stub(Exchange) + Message message = Stub(Message) + + def "should return false if headers don't match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageBody(foo: "bar") + messageHeaders { + header("foo", $(c(regex("[0-9]{3}")), p(123))) + } + } + } + and: + StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate(dsl) + exchange.in >> message + message.headers >> [ + foo: "non matching stuff" + ] + expect: + !predicate.matches(exchange) + } + + def "should return false if headers match and body doesn't"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: $(c(regex("[0-9]{3}")), p(123))) + } + } + and: + StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate(dsl) + exchange.in >> message + message.headers >> [ + foo: 123 + ] + message.body >> [ + foo: "non matching stuff" + ] + expect: + !predicate.matches(exchange) + } + + def "should return false if headers match and body doesn't when it's using matchers"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: "non matching stuff") + stubMatchers { + jsonPath('$.foo', byRegex("[0-9]{3}")) + } + } + } + and: + StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate(dsl) + exchange.in >> message + message.headers >> [ + foo: 123 + ] + message.body >> [ + foo: "non matching stuff" + ] + expect: + !predicate.matches(exchange) + } + def "should return true if headers and body match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: $(c(regex("[0-9]{3}")), p(123))) + + } + } + and: + StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate(dsl) + exchange.in >> message + message.headers >> [ + foo: 123 + ] + message.body >> [ + foo: 123 + ] + expect: + predicate.matches(exchange) + + } + def "should return true if headers and body using matchers match"() { + given: + Contract dsl = Contract.make { + input { + messageFrom "foo" + messageHeaders { + header("foo", 123) + } + messageBody(foo: 123) + stubMatchers { + jsonPath('$.foo', byRegex("[0-9]{3}")) + } + } + } + and: + StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate(dsl) + exchange.in >> message + message.headers >> [ + foo: 123 + ] + message.body >> [ + foo: 123 + ] + expect: + predicate.matches(exchange) + } +} diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootConsulExample.java b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootConsulExample.java index 538edfe53d..9cc35ccab1 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootConsulExample.java +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootConsulExample.java @@ -43,6 +43,7 @@ public class StubRunnerBootConsulExample { -Dstubrunner.ids=org.springframework.cloud.contract.verifier.stubs:loanIssuance,org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer,org.springframework.cloud.contract.verifier.stubs:bootService -Dstubrunner.idsToServiceIds.fraudDetectionServer=someNameThatShouldMapFraudDetectionServer -Dstubrunner.cloud.consul.enabled=true +-Dstubrunner.camel.enabled=false -Dspring.cloud.zookeeper.enabled=false -Deureka.client.enabled=false -Dspring.cloud.zookeeper.discovery.enabled=false diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootEurekaExample.java b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootEurekaExample.java index a8dcc118f3..7973900003 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootEurekaExample.java +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootEurekaExample.java @@ -56,6 +56,7 @@ public class StubRunnerBootEurekaExample { -Dstubrunner.cloud.eureka.enabled=true -Dstubrunner.repositoryRoot=classpath:m2repo/repository/ +-Dstubrunner.camel.enabled=false -Dspring.cloud.zookeeper.enabled=false -Dspring.cloud.zookeeper.discovery.enabled=false -Ddebug=true diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootZookeeperExample.java b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootZookeeperExample.java index 984722d66c..8dea7638dc 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootZookeeperExample.java +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/serverexamples/StubRunnerBootZookeeperExample.java @@ -42,6 +42,7 @@ public class StubRunnerBootZookeeperExample { -Dstubrunner.idsToServiceIds.fraudDetectionServer=someNameThatShouldMapFraudDetectionServer -Dstubrunner.cloud.stubbed.discovery.enabled=false -Dstubrunner.cloud.zookeepr.enabled=true +-Dstubrunner.camel.enabled=false -Dstubrunner.repositoryRoot=classpath:m2repo/repository/ -Deureka.client.enabled=false -Ddebug=true diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index 2773b5f414..d41c4d169f 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -30,6 +30,26 @@ spring-messaging true + + org.apache.camel + camel-spring-boot-starter + true + + + org.apache.camel + camel-spring + true + + + org.apache.camel + camel-core + true + + + org.apache.camel + camel-kafka + true + org.springframework.cloud spring-cloud-stream-test-support diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelStubMessages.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelStubMessages.java new file mode 100644 index 0000000000..502c81c1ef --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelStubMessages.java @@ -0,0 +1,89 @@ +/* + * 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.verifier.messaging.camel; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.camel.CamelContext; +import org.apache.camel.ConsumerTemplate; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.impl.DefaultExchange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.contract.verifier.messaging.MessageVerifier; +import org.springframework.stereotype.Component; + +/** + * @author Marcin Grzejszczak + */ +@Component +public class CamelStubMessages implements MessageVerifier { + + private static final Logger log = LoggerFactory.getLogger( + CamelStubMessages.class); + + private final CamelContext context; + private final ContractVerifierCamelMessageBuilder builder; + + @Autowired + public CamelStubMessages(CamelContext context) { + this.context = context; + this.builder = new ContractVerifierCamelMessageBuilder(context); + } + + @Override + public void send(Message message, String destination) { + try { + ProducerTemplate producerTemplate = this.context.createProducerTemplate(); + Exchange exchange = new DefaultExchange(this.context); + exchange.setIn(message); + producerTemplate.send(destination, exchange); + } catch (Exception e) { + log.error("Exception occurred while trying to send a message [" + message + "] " + + "to a channel with name [" + destination + "]", e); + throw e; + } + } + + @Override + public void send(T payload, Map headers, String destination) { + send(this.builder.create(payload, headers), destination); + } + + @Override + public Message receive(String destination, long timeout, TimeUnit timeUnit) { + try { + ConsumerTemplate consumerTemplate = this.context.createConsumerTemplate(); + Exchange exchange = consumerTemplate.receive(destination, timeUnit.toMillis(timeout)); + return exchange.getIn(); + } catch (Exception e) { + log.error("Exception occurred while trying to read a message from " + + " a channel with name [" + destination + "]", e); + throw new IllegalStateException(e); + } + } + + @Override + public Message receive(String destination) { + return receive(destination, 5, TimeUnit.SECONDS); + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java new file mode 100644 index 0000000000..33da68db09 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java @@ -0,0 +1,66 @@ +/* + * 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.verifier.messaging.camel; + +import org.apache.camel.CamelContext; +import org.apache.camel.Message; +import org.apache.camel.spring.boot.CamelAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.*; +import org.springframework.cloud.contract.verifier.messaging.MessageVerifier; +import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage; +import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging; +import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration; +import org.springframework.context.annotation.*; + +/** + * @author Marcin Grzejszczak + */ +@Configuration +@ConditionalOnClass(Message.class) +@Import(CamelAutoConfiguration.class) +@ConditionalOnProperty(name="stubrunner.camel.enabled", havingValue="true", matchIfMissing = true) +@AutoConfigureBefore(NoOpContractVerifierAutoConfiguration.class) +public class ContractVerifierCamelConfiguration { + + @Bean + @ConditionalOnMissingBean + MessageVerifier contractVerifierMessageExchange( + CamelContext camelContext) { + return new CamelStubMessages(camelContext); + } + + @Bean + @ConditionalOnMissingBean + public ContractVerifierMessaging contractVerifierMessaging( + MessageVerifier exchange) { + return new ContractVerifierCamelHelper(exchange); + } +} + +class ContractVerifierCamelHelper extends ContractVerifierMessaging { + + public ContractVerifierCamelHelper( + MessageVerifier exchange) { + super(exchange); + } + + @Override + protected ContractVerifierMessage convert(Message receive) { + return new ContractVerifierMessage(receive.getBody(), receive.getHeaders()); + } +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelMessageBuilder.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelMessageBuilder.java new file mode 100644 index 0000000000..bd17e1991a --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelMessageBuilder.java @@ -0,0 +1,43 @@ +/* + * 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.verifier.messaging.camel; + +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.Message; +import org.apache.camel.impl.DefaultMessage; + +/** + * @author Marcin Grzejszczak + */ +class ContractVerifierCamelMessageBuilder { + + private final CamelContext context; + + ContractVerifierCamelMessageBuilder(CamelContext context) { + this.context = context; + } + + public Message create(T payload, Map headers) { + DefaultMessage message = new DefaultMessage(this.context); + message.setBody(payload); + message.setHeaders(headers); + return message; + } + +} diff --git a/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories index c7cfc35629..0e6be1c75d 100644 --- a/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories @@ -4,4 +4,5 @@ org.springframework.cloud.contract.verifier.messaging.stream.ContractVerifierStr org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration,\ org.springframework.cloud.contract.verifier.messaging.amqp.ContractVerifierAmqpAutoConfiguration,\ org.springframework.cloud.contract.verifier.messaging.amqp.RabbitMockConnectionFactoryAutoConfiguration,\ +org.springframework.cloud.contract.verifier.messaging.camel.ContractVerifierCamelConfiguration,\ org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration \ No newline at end of file diff --git a/tests/pom.xml b/tests/pom.xml index 3679607d51..ef343e744c 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -18,6 +18,7 @@ Spring Cloud Contract Tests + samples-messaging-camel samples-messaging-stream samples-messaging-integration samples-messaging-amqp diff --git a/tests/samples-messaging-camel/.springBeans b/tests/samples-messaging-camel/.springBeans new file mode 100644 index 0000000000..b74cfc29b8 --- /dev/null +++ b/tests/samples-messaging-camel/.springBeans @@ -0,0 +1,16 @@ + + + 1 + + + + + + + java:com.example.fraud.CamelMessagingApplication + + + + + + diff --git a/tests/samples-messaging-camel/pom.xml b/tests/samples-messaging-camel/pom.xml new file mode 100644 index 0000000000..9b279df007 --- /dev/null +++ b/tests/samples-messaging-camel/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-contract-tests + 2.1.0.BUILD-SNAPSHOT + .. + + spring-cloud-contract-sample-camel + jar + Spring Cloud Contract Sample Camel + Spring Cloud Contract Sample Camel + + + org.apache.camel + camel-spring-boot-starter + + + org.apache.camel + camel-jms + + + org.apache.activemq + activemq-camel + + + org.apache.activemq + activemq-pool + + + org.apache.camel + camel-jackson + + + org.springframework.cloud + spring-cloud-contract-verifier + test + + + org.spockframework + spock-spring + test + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/tests/samples-messaging-camel/src/main/java/com/example/BookDeleted.java b/tests/samples-messaging-camel/src/main/java/com/example/BookDeleted.java new file mode 100644 index 0000000000..03572a97c6 --- /dev/null +++ b/tests/samples-messaging-camel/src/main/java/com/example/BookDeleted.java @@ -0,0 +1,32 @@ +/* + * 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 com.example; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +@SuppressWarnings("serial") +public class BookDeleted implements Serializable { + public final String bookName; + + @JsonCreator + public BookDeleted(@JsonProperty("bookName") String bookName) { + this.bookName = bookName; + } +} diff --git a/tests/samples-messaging-camel/src/main/java/com/example/BookDeleter.java b/tests/samples-messaging-camel/src/main/java/com/example/BookDeleter.java new file mode 100644 index 0000000000..31e051f696 --- /dev/null +++ b/tests/samples-messaging-camel/src/main/java/com/example/BookDeleter.java @@ -0,0 +1,45 @@ +/* + * 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 com.example; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.camel.Exchange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class BookDeleter { + + private static final Logger log = LoggerFactory.getLogger(BookDeleter.class); + + /** + * Scenario for "should generate tests triggered by a message": client side: if sends + * a message to input.messageFrom then message will be sent to output.messageFrom + * server side: will send a message to input, verify the message contents and await + * upon receiving message on the output messageFrom + */ + public void bookDeleted(Exchange exchange) { + BookDeleted bookDeleted = exchange.getIn().getBody(BookDeleted.class); + log.info("Deleting book " + bookDeleted); + this.bookSuccessfulyDeleted.set(true); + log.info("Book successfuly deleted [" + this.bookSuccessfulyDeleted + "]"); + } + + private AtomicBoolean bookSuccessfulyDeleted = new AtomicBoolean(false); +} diff --git a/tests/samples-messaging-camel/src/main/java/com/example/BookReturned.java b/tests/samples-messaging-camel/src/main/java/com/example/BookReturned.java new file mode 100644 index 0000000000..6eea1a5780 --- /dev/null +++ b/tests/samples-messaging-camel/src/main/java/com/example/BookReturned.java @@ -0,0 +1,32 @@ +/* + * 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 com.example; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +@SuppressWarnings("serial") +public class BookReturned implements Serializable { + public final String bookName; + + @JsonCreator + BookReturned(@JsonProperty("bookName") String bookName) { + this.bookName = bookName; + } +} diff --git a/tests/samples-messaging-camel/src/main/java/com/example/BookRouteConfiguration.java b/tests/samples-messaging-camel/src/main/java/com/example/BookRouteConfiguration.java new file mode 100644 index 0000000000..3f4f6c9022 --- /dev/null +++ b/tests/samples-messaging-camel/src/main/java/com/example/BookRouteConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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 com.example; + +import org.apache.activemq.camel.component.ActiveMQComponent; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.model.dataformat.JsonLibrary; +import org.apache.camel.spring.SpringRouteBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Marcin Grzejszczak + */ +@Configuration +public class BookRouteConfiguration { + + @Bean + ActiveMQComponent activeMQComponent(@Value("${activemq.url:vm://localhost?broker.persistent=false}") String url) { + ActiveMQComponent component = new ActiveMQComponent(); + component.setBrokerURL(url); + return component; + } + + @Bean + RoutesBuilder myRouter(final BookService bookService, final BookDeleter bookDeleter) { + return new SpringRouteBuilder() { + + @Override + public void configure() throws Exception { + // scenario 1 - from bean to output + from("direct:start").unmarshal().json(JsonLibrary.Jackson, BookReturned.class).bean(bookService).to("jms:output"); + // scenario 2 - from input to output + from("jms:input").unmarshal().json(JsonLibrary.Jackson, BookReturned.class).bean(bookService).to("jms:output"); + // scenario 3 - from input to no output + from("jms:delete").unmarshal().json(JsonLibrary.Jackson, BookDeleted.class).bean(bookDeleter); + } + + }; + } +} diff --git a/tests/samples-messaging-camel/src/main/java/com/example/BookService.java b/tests/samples-messaging-camel/src/main/java/com/example/BookService.java new file mode 100644 index 0000000000..a145e482d6 --- /dev/null +++ b/tests/samples-messaging-camel/src/main/java/com/example/BookService.java @@ -0,0 +1,43 @@ +/* + * 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 com.example; + +import org.apache.camel.Exchange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class BookService { + + private static final Logger log = LoggerFactory.getLogger(BookService.class); + + /** + * Scenario for "should generate tests triggered by a method": client side: must have + * a possibility to "trigger" sending of a message to the given messageFrom server + * side: will run the method and await upon receiving message on the output + * messageFrom + * + * Method triggers sending a message to a source + */ + public void returnBook(Exchange exchange) { + BookReturned bookReturned = exchange.getIn().getBody(BookReturned.class); + log.info("Returning book [$bookReturned]"); + exchange.getOut().setBody(bookReturned); + exchange.getOut().setHeader("BOOK-NAME", bookReturned.bookName); + } +} diff --git a/tests/samples-messaging-camel/src/main/java/com/example/CamelMessagingApplication.java b/tests/samples-messaging-camel/src/main/java/com/example/CamelMessagingApplication.java new file mode 100644 index 0000000000..25ff4fccc4 --- /dev/null +++ b/tests/samples-messaging-camel/src/main/java/com/example/CamelMessagingApplication.java @@ -0,0 +1,28 @@ +/* + * 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 com.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CamelMessagingApplication { + + public static void main(String[] args) { + SpringApplication.run(CamelMessagingApplication.class, args); + } +} diff --git a/tests/samples-messaging-camel/src/test/groovy/com/example/CamelMessagingApplicationSpec.groovy b/tests/samples-messaging-camel/src/test/groovy/com/example/CamelMessagingApplicationSpec.groovy new file mode 100644 index 0000000000..2785036d98 --- /dev/null +++ b/tests/samples-messaging-camel/src/test/groovy/com/example/CamelMessagingApplicationSpec.groovy @@ -0,0 +1,157 @@ +/* + * 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 com.example + +import javax.inject.Inject + +import org.apache.camel.Message +import org.apache.camel.model.ModelCamelContext +import org.junit.BeforeClass +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootContextLoader +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.messaging.MessageVerifier +import org.springframework.cloud.contract.verifier.messaging.boot.AutoConfigureMessageVerifier +import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper; +import org.springframework.test.context.ContextConfiguration + +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import com.jayway.jsonpath.DocumentContext +import com.jayway.jsonpath.JsonPath +import com.toomuchcoding.jsonassert.JsonAssertion +/** + * SPIKE ON TESTS FROM NOTES IN MessagingSpec + */ +// Context configuration would end up in base class +@ContextConfiguration(classes = [CamelMessagingApplication], loader = SpringBootContextLoader) +@AutoConfigureMessageVerifier +class CamelMessagingApplicationSpec extends Specification { + + // ALL CASES + @Autowired ModelCamelContext camelContext + @Autowired BookDeleter bookDeleter + @Inject MessageVerifier contractVerifierMessaging + + ContractVerifierObjectMapper contractVerifierObjectMapper = new ContractVerifierObjectMapper() + + @BeforeClass + static void init() { + System.setProperty("org.apache.activemq.SERIALIZABLE_PACKAGES", "*") + } + + def "should work for triggered based messaging"() { + given: + Contract.make { + label 'some_label' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('activemq:output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } + } + // generated test should look like this: + when: + bookReturnedTriggered() + then: + def response = contractVerifierMessaging.receive('activemq:output') + response.headers.get('BOOK-NAME') == 'foo' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.body)) + JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo') + } + + def "should generate tests triggered by a message"() { + given: + Contract.make { + label 'some_label' + input { + messageFrom('jms:input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + + } + } + outputMessage { + sentTo('jms:output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + // generated test should look like this: + when: + contractVerifierMessaging.send( + contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']), + [sample: 'header'], 'jms:input') + then: + def response = contractVerifierMessaging.receive('jms:output') + response.headers.get('BOOK-NAME') == 'foo' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.body)) + JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo') + } + + def "should generate tests without destination, triggered by a message"() { + given: + Contract.make { + label 'some_label' + input { + messageFrom('jms:delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + // generated test should look like this: + when: + contractVerifierMessaging.send(contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']), + [sample: 'header'], 'jms:delete') + then: + noExceptionThrown() + bookWasDeleted() + } + + void bookReturnedTriggered() { + camelContext.createProducerTemplate().sendBody('direct:start', '''{"bookName" : "foo" }''') + } + + PollingConditions pollingConditions = new PollingConditions() + + void bookWasDeleted() { + pollingConditions.eventually { + assert bookDeleter.bookSuccessfulyDeleted.get() + } + } + +} \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-camel/README.adoc b/tests/spring-cloud-contract-stub-runner-camel/README.adoc new file mode 100644 index 0000000000..30c50c8e55 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-camel/README.adoc @@ -0,0 +1,123 @@ +:input_name: jms:input +:output_name: jms:output + +=== Stub Runner Camel + +Spring Cloud Contract Verifier Stub Runner's messaging module gives you an easy way to integrate with Apache Camel. +For the provided artifacts it will automatically download the stubs and register the required +routes. + +==== Adding it to the project + +It's enough to have both Apache Camel and Spring Cloud Contract Stub Runner on classpath. +Remember to annotate your test class with `@AutoConfigureStubRunner`. + +==== Disabling the functionality + +If you need to disable this functionality just pass `stubrunner.camel.enabled=false` property. + +==== Examples + +===== Stubs structure + +Let us assume that we have the following Maven repository with a deployed stubs for the +`camelService` application. + +[source,bash,indent=0] +---- +└── .m2 + └── repository + └── io + └── codearte + └── accurest + └── stubs + └── camelService + ├── 0.0.1-SNAPSHOT + │   ├── camelService-0.0.1-SNAPSHOT.pom + │   ├── camelService-0.0.1-SNAPSHOT-stubs.jar + │   └── maven-metadata-local.xml + └── maven-metadata-local.xml +---- + +And the stubs contain the following structure: + +[source,bash,indent=0] +---- +├── META-INF +│   └── MANIFEST.MF +└── repository + ├── accurest + │   ├── bookDeleted.groovy + │   ├── bookReturned1.groovy + │   └── bookReturned2.groovy + └── mappings +---- + +Let's consider the following contracts (let' number it with *1*): + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=sample_dsl,indent=0] +---- + +and number *2* + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0] +---- + +===== Scenario 1 (no input message) + +So as to trigger a message via the `return_book_1` label we'll use the `StubTigger` interface as follows + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger,indent=0] +---- + +Next we'll want to listen to the output of the message sent to `{output_name}` + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0] +---- + +And the received message would pass the following assertions + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger_message,indent=0] +---- + +===== Scenario 2 (output triggered by input) + +Since the route is set for you it's enough to just send a message to the `{output_name}` destination. + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_send,indent=0] +---- + +Next we'll want to listen to the output of the message sent to `{output_name}` + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_receive,indent=0] +---- + +And the received message would pass the following assertions + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_receive_message,indent=0] +---- + +===== Scenario 3 (input with no output) + +Since the route is set for you it's enough to just send a message to the `{output_name}` destination. + +[source,groovy] +---- +include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=trigger_no_output,indent=0] +---- diff --git a/tests/spring-cloud-contract-stub-runner-camel/pom.xml b/tests/spring-cloud-contract-stub-runner-camel/pom.xml new file mode 100644 index 0000000000..b1f7906e63 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-camel/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-contract-tests + 2.1.0.BUILD-SNAPSHOT + .. + + spring-cloud-contract-stub-runner-camel + jar + Spring Cloud Contract Stub Runner Camel + Spring Cloud Contract Stub Runner Camel + + + org.springframework.cloud + spring-cloud-contract-stub-runner + + + org.springframework.cloud + spring-cloud-starter-contract-stub-runner-jetty + test + + + org.apache.camel + camel-spring-boot-starter + + + org.apache.camel + camel-jackson + + + junit + junit + test + + + org.codehaus.groovy + groovy + + + org.spockframework + spock-core + test + + + org.spockframework + spock-spring + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.apache.activemq + activemq-camel + test + + + org.apache.activemq + activemq-pool + test + + + org.springframework.boot + spring-boot-starter-web + test + + + info.solidsoft.spock + spock-global-unroll + test + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + + testCompile + + + + + + + diff --git a/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/BookReturned.groovy b/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/BookReturned.groovy new file mode 100644 index 0000000000..26428fd9fd --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/BookReturned.groovy @@ -0,0 +1,32 @@ +/* + * 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.messaging.camel + +import com.fasterxml.jackson.annotation.JsonCreator +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode + +@CompileStatic +@EqualsAndHashCode +class BookReturned implements Serializable { + final String bookName + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + BookReturned(String bookName) { + this.bookName = bookName + } +} 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..8c6c525c5b --- /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,254 @@ +/* + * 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.messaging.camel + +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import org.apache.activemq.camel.component.ActiveMQComponent +import org.apache.activemq.spring.ActiveMQConnectionFactory +import org.apache.camel.CamelContext +import org.apache.camel.Exchange +import org.apache.camel.component.jms.JmsConfiguration +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.context.ContextConfiguration +import spock.lang.IgnoreIf +import spock.lang.Specification +/** + * @author Marcin Grzejszczak + */ +@ContextConfiguration(classes = Config, loader = SpringBootContextLoader) +@SpringBootTest(properties = "debug=true") +@AutoConfigureStubRunner +@IgnoreIf({ os.windows }) +class CamelStubRunnerSpec extends Specification { + + @Autowired StubFinder stubFinder + @Autowired CamelContext camelContext + + def setup() { + // ensure that message were taken from the queue + camelContext.createConsumerTemplate().receive('jms:output', 100) + } + + def 'should download the stub and register a route for it'() { + when: + // tag::client_send[] + camelContext.createProducerTemplate().sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header']) + // end::client_send[] + then: + // tag::client_receive[] + Exchange receivedMessage = camelContext.createConsumerTemplate().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 = camelContext.createConsumerTemplate().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 = camelContext.createConsumerTemplate().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 = camelContext.createConsumerTemplate().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 = camelContext.createConsumerTemplate().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[] + camelContext.createProducerTemplate().sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header']) + // end::trigger_no_output[] + then: + noExceptionThrown() + } + + def 'should not trigger a message that does not match input'() { + when: + camelContext.createProducerTemplate().sendBodyAndHeaders('jms:input', new BookReturned('notmatching'), [wrong: 'header_value']) + then: + Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 100) + and: + receivedMessage == null + } + + 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-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessorSpec.groovy b/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessorSpec.groovy new file mode 100644 index 0000000000..279782c65e --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelProcessorSpec.groovy @@ -0,0 +1,98 @@ +package org.springframework.cloud.contract.stubrunner.messaging.camel + +import org.apache.camel.CamelContext +import org.apache.camel.Exchange +import org.apache.camel.builder.ExchangeBuilder +import org.apache.camel.spring.SpringCamelContext +import org.springframework.cloud.contract.spec.Contract +import spock.lang.Specification + +class StubRunnerCamelProcessorSpec extends Specification { + + CamelContext camelContext = new SpringCamelContext() + Exchange message = ExchangeBuilder.anExchange(camelContext).build() + + def noOutputMessageContract = Contract.make { + label 'return_book_2' + input { + messageFrom('bookStorage') + messageBody([ + bookId: $(consumer(regex('[0-9]+')), producer('123')) + ]) + messageHeaders { + header('sample', 'header') + } + } + } + + def 'should not process the message if there is no output message'() { + given: + StubRunnerCamelProcessor processor = new StubRunnerCamelProcessor(noOutputMessageContract) + when: + processor.process(message) + then: + noExceptionThrown() + } + + def dsl = Contract.make { + label 'return_book_2' + input { + messageFrom('bookStorage') + messageBody([ + bookId: $(consumer(regex('[0-9]+')), producer('123')) + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('returnBook') + body([ + responseId: $(producer(regex('[0-9]+')), consumer('123')) + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + + def 'should process message when it has an output message section'() { + given: + StubRunnerCamelProcessor processor = new StubRunnerCamelProcessor(dsl) + when: + processor.process(message) + then: + message.getIn().getBody(String) == '{"responseId":"123"}' + } + + def dslWithRegexInGString = Contract.make { + // Human readable description + description 'Should produce valid sensor data' + // Label by means of which the output message can be triggered + label 'sensor1' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('createSensorData()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo 'sensor-data' + headers { + header('contentType': 'application/json') + } + // the body of the output message + body("""{"id":"${value(producer(regex('[0-9]+')), consumer('99'))}","temperature":"123.45"}""") + } + } + + def 'should convert dsl into message with regex in GString'() { + given: + StubRunnerCamelProcessor processor = new StubRunnerCamelProcessor(dslWithRegexInGString) + when: + processor.process(message) + then: + message.getIn().getBody(String) == '''{"id":"99","temperature":"123.45"}''' + } +} diff --git a/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/application.yml b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/application.yml new file mode 100644 index 0000000000..b1b686626b --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/application.yml @@ -0,0 +1,7 @@ +stubrunner: + repositoryRoot: classpath:m2repo/repository/ + ids: org.springframework.cloud.contract.verifier.stubs:camelService:0.0.1-SNAPSHOT:stubs + stubs-mode: remote +server: + port: 0 +debug: true \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/camelService-0.0.1-SNAPSHOT-stubs.jar b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/camelService-0.0.1-SNAPSHOT-stubs.jar new file mode 100644 index 0000000000..b27b155a3e Binary files /dev/null and b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/camelService-0.0.1-SNAPSHOT-stubs.jar differ diff --git a/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/camelService-0.0.1-SNAPSHOT.pom b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/camelService-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..b2210f65ff --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/camelService-0.0.1-SNAPSHOT.pom @@ -0,0 +1,25 @@ + + + + + 4.0.0 + org.springframework.cloud.contract.verifier.stubs + camelService + 0.0.1-SNAPSHOT + pom + diff --git a/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/maven-metadata-local.xml b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 0000000000..f0954907f2 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/0.0.1-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + camelService + 0.0.1-SNAPSHOT + + + true + + 20160409062112 + + diff --git a/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata-local.xml b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata-local.xml new file mode 100644 index 0000000000..00bf4217ee --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata-local.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + camelService + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + + diff --git a/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata.xml b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata.xml new file mode 100644 index 0000000000..00bf4217ee --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-camel/src/test/resources/m2repo/repository/org/springframework/cloud/contract/verifier/stubs/camelService/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + + org.springframework.cloud.contract.verifier.stubs + camelService + 0.0.1-SNAPSHOT + + + 0.0.1-SNAPSHOT + + 20160409062112 + +